From fc74ae83e66ef2ce329269aa4d123690f6319312 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 00:40:26 +0200 Subject: [PATCH 1/8] #9974 added return type detection for PDOStatement::fetchAll, extended return type detection for PDOStatement::fetch --- .../PdoStatementReturnTypeProvider.php | 311 ++++++++++++++---- tests/MethodCallTest.php | 111 ++++++- 2 files changed, 351 insertions(+), 71 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0e09aa85a7c..3f1addedc85 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -27,88 +27,261 @@ 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 === '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 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 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 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 - 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 - list>> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + Type::getListAtomic(Type::getScalar()), + ]), ]), - ), - new TFalse(), - ]); - - case 5: // PDO::FETCH_OBJ - stdClass|false - return new Union([ - new TNamedObject('stdClass'), - 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 - 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..760d93b65b5 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -503,14 +503,29 @@ class A { /** @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 @@ 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' => '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 @@ 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 +764,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' => ' Date: Thu, 27 Jul 2023 01:39:21 +0200 Subject: [PATCH 2/8] #9974 extended return type detection for PDOStatement::fetchAll/fetch with FETCH_COLUMN and FETCH_KEY_PAIR --- .../PdoStatementReturnTypeProvider.php | 77 ++++++++++++++----- tests/MethodCallTest.php | 54 ++++++++++--- 2 files changed, 101 insertions(+), 30 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 3f1addedc85..2c245caa157 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -116,6 +116,13 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio 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([ $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), @@ -130,18 +137,35 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio new TFalse(), ]); - case 11: // PDO::FETCH_NAMED - array>|false + case 11: // PDO::FETCH_NAMED - array>|false return new Union([ new TArray([ Type::getString(), new Union([ new TScalar(), - Type::getListAtomic(Type::getScalar()), + 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( @@ -199,7 +223,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]), + ]) ), ]); @@ -214,7 +238,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]), + ]) ), ]); @@ -225,27 +249,27 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U ), ]); - case 8: // PDO::FETCH_CLASS - list + case 7: // PDO::FETCH_COLUMN - scalar|null|false return new Union([ Type::getListAtomic( new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() - ]), + new TScalar(), + new TNull(), + 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 + case 8: // PDO::FETCH_CLASS - list return new Union([ Type::getListAtomic( new Union([ - new TObject() - ]), + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() + ]) ), ]); - case 11: // PDO::FETCH_NAMED - list>> + case 11: // PDO::FETCH_NAMED - list>> return new Union([ Type::getListAtomic( new Union([ @@ -253,13 +277,30 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U Type::getString(), new Union([ new TScalar(), - Type::getListAtomic(Type::getScalar()), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]) + ), ]), ]), - ]), + ]) ), ]); + 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( @@ -268,9 +309,9 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]), + ]) ), - ]), + ]) ), ]); @@ -279,7 +320,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U Type::getListAtomic( new Union([ new TNamedObject('stdClass') - ]), + ]) ), ]); } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 760d93b65b5..f15f55ab489 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -622,6 +622,46 @@ function foo(string $foo): string { return $foo; }', ], + 'pdoStatementFetchColumn' => [ + '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 */ @@ -724,19 +764,9 @@ function fetch_lazy() { return $sth->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 */ + /** @return array>|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); @@ -746,7 +776,7 @@ function fetch_named() { ], 'pdoStatementFetchAllNamed' => [ 'code' => '>> */ + /** @return list>> */ function fetch_named() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); From 8a6774ba82f8e8b952b8d89f00ce56ac03a19fcb Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:45:21 +0200 Subject: [PATCH 3/8] #9974 fixed coding styles --- .../PdoStatementReturnTypeProvider.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 2c245caa157..11c49f42ee0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -48,7 +48,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) + private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event): ?Union { $source = $event->getSource(); $call_args = $event->getCallArgs(); @@ -148,7 +148,7 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio new Union([ new TScalar(), new TNull(), - ]) + ]), ), ]), ]), @@ -223,7 +223,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]) + ]), ), ]); @@ -238,14 +238,14 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]) + ]), ), ]); case 6: // PDO::FETCH_BOUND - list return new Union([ Type::getListAtomic( - Type::getBool() + Type::getBool(), ), ]); @@ -256,7 +256,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TScalar(), new TNull(), new TFalse(), - ]) + ]), ), ]); @@ -264,8 +264,8 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U return new Union([ Type::getListAtomic( new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() - ]) + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + ]), ), ]); @@ -282,11 +282,11 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]) + ]), ), ]), ]), - ]) + ]), ), ]); @@ -309,9 +309,9 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]) + ]), ), - ]) + ]), ), ]); @@ -319,8 +319,8 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U return new Union([ Type::getListAtomic( new Union([ - new TNamedObject('stdClass') - ]) + new TNamedObject('stdClass'), + ]), ), ]); } From 3d525cbd0eef1a4f57e90788228acfe059b0ab5e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:48:00 +0200 Subject: [PATCH 4/8] #9974 cleanup --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 11c49f42ee0..e4236b24e5a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -54,6 +54,9 @@ private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) $call_args = $event->getCallArgs(); $context = $event->getContext(); + $context->references_in_scope['fetch_mode'] = null; + $context->references_in_scope['fetch_class'] = null; + if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() From 13d53ecbf10c31492e17bd87fda662e43ed4a920 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:59:06 +0200 Subject: [PATCH 5/8] #9974 cleanup --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index e4236b24e5a..a1c5963d798 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -54,14 +54,14 @@ private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) $call_args = $event->getCallArgs(); $context = $event->getContext(); - $context->references_in_scope['fetch_mode'] = null; - $context->references_in_scope['fetch_class'] = null; + unset($context->references_in_scope['fetch_mode']); + unset($context->references_in_scope['fetch_class']); 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; + $context->references_in_scope['fetch_mode'] = (string) $first_arg_type->getSingleIntLiteral()->value; } if (isset($call_args[1]) From d600705b0c8ab0424fff0be3cb9872fc97778519 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 15:53:05 +0200 Subject: [PATCH 6/8] #9974 removed references_in_scope for setFetchMode --- .../PdoStatementReturnTypeProvider.php | 44 ++----------------- tests/MethodCallTest.php | 23 +++++++--- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index a1c5963d798..593be23ce1d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -33,10 +33,6 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - if ($method_name_lowercase === 'setfetchmode') { - return self::handleSetFetchMode($event); - } - if ($method_name_lowercase === 'fetch') { return self::handleFetch($event); } @@ -48,39 +44,11 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event): ?Union - { - $source = $event->getSource(); - $call_args = $event->getCallArgs(); - $context = $event->getContext(); - - unset($context->references_in_scope['fetch_mode']); - unset($context->references_in_scope['fetch_class']); - - 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'] = (string) $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; + $fetch_mode = 0; if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) @@ -89,8 +57,6 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; } - $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; - switch ($fetch_mode) { case 2: // PDO::FETCH_ASSOC - array|false return new Union([ @@ -128,7 +94,7 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio case 8: // PDO::FETCH_CLASS - object|false return new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + new TObject(), new TFalse(), ]); @@ -194,9 +160,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $context = $event->getContext(); - - $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + $fetch_mode = 0; if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) @@ -205,7 +169,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; } - $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + $fetch_class_name = null; if (isset($call_args[1]) && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index f15f55ab489..4b8ce13ea94 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -509,22 +509,35 @@ class B extends A {} $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $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); $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);', + $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);', 'assertions' => [ - '$a' => 'A|false', - '$b' => 'list', - '$c' => 'A|false', - '$d' => 'list', + '$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>', ], ], 'datePeriodConstructor' => [ From ee505259aab463c39b8b802c6db27a4f6c04c94f Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 15:59:31 +0200 Subject: [PATCH 7/8] #9974 extended test with PDO::ATTR_DEFAULT_FETCH_MODE for future implementation --- tests/MethodCallTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 4b8ce13ea94..f6cb3e75099 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -507,10 +507,13 @@ 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 */ @@ -525,7 +528,9 @@ class B extends A {} $h = $stmt2->fetch(); $i = $stmt2->fetchAll(); $j = $stmt2->fetch(PDO::FETCH_BOTH); - $k = $stmt2->fetchAll(PDO::FETCH_BOTH);', + $k = $stmt2->fetchAll(PDO::FETCH_BOTH); + /** @psalm-suppress MixedAssignment */ + $l = $stmt3->fetch();', 'assertions' => [ '$a' => 'mixed', '$b' => 'array|false', @@ -538,6 +543,7 @@ class B extends A {} '$i' => 'array|false', '$j' => 'array|false', '$k' => 'list>', + '$l' => 'mixed', ], ], 'datePeriodConstructor' => [ From 88ddb8976305ceae7b6cdb609270393195b8435e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 16:11:32 +0200 Subject: [PATCH 8/8] #9974 fixed return type for FETCH_COLUMN --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 593be23ce1d..3edc2342bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -216,13 +216,12 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U ), ]); - case 7: // PDO::FETCH_COLUMN - scalar|null|false + case 7: // PDO::FETCH_COLUMN - list return new Union([ Type::getListAtomic( new Union([ new TScalar(), new TNull(), - new TFalse(), ]), ), ]);