diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0e09aa85a7c..3edc2342bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -27,88 +27,268 @@ public static function getClassLikeNames(): array 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 === 'fetch') { + return self::handleFetch($event); + } + + if ($method_name_lowercase === 'fetchall') { + return self::handleFetchAll($event); + } + + return null; + } + + private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Union + { $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]) + $fetch_mode = 0; + + 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(), - ]), + 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 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 7: // PDO::FETCH_COLUMN - scalar|null|false + return new Union([ + new TScalar(), + new TNull(), + new TFalse(), + ]); + + case 8: // PDO::FETCH_CLASS - object|false + return new Union([ + 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(), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ]), + new TFalse(), + ]); + + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + + 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(); + $fetch_mode = 0; + + 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 = 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 6: // PDO::FETCH_BOUND - bool - return Type::getBool(); - - case 8: // PDO::FETCH_CLASS - object|false - return new Union([ - 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()), + ), + ]); + + 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 3: // PDO::FETCH_NUM - list|false - return new Union([ - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), + ), + ]); + + case 6: // PDO::FETCH_BOUND - list + return new Union([ + Type::getListAtomic( + Type::getBool(), + ), + ]); + + case 7: // PDO::FETCH_COLUMN - list + return new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]); + + case 8: // PDO::FETCH_CLASS - list + return new Union([ + Type::getListAtomic( + new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + ]), + ), + ]); + + case 11: // PDO::FETCH_NAMED - list>> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + 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(), - ]); - } + ]), + ), + ]); + + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + + 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 - list + return new Union([ + Type::getListAtomic( + new Union([ + new TNamedObject('stdClass'), + ]), + ), + ]); } return null; diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 3b656752bcf..f6cb3e75099 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -503,14 +503,48 @@ class A { /** @var ?string */ public $a; } + class B extends A {} $db = new PDO("sqlite::memory:"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); + $stmt2 = $db->prepare("select \"a\" as a"); + $stmt2->setFetchMode(PDO::FETCH_ASSOC); + $stmt3 = $db->prepare("select \"a\" as a"); + $stmt3->setFetchMode(PDO::ATTR_DEFAULT_FETCH_MODE); $stmt->execute(); + $stmt2->execute(); + /** @psalm-suppress MixedAssignment */ + $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); + /** @psalm-suppress MixedAssignment */ + $h = $stmt2->fetch(); + $i = $stmt2->fetchAll(); + $j = $stmt2->fetch(PDO::FETCH_BOTH); + $k = $stmt2->fetchAll(PDO::FETCH_BOTH); /** @psalm-suppress MixedAssignment */ - $a = $stmt->fetch();', + $l = $stmt3->fetch();', + 'assertions' => [ + '$a' => 'mixed', + '$b' => 'array|false', + '$c' => 'false|object', + '$d' => 'list', + '$e' => 'list', + '$f' => 'array|false', + '$g' => 'list>', + '$h' => 'mixed', + '$i' => 'array|false', + '$j' => 'array|false', + '$k' => 'list>', + '$l' => 'mixed', + ], ], 'datePeriodConstructor' => [ 'code' => ' [ + 'code' => 'prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchAllColumn' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_KEY_PAIR); + }', + ], + 'pdoStatementFetchAllKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_KEY_PAIR); + }', + ], 'pdoStatementFetchAssoc' => [ 'code' => '|false */ @@ -617,6 +691,16 @@ function fetch_assoc() { return $sth->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 +711,16 @@ function fetch_both() { 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' => ' [ 'code' => '>|false */ + /** @return array>|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); @@ -667,6 +793,16 @@ function fetch_named() { 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 +813,16 @@ function fetch_named() { 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' => '