1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 09:37:59 +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 public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{ {
$config = Config::getInstance(); $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(); $source = $event->getSource();
$call_args = $event->getCallArgs(); $call_args = $event->getCallArgs();
$method_name_lowercase = $event->getMethodNameLowercase(); $fetch_mode = 0;
if ($method_name_lowercase === 'fetch'
&& $config->php_extensions["pdo"] if (isset($call_args[0])
&& isset($call_args[0])
&& ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value))
&& $first_arg_type->isSingleIntLiteral() && $first_arg_type->isSingleIntLiteral()
) { ) {
$fetch_mode = $first_arg_type->getSingleIntLiteral()->value; $fetch_mode = $first_arg_type->getSingleIntLiteral()->value;
}
switch ($fetch_mode) { switch ($fetch_mode) {
case 2: // PDO::FETCH_ASSOC - array<string,scalar|null>|false case 2: // PDO::FETCH_ASSOC - array<string,scalar|null>|false
return new Union([ return new Union([
new TArray([ new TArray([
Type::getString(), Type::getString(),
new Union([ new Union([
new TScalar(), new TScalar(),
new TNull(), 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 case 4: // PDO::FETCH_BOTH - list<array<array-key,scalar|null>>
return new Union([ return new Union([
new TArray([ Type::getListAtomic(
Type::getArrayKey(), new Union([
new Union([ new TArray([
new TScalar(), Type::getArrayKey(),
new TNull(), new Union([
new TScalar(),
new TNull(),
]),
]), ]),
]), ]),
new TFalse(), ),
]); ]);
case 6: // PDO::FETCH_BOUND - bool case 6: // PDO::FETCH_BOUND - list<bool>
return Type::getBool(); return new Union([
Type::getListAtomic(
Type::getBool(),
),
]);
case 8: // PDO::FETCH_CLASS - object|false case 7: // PDO::FETCH_COLUMN - list<scalar|null>
return new Union([ return new Union([
new TObject(), Type::getListAtomic(
new TFalse(), new Union([
]); new TScalar(),
new TNull(),
]),
),
]);
case 1: // PDO::FETCH_LAZY - object|false case 8: // PDO::FETCH_CLASS - list<object>
// This actually returns a PDORow object, but that class is return new Union([
// undocumented, and its attributes are all dynamic anyway Type::getListAtomic(
return new Union([ new Union([
new TObject(), $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(),
new TFalse(), ]),
]); ),
]);
case 11: // PDO::FETCH_NAMED - array<string, scalar|list<scalar>>|false case 11: // PDO::FETCH_NAMED - list<array<string, scalar|null|list<scalar|null>>>
return new Union([ return new Union([
new TArray([ Type::getListAtomic(
Type::getString(), new Union([
new Union([ new TArray([
new TScalar(), Type::getString(),
Type::getListAtomic(Type::getScalar()), 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 case 12: // PDO::FETCH_KEY_PAIR - array<array-key,scalar|null>
return new Union([ return new Union([
Type::getListAtomic( new TArray([
new Union([ Type::getArrayKey(),
new TScalar(), new Union([
new TNull(), new TScalar(),
]), new TNull(),
), ]),
new TFalse(), ]),
]); ]);
case 5: // PDO::FETCH_OBJ - stdClass|false case 3: // PDO::FETCH_NUM - list<list<scalar|null>>
return new Union([ return new Union([
new TNamedObject('stdClass'), Type::getListAtomic(
new TFalse(), 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; return null;

View File

@ -503,14 +503,48 @@ class MethodCallTest extends TestCase
/** @var ?string */ /** @var ?string */
public $a; public $a;
} }
class B extends A {}
$db = new PDO("sqlite::memory:"); $db = new PDO("sqlite::memory:");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $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 = $db->prepare("select \"a\" as a");
$stmt->setFetchMode(PDO::FETCH_CLASS, A::class); $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(); $stmt->execute();
$stmt2->execute();
/** @psalm-suppress MixedAssignment */ /** @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' => [ 'datePeriodConstructor' => [
'code' => '<?php 'code' => '<?php
@ -607,6 +641,46 @@ class MethodCallTest extends TestCase
return $foo; 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' => [ 'pdoStatementFetchAssoc' => [
'code' => '<?php 'code' => '<?php
/** @return array<string,null|scalar>|false */ /** @return array<string,null|scalar>|false */
@ -617,6 +691,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_ASSOC); 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' => [ 'pdoStatementFetchBoth' => [
'code' => '<?php 'code' => '<?php
/** @return array<null|scalar>|false */ /** @return array<null|scalar>|false */
@ -627,6 +711,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_BOTH); 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' => [ 'pdoStatementFetchBound' => [
'code' => '<?php 'code' => '<?php
/** @return bool */ /** @return bool */
@ -637,6 +731,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_BOUND); 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' => [ 'pdoStatementFetchClass' => [
'code' => '<?php 'code' => '<?php
/** @return object|false */ /** @return object|false */
@ -647,6 +751,28 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_CLASS); 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' => [ 'pdoStatementFetchLazy' => [
'code' => '<?php 'code' => '<?php
/** @return object|false */ /** @return object|false */
@ -659,7 +785,7 @@ class MethodCallTest extends TestCase
], ],
'pdoStatementFetchNamed' => [ 'pdoStatementFetchNamed' => [
'code' => '<?php 'code' => '<?php
/** @return array<string,scalar|list<scalar>>|false */ /** @return array<string,scalar|null|list<scalar|null>>|false */
function fetch_named() { function fetch_named() {
$p = new PDO("sqlite::memory:"); $p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1"); $sth = $p->prepare("SELECT 1");
@ -667,6 +793,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_NAMED); 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' => [ 'pdoStatementFetchNum' => [
'code' => '<?php 'code' => '<?php
/** @return list<null|scalar>|false */ /** @return list<null|scalar>|false */
@ -677,6 +813,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_NUM); 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' => [ 'pdoStatementFetchObj' => [
'code' => '<?php 'code' => '<?php
/** @return stdClass|false */ /** @return stdClass|false */
@ -687,6 +833,16 @@ class MethodCallTest extends TestCase
return $sth->fetch(PDO::FETCH_OBJ); 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' => [ 'dateTimeSecondArg' => [
'code' => '<?php 'code' => '<?php
$date = new DateTime(null, new DateTimeZone("Pacific/Nauru")); $date = new DateTime(null, new DateTimeZone("Pacific/Nauru"));