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

Fix treatment of PDOStatement::setFetchMode

Fixes #1496
This commit is contained in:
Brown 2019-03-29 13:26:13 -04:00
parent f82a55d836
commit 1ab6345bac
6 changed files with 187 additions and 60 deletions

View File

@ -1214,50 +1214,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
return $this->codebase;
}
/**
* @param string $method_id
* @param array<int, PhpParser\Node\Arg> $args
*
* @return array<int, FunctionLikeParameter>
*/
public static function getMethodParamsById(
StatementsAnalyzer $statements_analyzer,
$method_id,
array $args
) {
$fq_class_name = strpos($method_id, '::') !== false ? explode('::', $method_id)[0] : null;
$codebase = $statements_analyzer->getCodebase();
if ($fq_class_name) {
$fq_class_name = $codebase->classlikes->getUnAliasedName($fq_class_name);
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if ($class_storage->user_defined || $class_storage->stubbed) {
$method_params = $codebase->methods->getMethodParams($method_id, $statements_analyzer, $args);
return $method_params;
}
}
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if (CallMap::inCallMap($declaring_method_id ?: $method_id)) {
$function_param_options = CallMap::getParamsFromCallMap($declaring_method_id ?: $method_id);
if ($function_param_options === null) {
throw new \UnexpectedValueException(
'Not expecting $function_param_options to be null for ' . $method_id
);
}
return self::getMatchingParamsFromCallMapOptions($codebase, $function_param_options, $args);
}
return $codebase->methods->getMethodParams($method_id);
}
/**
* @param string $method_id
* @param array<int, PhpParser\Node\Arg> $args
@ -1283,7 +1239,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
*
* @return array<int, FunctionLikeParameter>
*/
protected static function getMatchingParamsFromCallMapOptions(
public static function getMatchingParamsFromCallMapOptions(
Codebase $codebase,
array $function_param_options,
array $args

View File

@ -232,7 +232,7 @@ class CallAnalyzer
$codebase = $statements_analyzer->getCodebase();
$method_params = $method_id
? FunctionLikeAnalyzer::getMethodParamsById($statements_analyzer, $method_id, $args)
? $codebase->methods->getMethodParams($method_id, $statements_analyzer, $args, $context)
: null;
if (self::checkFunctionArguments(
@ -295,15 +295,6 @@ class CallAnalyzer
}
}
if (!$class_storage->user_defined) {
// check again after we've processed args
$method_params = FunctionLikeAnalyzer::getMethodParamsById(
$statements_analyzer,
$method_id,
$args
);
}
if (self::checkFunctionLikeArgumentsMatch(
$statements_analyzer,
$args,

View File

@ -4,6 +4,8 @@ namespace Psalm\Internal\Codebase;
use PhpParser;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Provider\{
ClassLikeStorageProvider,
FileReferenceProvider,
@ -221,12 +223,16 @@ class Methods
/**
* @param string $method_id
* @param array<PhpParser\Node\Arg> $args
* @param array<int, PhpParser\Node\Arg> $args
*
* @return array<int, FunctionLikeParameter>
*/
public function getMethodParams($method_id, StatementsSource $source = null, array $args = null)
{
public function getMethodParams(
$method_id,
StatementsSource $source = null,
array $args = null,
Context $context = null
) : array {
list($fq_class_name, $method_name) = explode('::', $method_id);
if ($this->params_provider->has($fq_class_name)) {
@ -234,7 +240,8 @@ class Methods
$fq_class_name,
$method_name,
$args,
$source
$source,
$context
);
if ($method_params !== null) {
@ -242,7 +249,46 @@ class Methods
}
}
if ($declaring_method_id = $this->getDeclaringMethodId($method_id)) {
$declaring_method_id = $this->getDeclaringMethodId($method_id);
// functions
if (CallMap::inCallMap($declaring_method_id ?: $method_id)) {
$declaring_fq_class_name = explode('::', $declaring_method_id ?: $method_id)[0];
$class_storage = $this->classlike_storage_provider->get($declaring_fq_class_name);
if (!$class_storage->stubbed) {
$function_param_options = CallMap::getParamsFromCallMap($declaring_method_id ?: $method_id);
if ($function_param_options === null) {
throw new \UnexpectedValueException(
'Not expecting $function_param_options to be null for ' . $declaring_method_id
);
}
if (!$source || $args === null || count($function_param_options) === 1) {
return $function_param_options[0];
}
if ($context && $source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
foreach ($args as $arg) {
\Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(
$source,
$arg->value,
$context
);
}
}
return FunctionLikeAnalyzer::getMatchingParamsFromCallMapOptions(
$source->getCodebase(),
$function_param_options,
$args
);
}
}
if ($declaring_method_id) {
$storage = $this->getStorage($declaring_method_id);
$params = $storage->params;

View File

@ -28,6 +28,8 @@ class MethodParamsProvider
public function __construct()
{
self::$handlers = [];
$this->registerClass(ReturnTypeProvider\PdoStatementSetFetchMode::class);
}
/**

View File

@ -0,0 +1,107 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\StatementsSource;
use Psalm\Type;
class PdoStatementSetFetchMode implements \Psalm\Plugin\Hook\MethodParamsProviderInterface
{
public static function getClassLikeNames() : array
{
return ['PDOStatement'];
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @return ?array<int, \Psalm\Storage\FunctionLikeParameter>
*/
public static function getMethodParams(
string $fq_classlike_name,
string $method_name_lowercase,
array $call_args = null,
StatementsSource $statements_source = null,
Context $context = null,
CodeLocation $code_location = null
) {
if ($method_name_lowercase === 'setfetchmode') {
if (!$context
|| !$call_args
|| !$statements_source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer
|| \Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(
$statements_source,
$call_args[0]->value,
$context
) === false
) {
return;
}
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->isSingleIntLiteral()
) {
$params = [
new \Psalm\Storage\FunctionLikeParameter(
'mode',
false,
Type::getInt(),
null,
null,
false
)
];
$value = $call_args[0]->value->inferredType->getSingleIntLiteral()->value;
switch ($value) {
case \PDO::FETCH_COLUMN:
$params[] = new \Psalm\Storage\FunctionLikeParameter(
'colno',
false,
Type::getInt(),
null,
null,
false
);
break;
case \PDO::FETCH_CLASS:
$params[] = new \Psalm\Storage\FunctionLikeParameter(
'classname',
false,
Type::getClassString(),
null,
null,
false
);
$params[] = new \Psalm\Storage\FunctionLikeParameter(
'ctorargs',
false,
Type::getArray(),
null,
null,
true
);
break;
case \PDO::FETCH_INTO:
$params[] = new \Psalm\Storage\FunctionLikeParameter(
'object',
false,
Type::getObject(),
null,
null,
false
);
break;
}
return $params;
}
}
}
}

View File

@ -291,6 +291,31 @@ class MethodCallTest extends TestCase
Foo::$current->bar();',
],
'pdoStatementSetFetchMode' => [
'<?php
class A {
/** @var ?string */
public $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();'
],
'datePeriodConstructor' => [
'<?php
function foo(DateTime $d1, DateTime $d2) : void {
new DatePeriod(
$d1,
DateInterval::createFromDateString("1 month"),
$d2
);
}'
],
];
}