mirror of
https://github.com/danog/psalm.git
synced 2024-11-29 20:28:59 +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:
parent
5eb4d88f1a
commit
ec901d2e44
@ -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()
|
||||||
)) {
|
)) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user