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:
commit
73ebe227b2
@ -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;
|
||||
|
@ -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"));
|
||||
|
Loading…
Reference in New Issue
Block a user