Skip to content

Commit 86f8b7a

Browse files
phpstan-botVincentLangletclaude
authored
Return list<mixed> from PDOStatement::fetchAll() (#5643)
Co-authored-by: Vincent Langlet <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Vincent Langlet <[email protected]>
1 parent 3b4e3e7 commit 86f8b7a

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PDO;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
use PHPStan\Type\Accessory\AccessoryArrayListType;
12+
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use PHPStan\Type\TypeUtils;
20+
use function count;
21+
22+
#[AutowiredService]
23+
final class PdoStatementFetchAllReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
26+
public function getClass(): string
27+
{
28+
return 'PDOStatement';
29+
}
30+
31+
public function isMethodSupported(MethodReflection $methodReflection): bool
32+
{
33+
return $methodReflection->getName() === 'fetchAll';
34+
}
35+
36+
public function getTypeFromMethodCall(
37+
MethodReflection $methodReflection,
38+
MethodCall $methodCall,
39+
Scope $scope,
40+
): ?Type
41+
{
42+
$args = $methodCall->getArgs();
43+
if (count($args) < 1) {
44+
return null;
45+
}
46+
47+
$modeType = $scope->getType($args[0]->value);
48+
$constantIntegers = TypeUtils::getConstantIntegers($modeType);
49+
50+
if (count($constantIntegers) === 0) {
51+
return null;
52+
}
53+
54+
foreach ($constantIntegers as $constantInteger) {
55+
$mode = $constantInteger->getValue();
56+
if (
57+
($mode & 0xFFFF) === PDO::FETCH_KEY_PAIR
58+
|| ($mode & PDO::FETCH_GROUP) !== 0
59+
|| ($mode & PDO::FETCH_UNIQUE) !== 0
60+
) {
61+
return null;
62+
}
63+
}
64+
65+
$variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants());
66+
$returnType = $variant->getReturnType();
67+
68+
$listType = TypeCombinator::intersect(
69+
new ArrayType(new IntegerType(), new MixedType()),
70+
new AccessoryArrayListType(),
71+
);
72+
73+
if (!$returnType->isFalse()->no()) {
74+
return TypeCombinator::union($listType, new ConstantBooleanType(false));
75+
}
76+
77+
return $listType;
78+
}
79+
80+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php // lint < 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11889Php7;
6+
7+
use PDO;
8+
use PDOStatement;
9+
use function PHPStan\Testing\assertType;
10+
11+
function test(PDOStatement $stmt): void
12+
{
13+
assertType('list|false', $stmt->fetchAll(PDO::FETCH_ASSOC));
14+
assertType('list|false', $stmt->fetchAll(PDO::FETCH_COLUMN));
15+
16+
// Non-list modes
17+
assertType('array|false', $stmt->fetchAll(PDO::FETCH_KEY_PAIR));
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11889;
6+
7+
use PDO;
8+
use PDOStatement;
9+
use function PHPStan\Testing\assertType;
10+
11+
function test(PDOStatement $stmt): void
12+
{
13+
// No mode argument - unknown default, stays as array
14+
assertType('array', $stmt->fetchAll());
15+
16+
// Single-argument modes that return lists
17+
assertType('list', $stmt->fetchAll(PDO::FETCH_ASSOC));
18+
assertType('list', $stmt->fetchAll(PDO::FETCH_NUM));
19+
assertType('list', $stmt->fetchAll(PDO::FETCH_BOTH));
20+
assertType('list', $stmt->fetchAll(PDO::FETCH_OBJ));
21+
assertType('list', $stmt->fetchAll(PDO::FETCH_COLUMN));
22+
assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS));
23+
assertType('list', $stmt->fetchAll(PDO::FETCH_NAMED));
24+
25+
// Modes that return non-list arrays
26+
assertType('array', $stmt->fetchAll(PDO::FETCH_KEY_PAIR));
27+
assertType('array', $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC));
28+
assertType('array', $stmt->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC));
29+
30+
// Multi-argument overload variants always return lists
31+
assertType('list', $stmt->fetchAll(PDO::FETCH_COLUMN, 0));
32+
assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS, \stdClass::class));
33+
assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS, \stdClass::class, []));
34+
assertType('list', $stmt->fetchAll(PDO::FETCH_FUNC, function () {
35+
return 'test';
36+
}));
37+
}
38+
39+
/**
40+
* @return list<string>
41+
*/
42+
function get_cv_files(): array
43+
{
44+
$pdo = new PDO("");
45+
$stmt = $pdo->prepare('SELECT `file` FROM `commonvoice`');
46+
$stmt->execute();
47+
48+
return $stmt->fetchAll(PDO::FETCH_COLUMN);
49+
}

0 commit comments

Comments
 (0)