From fc74ae83e66ef2ce329269aa4d123690f6319312 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 00:40:26 +0200 Subject: [PATCH] #9974 added return type detection for PDOStatement::fetchAll, extended return type detection for PDOStatement::fetch --- .../PdoStatementReturnTypeProvider.php | 297 ++++++++++++++---- tests/MethodCallTest.php | 111 ++++++- 2 files changed, 344 insertions(+), 64 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0e09aa85a..3f1addedc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -27,88 +27,261 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union { $config = Config::getInstance(); + $method_name_lowercase = $event->getMethodNameLowercase(); + + if (!$config->php_extensions["pdo"]) { + return null; + } + + if ($method_name_lowercase === 'setfetchmode') { + return self::handleSetFetchMode($event); + } + + if ($method_name_lowercase === 'fetch') { + return self::handleFetch($event); + } + + if ($method_name_lowercase === 'fetchall') { + return self::handleFetchAll($event); + } + + return null; + } + + private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) + { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $method_name_lowercase = $event->getMethodNameLowercase(); - if ($method_name_lowercase === 'fetch' - && $config->php_extensions["pdo"] - && isset($call_args[0]) + $context = $event->getContext(); + + if (isset($call_args[0]) + && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) + && $first_arg_type->isSingleIntLiteral() + ) { + $context->references_in_scope['fetch_mode'] = $first_arg_type->getSingleIntLiteral()->value; + } + + if (isset($call_args[1]) + && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) + && $second_arg_type->isSingleStringLiteral() + ) { + $context->references_in_scope['fetch_class'] = $second_arg_type->getSingleStringLiteral()->value; + } + + return null; + } + + private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Union + { + $source = $event->getSource(); + $call_args = $event->getCallArgs(); + $context = $event->getContext(); + + $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + + if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() ) { $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; + } - switch ($fetch_mode) { - case 2: // PDO::FETCH_ASSOC - array|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - new TNull(), + $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - array|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + new TFalse(), + ]); + + case 4: // PDO::FETCH_BOTH - array|false + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + new TFalse(), + ]); + + case 6: // PDO::FETCH_BOUND - bool + return Type::getBool(); + + case 8: // PDO::FETCH_CLASS - object|false + return new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + new TFalse(), + ]); + + case 1: // PDO::FETCH_LAZY - object|false + // This actually returns a PDORow object, but that class is + // undocumented, and its attributes are all dynamic anyway + return new Union([ + new TObject(), + new TFalse(), + ]); + + case 11: // PDO::FETCH_NAMED - array>|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + Type::getListAtomic(Type::getScalar()), + ]), + ]), + new TFalse(), + ]); + + case 3: // PDO::FETCH_NUM - list|false + return new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + new TFalse(), + ]); + + case 5: // PDO::FETCH_OBJ - stdClass|false + return new Union([ + new TNamedObject('stdClass'), + new TFalse(), + ]); + } + + return null; + } + + private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?Union + { + $source = $event->getSource(); + $call_args = $event->getCallArgs(); + $context = $event->getContext(); + + $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + + if (isset($call_args[0]) + && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) + && $first_arg_type->isSingleIntLiteral() + ) { + $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; + } + + $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + + if (isset($call_args[1]) + && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) + && $second_arg_type->isSingleStringLiteral() + ) { + $fetch_class_name = $second_arg_type->getSingleStringLiteral()->value; + } + + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); + ), + ]); - case 4: // PDO::FETCH_BOTH - array|false - return new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TScalar(), - new TNull(), + case 4: // PDO::FETCH_BOTH - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); + ), + ]); - case 6: // PDO::FETCH_BOUND - bool - return Type::getBool(); + case 6: // PDO::FETCH_BOUND - list + return new Union([ + Type::getListAtomic( + Type::getBool() + ), + ]); - case 8: // PDO::FETCH_CLASS - object|false - return new Union([ - new TObject(), - new TFalse(), - ]); + case 8: // PDO::FETCH_CLASS - list + return new Union([ + Type::getListAtomic( + new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() + ]), + ), + ]); - case 1: // PDO::FETCH_LAZY - object|false - // This actually returns a PDORow object, but that class is - // undocumented, and its attributes are all dynamic anyway - return new Union([ - new TObject(), - new TFalse(), - ]); + case 1: // PDO::FETCH_LAZY - list + // This actually returns a PDORow object, but that class is + // undocumented, and its attributes are all dynamic anyway + return new Union([ + Type::getListAtomic( + new Union([ + new TObject() + ]), + ), + ]); - case 11: // PDO::FETCH_NAMED - array>|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - Type::getListAtomic(Type::getScalar()), + case 11: // PDO::FETCH_NAMED - list>> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + Type::getListAtomic(Type::getScalar()), + ]), ]), ]), - new TFalse(), - ]); + ), + ]); - case 3: // PDO::FETCH_NUM - list|false - return new Union([ - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), - ]), - ), - new TFalse(), - ]); + case 3: // PDO::FETCH_NUM - list> + return new Union([ + Type::getListAtomic( + new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ), + ]); - case 5: // PDO::FETCH_OBJ - stdClass|false - return new Union([ - new TNamedObject('stdClass'), - new TFalse(), - ]); - } + case 5: // PDO::FETCH_OBJ - list + return new Union([ + Type::getListAtomic( + new Union([ + new TNamedObject('stdClass') + ]), + ), + ]); } return null; diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 3b656752b..760d93b65 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -503,14 +503,29 @@ class MethodCallTest extends TestCase /** @var ?string */ public $a; } + class B extends A {} $db = new PDO("sqlite::memory:"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); $stmt->execute(); - /** @psalm-suppress MixedAssignment */ - $a = $stmt->fetch();', + $a = $stmt->fetch(); + $b = $stmt->fetchAll(); + $c = $stmt->fetch(PDO::FETCH_CLASS); + $d = $stmt->fetchAll(PDO::FETCH_CLASS); + $e = $stmt->fetchAll(PDO::FETCH_CLASS, B::class); + $f = $stmt->fetch(PDO::FETCH_ASSOC); + $g = $stmt->fetchAll(PDO::FETCH_ASSOC);', + 'assertions' => [ + '$a' => 'A|false', + '$b' => 'list', + '$c' => 'A|false', + '$d' => 'list', + '$e' => 'list', + '$f' => 'array|false', + '$g' => 'list>', + ], ], 'datePeriodConstructor' => [ 'code' => 'fetch(PDO::FETCH_ASSOC); }', ], + 'pdoStatementFetchAllAssoc' => [ + 'code' => '> */ + function fetch_assoc() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_ASSOC); + }', + ], 'pdoStatementFetchBoth' => [ 'code' => '|false */ @@ -627,6 +652,16 @@ class MethodCallTest extends TestCase return $sth->fetch(PDO::FETCH_BOTH); }', ], + 'pdoStatementFetchAllBoth' => [ + 'code' => '> */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOTH); + }', + ], 'pdoStatementFetchBound' => [ 'code' => 'fetch(PDO::FETCH_BOUND); }', ], + 'pdoStatementFetchAllBound' => [ + 'code' => ' */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOUND); + }', + ], 'pdoStatementFetchClass' => [ 'code' => 'fetch(PDO::FETCH_CLASS); }', ], + 'pdoStatementFetchAllClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS); + }', + ], + 'pdoStatementFetchAllNamedClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS, Foo::class); + }', + ], 'pdoStatementFetchLazy' => [ 'code' => 'fetch(PDO::FETCH_LAZY); }', ], + 'pdoStatementFetchAllLazy' => [ + 'code' => ' */ + function fetch_lazy() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_LAZY); + }', + ], 'pdoStatementFetchNamed' => [ 'code' => '>|false */ @@ -667,6 +744,16 @@ class MethodCallTest extends TestCase return $sth->fetch(PDO::FETCH_NAMED); }', ], + 'pdoStatementFetchAllNamed' => [ + 'code' => '>> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NAMED); + }', + ], 'pdoStatementFetchNum' => [ 'code' => '|false */ @@ -677,6 +764,16 @@ class MethodCallTest extends TestCase return $sth->fetch(PDO::FETCH_NUM); }', ], + 'pdoStatementFetchAllNum' => [ + 'code' => '> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NUM); + }', + ], 'pdoStatementFetchObj' => [ 'code' => 'fetch(PDO::FETCH_OBJ); }', ], + 'pdoStatementFetchAllObj' => [ + 'code' => ' */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_OBJ); + }', + ], 'dateTimeSecondArg' => [ 'code' => '