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

Check iterator types on yield from (#5042)

* Check iterator types on yield from

* Switch to NodeAbstract

* Make Foo iterable
This commit is contained in:
Daniil Gentili 2021-01-20 23:41:15 +01:00
parent 5eb4d88f1a
commit ec901d2e44
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
4 changed files with 94 additions and 14 deletions

View File

@ -215,6 +215,7 @@ class ForeachAnalyzer
if (self::checkIteratorType( if (self::checkIteratorType(
$statements_analyzer, $statements_analyzer,
$stmt, $stmt,
$stmt->expr,
$iterator_type, $iterator_type,
$codebase, $codebase,
$context, $context,
@ -335,11 +336,13 @@ class ForeachAnalyzer
} }
/** /**
* @param PhpParser\Node\Stmt\Foreach_|PhpParser\Node\Expr\YieldFrom $stmt
* @return false|null * @return false|null
*/ */
public static function checkIteratorType( public static function checkIteratorType(
StatementsAnalyzer $statements_analyzer, StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt\Foreach_ $stmt, PhpParser\NodeAbstract $stmt,
PhpParser\Node\Expr $expr,
Type\Union $iterator_type, Type\Union $iterator_type,
Codebase $codebase, Codebase $codebase,
Context $context, Context $context,
@ -351,7 +354,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new NullIterator( new NullIterator(
'Cannot iterate over null', 'Cannot iterate over null',
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -361,7 +364,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new PossiblyNullIterator( new PossiblyNullIterator(
'Cannot iterate over nullable var ' . $iterator_type, 'Cannot iterate over nullable var ' . $iterator_type,
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -371,7 +374,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new PossiblyFalseIterator( new PossiblyFalseIterator(
'Cannot iterate over falsable var ' . $iterator_type, 'Cannot iterate over falsable var ' . $iterator_type,
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -415,7 +418,7 @@ class ForeachAnalyzer
$iterator_atomic_type = $iterator_atomic_type->getGenericArrayType(); $iterator_atomic_type = $iterator_atomic_type->getGenericArrayType();
} elseif ($iterator_atomic_type instanceof Type\Atomic\TList) { } elseif ($iterator_atomic_type instanceof Type\Atomic\TList) {
$list_var_id = ExpressionIdentifier::getArrayVarId( $list_var_id = ExpressionIdentifier::getArrayVarId(
$stmt->expr, $expr,
$statements_analyzer->getFQCLN(), $statements_analyzer->getFQCLN(),
$statements_analyzer $statements_analyzer
); );
@ -444,7 +447,7 @@ class ForeachAnalyzer
ArrayFetchAnalyzer::taintArrayFetch( ArrayFetchAnalyzer::taintArrayFetch(
$statements_analyzer, $statements_analyzer,
$stmt->expr, $expr,
null, null,
$value_type, $value_type,
Type::getMixed() Type::getMixed()
@ -479,7 +482,7 @@ class ForeachAnalyzer
ArrayFetchAnalyzer::taintArrayFetch( ArrayFetchAnalyzer::taintArrayFetch(
$statements_analyzer, $statements_analyzer,
$stmt->expr, $expr,
null, null,
$value_type, $value_type,
Type::getMixed() Type::getMixed()
@ -592,7 +595,7 @@ class ForeachAnalyzer
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer, $statements_analyzer,
$iterator_atomic_type->value, $iterator_atomic_type->value,
new CodeLocation($statements_analyzer->getSource(), $stmt->expr), new CodeLocation($statements_analyzer->getSource(), $expr),
$context->self, $context->self,
$context->calling_method_id, $context->calling_method_id,
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
@ -609,7 +612,7 @@ class ForeachAnalyzer
self::handleIterable( self::handleIterable(
$statements_analyzer, $statements_analyzer,
$iterator_atomic_type, $iterator_atomic_type,
$stmt->expr, $expr,
$codebase, $codebase,
$context, $context,
$key_type, $key_type,
@ -647,7 +650,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new PossibleRawObjectIteration( new PossibleRawObjectIteration(
'Possibly undesired iteration over regular object ' . \reset($raw_object_types), 'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -657,7 +660,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new RawObjectIteration( new RawObjectIteration(
'Possibly undesired iteration over regular object ' . \reset($raw_object_types), 'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -671,7 +674,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new PossiblyInvalidIterator( new PossiblyInvalidIterator(
'Cannot iterate over ' . $invalid_iterator_types[0], 'Cannot iterate over ' . $invalid_iterator_types[0],
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
@ -681,7 +684,7 @@ class ForeachAnalyzer
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new InvalidIterator( new InvalidIterator(
'Cannot iterate over ' . $invalid_iterator_types[0], 'Cannot iterate over ' . $invalid_iterator_types[0],
new CodeLocation($statements_analyzer->getSource(), $stmt->expr) new CodeLocation($statements_analyzer->getSource(), $expr)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {

View File

@ -5,6 +5,7 @@ use PhpParser;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Context; use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Type; use Psalm\Type;
use function strtolower; use function strtolower;
@ -26,6 +27,24 @@ class YieldFromAnalyzer
} }
if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) { if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) {
$key_type = null;
$value_type = null;
$always_non_empty_array = true;
if (ForeachAnalyzer::checkIteratorType(
$statements_analyzer,
$stmt,
$stmt->expr,
$stmt_expr_type,
$statements_analyzer->getCodebase(),
$context,
$key_type,
$value_type,
$always_non_empty_array
) === false
) {
return false;
}
$yield_from_type = null; $yield_from_type = null;
foreach ($stmt_expr_type->getAtomicTypes() as $atomic_type) { foreach ($stmt_expr_type->getAtomicTypes() as $atomic_type) {

View File

@ -314,6 +314,63 @@ class GeneratorTest extends TestCase
}', }',
'error_message' => 'NullableReturnStatement', 'error_message' => 'NullableReturnStatement',
], ],
'invalidIterator' => [
'<?php
function example() : int {
return 0;
}
function example2() : Generator {
yield from example();
}',
'error_message' => 'InvalidIterator',
],
'rawObjectIteration' => [
'<?php
class A {
/** @var ?string */
public $foo;
}
function example() : Generator {
$arr = new A;
yield from $arr;
}',
'error_message' => 'RawObjectIteration',
],
'possibleRawObjectIteration' => [
'<?php
class A {
/** @var ?string */
public $foo;
}
class B extends A {}
function bar(A $a): void {}
function gen() : Generator {
$arr = [];
if (rand(0, 10) > 5) {
$arr[] = new A;
} else {
$arr = new B;
}
yield from $arr;
}',
'error_message' => 'PossibleRawObjectIteration',
],
'possibleRawObjectIterationFromIsset' => [
'<?php
function foo(array $a) : Generator {
if (isset($a["a"]["b"])) {
yield from $a["a"];
}
}',
'error_message' => 'PossibleRawObjectIteration',
],
]; ];
} }
} }

View File

@ -2024,7 +2024,8 @@ class ClassTemplateTest extends TestCase
], ],
'yieldFromGenericObjectNotExtendingIterator' => [ 'yieldFromGenericObjectNotExtendingIterator' => [
'<?php '<?php
class Foo{} /** @extends \ArrayObject<int, int> */
class Foo extends \ArrayObject {}
class A { class A {
/** /**