From 6500e7194fb4064e8908c61f9c58272a4710f098 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 15 May 2026 13:12:53 +0200 Subject: [PATCH 1/5] Introduce PdoStatementFetchAllReturnTypeExtension --- ...doStatementFetchAllReturnTypeExtension.php | 79 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-11889.php | 49 ++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11889.php diff --git a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php new file mode 100644 index 00000000000..6bc83a58114 --- /dev/null +++ b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php @@ -0,0 +1,79 @@ +getName() === 'fetchAll'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope, + ): ?Type + { + $args = $methodCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $modeType = $scope->getType($args[0]->value); + $constantIntegers = TypeUtils::getConstantIntegers($modeType); + + if (count($constantIntegers) === 0) { + return null; + } + + foreach ($constantIntegers as $constantInteger) { + $mode = $constantInteger->getValue(); + if ($mode === 0 || ($mode & 0xFFFF) === self::FETCH_KEY_PAIR || ($mode & self::FETCH_GROUP) !== 0) { + return null; + } + } + + $variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants()); + $returnType = $variant->getReturnType(); + + $listType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), new MixedType()), + new AccessoryArrayListType(), + ); + + if (!$returnType->isFalse()->no()) { + return TypeCombinator::union($listType, new ConstantBooleanType(false)); + } + + return $listType; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11889.php b/tests/PHPStan/Analyser/nsrt/bug-11889.php new file mode 100644 index 00000000000..27343e0cace --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11889.php @@ -0,0 +1,49 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11889; + +use PDO; +use PDOStatement; +use function PHPStan\Testing\assertType; + +function test(PDOStatement $stmt): void +{ + // No mode argument - unknown default, stays as array + assertType('array', $stmt->fetchAll()); + + // Single-argument modes that return lists + assertType('list', $stmt->fetchAll(PDO::FETCH_ASSOC)); + assertType('list', $stmt->fetchAll(PDO::FETCH_NUM)); + assertType('list', $stmt->fetchAll(PDO::FETCH_BOTH)); + assertType('list', $stmt->fetchAll(PDO::FETCH_OBJ)); + assertType('list', $stmt->fetchAll(PDO::FETCH_COLUMN)); + assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS)); + assertType('list', $stmt->fetchAll(PDO::FETCH_NAMED)); + + // Modes that return non-list arrays + assertType('array', $stmt->fetchAll(PDO::FETCH_KEY_PAIR)); + assertType('array', $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC)); + assertType('array', $stmt->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC)); + + // Multi-argument overload variants always return lists + assertType('list', $stmt->fetchAll(PDO::FETCH_COLUMN, 0)); + assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS, \stdClass::class)); + assertType('list', $stmt->fetchAll(PDO::FETCH_CLASS, \stdClass::class, [])); + assertType('list', $stmt->fetchAll(PDO::FETCH_FUNC, function () { + return 'test'; + })); +} + +/** + * @return list + */ +function get_cv_files(): array +{ + $pdo = new PDO(""); + $stmt = $pdo->prepare('SELECT `file` FROM `commonvoice`'); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_COLUMN); +} From e56988deed2140054b0b310aaeba5ac23ecdc815 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 15 May 2026 14:08:13 +0200 Subject: [PATCH 2/5] Fix --- src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php index 6bc83a58114..d6ca10fa43a 100644 --- a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php +++ b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php @@ -22,10 +22,6 @@ final class PdoStatementFetchAllReturnTypeExtension implements DynamicMethodReturnTypeExtension { - private const FETCH_KEY_PAIR = 12; - - private const FETCH_GROUP = 0x10000; - public function getClass(): string { return 'PDOStatement'; @@ -56,7 +52,7 @@ public function getTypeFromMethodCall( foreach ($constantIntegers as $constantInteger) { $mode = $constantInteger->getValue(); - if ($mode === 0 || ($mode & 0xFFFF) === self::FETCH_KEY_PAIR || ($mode & self::FETCH_GROUP) !== 0) { + if ($mode === 0 || ($mode & 0xFFFF) === \PDO::FETCH_KEY_PAIR || ($mode & \PDO::FETCH_GROUP) !== 0) { return null; } } From baf366c0fae12e9f13fabb93afbca9a395b97033 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 15 May 2026 14:15:54 +0200 Subject: [PATCH 3/5] Fix --- src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php index d6ca10fa43a..722343d0e3e 100644 --- a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php +++ b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PDO; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; @@ -52,7 +53,11 @@ public function getTypeFromMethodCall( foreach ($constantIntegers as $constantInteger) { $mode = $constantInteger->getValue(); - if ($mode === 0 || ($mode & 0xFFFF) === \PDO::FETCH_KEY_PAIR || ($mode & \PDO::FETCH_GROUP) !== 0) { + if ( + ($mode & 0xFFFF) === PDO::FETCH_KEY_PAIR + || ($mode & PDO::FETCH_GROUP) !== 0 + || ($mode & PDO::FETCH_UNIQUE) !== 0 + ) { return null; } } From c86f9e619afad27f3157891646fc174adc47bcbd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 15 May 2026 13:07:39 +0000 Subject: [PATCH 4/5] Add test for list|false return type on PHP < 8.0 On PHP < 8.0, PDOStatement::fetchAll() returns array|false. The extension's false-union branch was untested. This adds a lint < 8.0 test covering list|false for list modes and array|false for non-list modes. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-11889-php7.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11889-php7.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php new file mode 100644 index 00000000000..332610d974a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php @@ -0,0 +1,18 @@ +|false', $stmt->fetchAll(PDO::FETCH_ASSOC)); + assertType('list|false', $stmt->fetchAll(PDO::FETCH_COLUMN)); + + // Non-list modes + assertType('array|false', $stmt->fetchAll(PDO::FETCH_KEY_PAIR)); +} From cc1417194b79197e72cebf77f03f99d1d85bc774 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 15 May 2026 15:17:47 +0200 Subject: [PATCH 5/5] Update tests/PHPStan/Analyser/nsrt/bug-11889-php7.php --- tests/PHPStan/Analyser/nsrt/bug-11889-php7.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php index 332610d974a..528b8af0f9b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php @@ -10,8 +10,8 @@ function test(PDOStatement $stmt): void { - assertType('list|false', $stmt->fetchAll(PDO::FETCH_ASSOC)); - assertType('list|false', $stmt->fetchAll(PDO::FETCH_COLUMN)); + assertType('list|false', $stmt->fetchAll(PDO::FETCH_ASSOC)); + assertType('list|false', $stmt->fetchAll(PDO::FETCH_COLUMN)); // Non-list modes assertType('array|false', $stmt->fetchAll(PDO::FETCH_KEY_PAIR));