1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #3234 - infer iterator key types properly

This commit is contained in:
Brown 2020-04-27 00:41:34 -04:00
parent f91e94b64e
commit 755ada9114
2 changed files with 114 additions and 33 deletions

View File

@ -854,46 +854,33 @@ class ForeachAnalyzer
) )
) )
) { ) {
$old_data_provider = $statements_analyzer->node_data; $iterator_value_type = self::getFakeMethodCallType(
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
$foreach_expr,
new PhpParser\Node\Identifier('current', $foreach_expr->getAttributes())
);
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}
$was_inside_call = $context->inside_call;
$context->inside_call = true;
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer, $statements_analyzer,
$fake_method_call, $foreach_expr,
$context $context,
'current'
); );
$context->inside_call = $was_inside_call; $iterator_key_type = self::getFakeMethodCallType(
$statements_analyzer,
$foreach_expr,
$context,
'current'
);
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { if ($iterator_value_type && !$iterator_value_type->isMixed()) {
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); if (!$value_type) {
$value_type = $iterator_value_type;
} else {
$value_type = Type::combineUnionTypes($value_type, $iterator_value_type);
}
} }
$iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null; if ($iterator_key_type && !$iterator_key_type->isMixed()) {
if (!$key_type) {
$statements_analyzer->node_data = $old_data_provider; $key_type = $iterator_key_type;
if ($iterator_class_type && !$iterator_class_type->isMixed()) {
if (!$value_type) {
$value_type = $iterator_class_type;
} else { } else {
$value_type = Type::combineUnionTypes($value_type, $iterator_class_type); $key_type = Type::combineUnionTypes($key_type, $iterator_key_type);
} }
} }
} }
@ -1015,6 +1002,58 @@ class ForeachAnalyzer
} }
} }
private static function getFakeMethodCallType(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $foreach_expr,
Context $context,
string $method_name
) : ?Type\Union {
$old_data_provider = $statements_analyzer->node_data;
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
$foreach_expr,
new PhpParser\Node\Identifier($method_name, $foreach_expr->getAttributes())
);
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}
if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyUndefinedMethod']);
}
$was_inside_call = $context->inside_call;
$context->inside_call = true;
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call,
$context
);
$context->inside_call = $was_inside_call;
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
}
if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['PossiblyUndefinedMethod']);
}
$iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
$statements_analyzer->node_data = $old_data_provider;
return $iterator_class_type;
}
/** /**
* @param string $template_name * @param string $template_name
* @param array<string, array<int|string, Type\Union>> $template_type_extends * @param array<string, array<int|string, Type\Union>> $template_type_extends

View File

@ -999,6 +999,48 @@ class ForeachTest extends \Psalm\Tests\TestCase
} }
}' }'
], ],
'iteratorForeach' => [
'<?php
/**
* @implements Iterator<int, string>
*/
class FooIterator implements \Iterator {
private ?int $key = null;
public function current(): string
{
return "a";
}
public function next(): void
{
$this->key = $this->key === null ? 0 : $this->key + 1;
}
public function key(): int
{
if ($this->key === null) {
throw new \Exception();
}
return $this->key;
}
public function valid(): bool
{
return $this->key !== null && $this->key <= 3;
}
public function rewind(): void
{
$this->key = null;
$this->next();
}
}
foreach (new FooIterator() as $key => $value) {
echo $key . " " . $value;
}'
],
]; ];
} }