mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
#9974 added return type detection for PDOStatement::fetchAll, extended return type detection for PDOStatement::fetch
This commit is contained in:
parent
be875f50e4
commit
fc74ae83e6
@ -27,88 +27,261 @@ 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 === '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<string,scalar|null>|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<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 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<string, scalar|list<scalar>>|false
|
||||
return new Union([
|
||||
new TArray([
|
||||
Type::getString(),
|
||||
new Union([
|
||||
new TScalar(),
|
||||
Type::getListAtomic(Type::getScalar()),
|
||||
]),
|
||||
]),
|
||||
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 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<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 8: // PDO::FETCH_CLASS - list<object>
|
||||
return new Union([
|
||||
Type::getListAtomic(
|
||||
new Union([
|
||||
$fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject()
|
||||
]),
|
||||
),
|
||||
]);
|
||||
|
||||
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 1: // PDO::FETCH_LAZY - list<object>
|
||||
// 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 - 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|list<scalar>>>
|
||||
return new Union([
|
||||
Type::getListAtomic(
|
||||
new Union([
|
||||
new TArray([
|
||||
Type::getString(),
|
||||
new Union([
|
||||
new TScalar(),
|
||||
Type::getListAtomic(Type::getScalar()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
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 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 - stdClass|false
|
||||
return new Union([
|
||||
new TNamedObject('stdClass'),
|
||||
new TFalse(),
|
||||
]);
|
||||
}
|
||||
case 5: // PDO::FETCH_OBJ - list<stdClass>
|
||||
return new Union([
|
||||
Type::getListAtomic(
|
||||
new Union([
|
||||
new TNamedObject('stdClass')
|
||||
]),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -503,14 +503,29 @@ 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);
|
||||
$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<A>',
|
||||
'$c' => 'A|false',
|
||||
'$d' => 'list<A>',
|
||||
'$e' => 'list<B>',
|
||||
'$f' => 'array<string, null|scalar>|false',
|
||||
'$g' => 'list<array<string, null|scalar>>',
|
||||
],
|
||||
],
|
||||
'datePeriodConstructor' => [
|
||||
'code' => '<?php
|
||||
@ -617,6 +632,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 +652,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 +672,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 +692,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 */
|
||||
@ -657,6 +724,16 @@ class MethodCallTest extends TestCase
|
||||
return $sth->fetch(PDO::FETCH_LAZY);
|
||||
}',
|
||||
],
|
||||
'pdoStatementFetchAllLazy' => [
|
||||
'code' => '<?php
|
||||
/** @return list<object> */
|
||||
function fetch_lazy() : array {
|
||||
$p = new PDO("sqlite::memory:");
|
||||
$sth = $p->prepare("SELECT 1");
|
||||
$sth->execute();
|
||||
return $sth->fetchAll(PDO::FETCH_LAZY);
|
||||
}',
|
||||
],
|
||||
'pdoStatementFetchNamed' => [
|
||||
'code' => '<?php
|
||||
/** @return array<string,scalar|list<scalar>>|false */
|
||||
@ -667,6 +744,16 @@ class MethodCallTest extends TestCase
|
||||
return $sth->fetch(PDO::FETCH_NAMED);
|
||||
}',
|
||||
],
|
||||
'pdoStatementFetchAllNamed' => [
|
||||
'code' => '<?php
|
||||
/** @return list<array<string,scalar|list<scalar>>> */
|
||||
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 +764,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 +784,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…
x
Reference in New Issue
Block a user