diff --git a/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php new file mode 100644 index 00000000000..722343d0e3e --- /dev/null +++ b/src/Type/Php/PdoStatementFetchAllReturnTypeExtension.php @@ -0,0 +1,80 @@ +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 & 0xFFFF) === PDO::FETCH_KEY_PAIR + || ($mode & PDO::FETCH_GROUP) !== 0 + || ($mode & PDO::FETCH_UNIQUE) !== 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-php7.php b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php new file mode 100644 index 00000000000..528b8af0f9b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11889-php7.php @@ -0,0 +1,18 @@ +fetchAll(PDO::FETCH_ASSOC)); + assertType('list|false', $stmt->fetchAll(PDO::FETCH_COLUMN)); + + // Non-list modes + assertType('array|false', $stmt->fetchAll(PDO::FETCH_KEY_PAIR)); +} 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); +}