1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Merge pull request #10055 from thbley/master

Add type detection for PDOStatement::fetchAll(PDO::FETCH_CLASS, SomeClass::class)
This commit is contained in:
orklah 2023-07-27 19:03:09 +02:00 committed by GitHub
commit 73ebe227b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 400 additions and 64 deletions

View File

@ -27,88 +27,268 @@ 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 === '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<string,scalar|null>|false
return new Union([
new TArray([
Type::getString(),
new Union([
new TScalar(),
new TNull(),
switch ($fetch_mode) {
case 2: // PDO::FETCH_ASSOC - array<string,scalar|null>|false
return new Union([
new TArray([
Type::getString(),
new Union([
new TScalar(),
new TNull(),
]),
]),
new TFalse(),
]);
case 4: // PDO::FETCH_BOTH - array<array-key,scalar|null>|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<string, scalar|null|list<scalar|null>>|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<array-key,scalar|null>
return new Union([
new TArray([
Type::getArrayKey(),
new Union([
new TScalar(),
new TNull(),
]),
]),
]);
case 3: // PDO::FETCH_NUM - list<scalar|null>|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<array<string,scalar|null>>
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<array-key,scalar|null>|false
return new Union([
new TArray([
Type::getArrayKey(),
new Union([
new TScalar(),
new TNull(),
case 4: // PDO::FETCH_BOTH - list<array<array-key,scalar|null>>
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<bool>
return new Union([
Type::getListAtomic(
Type::getBool(),
),
]);
case 8: // PDO::FETCH_CLASS - object|false
return new Union([
new TObject(),
new TFalse(),
]);
case 7: // PDO::FETCH_COLUMN - list<scalar|null>
return new Union([
Type::getListAtomic(
new Union([
new TScalar(),
new TNull(),
]),
),
]);
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 8: // PDO::FETCH_CLASS - list<object>
return new Union([
Type::getListAtomic(
new Union([
$fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(),
]),
),
]);
case 11: // PDO::FETCH_NAMED - array<string, scalar|list<scalar>>|false
return new Union([
new TArray([
Type::getString(),
new Union([
new TScalar(),
Type::getListAtomic(Type::getScalar()),
case 11: // PDO::FETCH_NAMED - list<array<string, scalar|null|list<scalar|null>>>
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 3: // PDO::FETCH_NUM - list<scalar|null>|false
return new Union([
Type::getListAtomic(
new Union([
new TScalar(),
new TNull(),
]),
),
new TFalse(),
]);
case 12: // PDO::FETCH_KEY_PAIR - array<array-key,scalar|null>
return new Union([
new TArray([
Type::getArrayKey(),
new Union([
new TScalar(),
new TNull(),
]),
]),
]);
case 5: // PDO::FETCH_OBJ - stdClass|false
return new Union([
new TNamedObject('stdClass'),
new TFalse(),
]);
}
case 3: // PDO::FETCH_NUM - list<list<scalar|null>>
return new Union([
Type::getListAtomic(
new Union([
Type::getListAtomic(
new Union([
new TScalar(),
new TNull(),
]),
),
]),
),
]);
case 5: // PDO::FETCH_OBJ - list<stdClass>
return new Union([
Type::getListAtomic(
new Union([
new TNamedObject('stdClass'),
]),
),
]);
}
return null;

View File

@ -503,14 +503,48 @@ 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);
$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();',
$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 */
$l = $stmt3->fetch();',
'assertions' => [
'$a' => 'mixed',
'$b' => 'array<array-key, mixed>|false',
'$c' => 'false|object',
'$d' => 'list<object>',
'$e' => 'list<B>',
'$f' => 'array<string, null|scalar>|false',
'$g' => 'list<array<string, null|scalar>>',
'$h' => 'mixed',
'$i' => 'array<array-key, mixed>|false',
'$j' => 'array<array-key, null|scalar>|false',
'$k' => 'list<array<array-key, null|scalar>>',
'$l' => 'mixed',
],
],
'datePeriodConstructor' => [
'code' => '<?php
@ -607,6 +641,46 @@ class MethodCallTest extends TestCase
return $foo;
}',
],
'pdoStatementFetchColumn' => [
'code' => '<?php
/** @return scalar|null|false */
function fetch_column() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetch(PDO::FETCH_COLUMN);
}',
],
'pdoStatementFetchAllColumn' => [
'code' => '<?php
/** @return list<scalar|null> */
function fetch_column() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_COLUMN);
}',
],
'pdoStatementFetchKeyPair' => [
'code' => '<?php
/** @return array<array-key,scalar|null> */
function fetch_column() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetch(PDO::FETCH_KEY_PAIR);
}',
],
'pdoStatementFetchAllKeyPair' => [
'code' => '<?php
/** @return array<array-key,scalar|null> */
function fetch_column() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_KEY_PAIR);
}',
],
'pdoStatementFetchAssoc' => [
'code' => '<?php
/** @return array<string,null|scalar>|false */
@ -617,6 +691,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_ASSOC);
}',
],
'pdoStatementFetchAllAssoc' => [
'code' => '<?php
/** @return list<array<string,null|scalar>> */
function fetch_assoc() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_ASSOC);
}',
],
'pdoStatementFetchBoth' => [
'code' => '<?php
/** @return array<null|scalar>|false */
@ -627,6 +711,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_BOTH);
}',
],
'pdoStatementFetchAllBoth' => [
'code' => '<?php
/** @return list<array<null|scalar>> */
function fetch_both() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_BOTH);
}',
],
'pdoStatementFetchBound' => [
'code' => '<?php
/** @return bool */
@ -637,6 +731,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_BOUND);
}',
],
'pdoStatementFetchAllBound' => [
'code' => '<?php
/** @return list<bool> */
function fetch_both() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_BOUND);
}',
],
'pdoStatementFetchClass' => [
'code' => '<?php
/** @return object|false */
@ -647,6 +751,28 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_CLASS);
}',
],
'pdoStatementFetchAllClass' => [
'code' => '<?php
/** @return list<object> */
function fetch_class() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_CLASS);
}',
],
'pdoStatementFetchAllNamedClass' => [
'code' => '<?php
class Foo {}
/** @return list<Foo> */
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' => '<?php
/** @return object|false */
@ -659,7 +785,7 @@ class MethodCallTest extends TestCase
],
'pdoStatementFetchNamed' => [
'code' => '<?php
/** @return array<string,scalar|list<scalar>>|false */
/** @return array<string,scalar|null|list<scalar|null>>|false */
function fetch_named() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
@ -667,6 +793,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_NAMED);
}',
],
'pdoStatementFetchAllNamed' => [
'code' => '<?php
/** @return list<array<string,scalar|null|list<scalar|null>>> */
function fetch_named() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_NAMED);
}',
],
'pdoStatementFetchNum' => [
'code' => '<?php
/** @return list<null|scalar>|false */
@ -677,6 +813,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_NUM);
}',
],
'pdoStatementFetchAllNum' => [
'code' => '<?php
/** @return list<list<null|scalar>> */
function fetch_named() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_NUM);
}',
],
'pdoStatementFetchObj' => [
'code' => '<?php
/** @return stdClass|false */
@ -687,6 +833,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_OBJ);
}',
],
'pdoStatementFetchAllObj' => [
'code' => '<?php
/** @return list<stdClass> */
function fetch_named() : array {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetchAll(PDO::FETCH_OBJ);
}',
],
'dateTimeSecondArg' => [
'code' => '<?php
$date = new DateTime(null, new DateTimeZone("Pacific/Nauru"));