2016-10-22 19:23:18 +02:00
|
|
|
|
<?php
|
2018-11-06 03:57:36 +01:00
|
|
|
|
namespace Psalm\Internal\Analyzer\Statements\Block;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
|
|
|
|
use PhpParser;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
use Psalm\Codebase;
|
2018-11-06 03:57:36 +01:00
|
|
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\CommentAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
|
|
|
use Psalm\Internal\Analyzer\TypeAnalyzer;
|
2019-09-26 21:08:05 +02:00
|
|
|
|
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
2017-05-19 06:48:26 +02:00
|
|
|
|
use Psalm\CodeLocation;
|
|
|
|
|
use Psalm\Context;
|
2017-11-15 03:43:31 +01:00
|
|
|
|
use Psalm\Exception\DocblockParseException;
|
|
|
|
|
use Psalm\Issue\InvalidDocblock;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
use Psalm\Issue\InvalidIterator;
|
2017-05-22 17:59:58 +02:00
|
|
|
|
use Psalm\Issue\NullIterator;
|
2018-03-21 03:59:22 +01:00
|
|
|
|
use Psalm\Issue\PossiblyFalseIterator;
|
|
|
|
|
use Psalm\Issue\PossiblyInvalidIterator;
|
2017-05-22 17:59:58 +02:00
|
|
|
|
use Psalm\Issue\PossiblyNullIterator;
|
2019-08-20 18:06:02 +02:00
|
|
|
|
use Psalm\Issue\PossibleRawObjectIteration;
|
2017-11-15 04:55:48 +01:00
|
|
|
|
use Psalm\Issue\RawObjectIteration;
|
2019-09-19 17:59:43 +02:00
|
|
|
|
use Psalm\Issue\UnnecessaryVarAnnotation;
|
2017-05-25 04:07:49 +02:00
|
|
|
|
use Psalm\IssueBuffer;
|
2018-11-06 03:57:36 +01:00
|
|
|
|
use Psalm\Internal\Scope\LoopScope;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
use Psalm\Type;
|
2019-06-26 22:52:29 +02:00
|
|
|
|
use function is_string;
|
|
|
|
|
use function in_array;
|
|
|
|
|
use function array_merge;
|
|
|
|
|
use function array_intersect_key;
|
|
|
|
|
use function array_values;
|
|
|
|
|
use function strtolower;
|
|
|
|
|
use function array_map;
|
|
|
|
|
use function array_search;
|
|
|
|
|
use function array_keys;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
class ForeachAnalyzer
|
2016-10-22 19:23:18 +02:00
|
|
|
|
{
|
|
|
|
|
/**
|
2018-11-11 18:01:14 +01:00
|
|
|
|
* @param StatementsAnalyzer $statements_analyzer
|
2016-11-02 07:29:00 +01:00
|
|
|
|
* @param PhpParser\Node\Stmt\Foreach_ $stmt
|
|
|
|
|
* @param Context $context
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
|
* @return false|null
|
2016-10-22 19:23:18 +02:00
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
|
public static function analyze(
|
2018-11-11 18:01:14 +01:00
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
2016-11-02 07:29:00 +01:00
|
|
|
|
PhpParser\Node\Stmt\Foreach_ $stmt,
|
|
|
|
|
Context $context
|
|
|
|
|
) {
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$var_comments = [];
|
|
|
|
|
|
2019-06-01 17:53:32 +02:00
|
|
|
|
$doc_comment = $stmt->getDocComment();
|
2019-01-19 17:31:51 +01:00
|
|
|
|
|
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
|
|
2019-06-01 17:53:32 +02:00
|
|
|
|
if ($doc_comment) {
|
2019-01-19 17:31:51 +01:00
|
|
|
|
try {
|
|
|
|
|
$var_comments = CommentAnalyzer::getTypeFromComment(
|
2019-06-01 17:53:32 +02:00
|
|
|
|
$doc_comment,
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$statements_analyzer->getSource(),
|
2019-06-01 18:25:57 +02:00
|
|
|
|
$statements_analyzer->getSource()->getAliases()
|
2019-01-19 17:31:51 +01:00
|
|
|
|
);
|
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidDocblock(
|
|
|
|
|
(string)$e->getMessage(),
|
|
|
|
|
new CodeLocation($statements_analyzer, $stmt)
|
|
|
|
|
)
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-20 17:49:13 +01:00
|
|
|
|
$safe_var_ids = [];
|
|
|
|
|
|
|
|
|
|
if ($stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
|
|
|
|
|
$safe_var_ids['$' . $stmt->keyVar->name] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($stmt->valueVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->valueVar->name)) {
|
|
|
|
|
$safe_var_ids['$' . $stmt->valueVar->name] = true;
|
|
|
|
|
} elseif ($stmt->valueVar instanceof PhpParser\Node\Expr\List_) {
|
|
|
|
|
foreach ($stmt->valueVar->items as $list_item) {
|
|
|
|
|
if (!$list_item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$list_item_key = $list_item->key;
|
|
|
|
|
$list_item_value = $list_item->value;
|
|
|
|
|
|
|
|
|
|
if ($list_item_value instanceof PhpParser\Node\Expr\Variable && is_string($list_item_value->name)) {
|
|
|
|
|
$safe_var_ids['$' . $list_item_value->name] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($list_item_key instanceof PhpParser\Node\Expr\Variable && is_string($list_item_key->name)) {
|
|
|
|
|
$safe_var_ids['$' . $list_item_key->name] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
foreach ($var_comments as $var_comment) {
|
2019-08-21 17:25:08 +02:00
|
|
|
|
if (!$var_comment->var_id || !$var_comment->type) {
|
2019-01-19 17:31:51 +01:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-20 17:49:13 +01:00
|
|
|
|
if (isset($safe_var_ids[$var_comment->var_id])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$comment_type = ExpressionAnalyzer::fleshOutType(
|
|
|
|
|
$codebase,
|
|
|
|
|
$var_comment->type,
|
|
|
|
|
$context->self,
|
2019-05-25 17:51:09 +02:00
|
|
|
|
$context->self,
|
|
|
|
|
$statements_analyzer->getParentFQCLN()
|
2019-01-19 17:31:51 +01:00
|
|
|
|
);
|
|
|
|
|
|
2019-09-26 21:08:05 +02:00
|
|
|
|
$type_location = null;
|
|
|
|
|
|
2019-06-04 22:36:32 +02:00
|
|
|
|
if ($var_comment->type_start
|
|
|
|
|
&& $var_comment->type_end
|
|
|
|
|
&& $var_comment->line_number
|
|
|
|
|
) {
|
|
|
|
|
$type_location = new CodeLocation\DocblockTypeLocation(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$var_comment->type_start,
|
|
|
|
|
$var_comment->type_end,
|
2019-06-05 06:00:42 +02:00
|
|
|
|
$var_comment->line_number
|
2019-06-04 22:36:32 +02:00
|
|
|
|
);
|
|
|
|
|
|
2019-09-26 21:08:05 +02:00
|
|
|
|
if ($codebase->alter_code) {
|
|
|
|
|
$codebase->classlikes->handleDocblockTypeInMigration(
|
|
|
|
|
$codebase,
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$comment_type,
|
|
|
|
|
$type_location,
|
2019-12-29 00:37:55 +01:00
|
|
|
|
$context->calling_function_id
|
2019-09-26 21:08:05 +02:00
|
|
|
|
);
|
|
|
|
|
}
|
2019-06-04 22:36:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
if (isset($context->vars_in_scope[$var_comment->var_id])
|
2019-03-14 01:15:29 +01:00
|
|
|
|
|| $statements_analyzer->isSuperGlobal($var_comment->var_id)
|
2019-01-19 17:31:51 +01:00
|
|
|
|
) {
|
2019-09-19 17:59:43 +02:00
|
|
|
|
if ($codebase->find_unused_variables
|
|
|
|
|
&& $doc_comment
|
2019-09-26 21:08:05 +02:00
|
|
|
|
&& $type_location
|
2019-09-19 17:59:43 +02:00
|
|
|
|
&& isset($context->vars_in_scope[$var_comment->var_id])
|
|
|
|
|
&& $context->vars_in_scope[$var_comment->var_id]->getId() === $comment_type->getId()
|
2019-12-01 16:19:36 +01:00
|
|
|
|
&& !$comment_type->isMixed()
|
2019-09-19 17:59:43 +02:00
|
|
|
|
) {
|
2019-09-26 21:08:05 +02:00
|
|
|
|
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
|
|
|
|
|
|
|
|
|
|
if ($codebase->alter_code
|
|
|
|
|
&& isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
|
|
|
|
|
) {
|
|
|
|
|
FileManipulationBuffer::addVarAnnotationToRemove($type_location);
|
|
|
|
|
} elseif (IssueBuffer::accepts(
|
2019-09-19 17:59:43 +02:00
|
|
|
|
new UnnecessaryVarAnnotation(
|
2019-12-01 16:19:36 +01:00
|
|
|
|
'The @var ' . $comment_type . ' annotation for '
|
|
|
|
|
. $var_comment->var_id . ' is unnecessary',
|
2019-09-26 21:08:05 +02:00
|
|
|
|
$type_location
|
2019-12-02 21:24:01 +01:00
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
true
|
2019-09-19 17:59:43 +02:00
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$context->vars_in_scope[$var_comment->var_id] = $comment_type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 19:15:23 +02:00
|
|
|
|
$context->inside_assignment = true;
|
2018-11-11 18:01:14 +01:00
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-08-13 19:15:23 +02:00
|
|
|
|
$context->inside_assignment = false;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
|
|
|
|
$key_type = null;
|
|
|
|
|
$value_type = null;
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$always_non_empty_array = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$var_id = ExpressionAnalyzer::getVarId(
|
2016-10-22 19:23:18 +02:00
|
|
|
|
$stmt->expr,
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->getFQCLN(),
|
|
|
|
|
$statements_analyzer
|
2016-10-22 19:23:18 +02:00
|
|
|
|
);
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) {
|
|
|
|
|
$iterator_type = $stmt_expr_type;
|
2018-11-11 18:01:14 +01:00
|
|
|
|
} elseif ($var_id && $context->hasVariable($var_id, $statements_analyzer)) {
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$iterator_type = $context->vars_in_scope[$var_id];
|
2016-11-02 07:29:00 +01:00
|
|
|
|
} else {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
$iterator_type = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($iterator_type) {
|
2018-11-21 23:56:04 +01:00
|
|
|
|
if (self::checkIteratorType(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$stmt,
|
|
|
|
|
$iterator_type,
|
|
|
|
|
$codebase,
|
|
|
|
|
$context,
|
|
|
|
|
$key_type,
|
|
|
|
|
$value_type,
|
|
|
|
|
$always_non_empty_array
|
|
|
|
|
) === false
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
2018-03-21 03:59:22 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$foreach_context = clone $context;
|
|
|
|
|
|
2019-06-26 06:14:06 +02:00
|
|
|
|
foreach ($foreach_context->vars_in_scope as $context_var_id => $context_type) {
|
|
|
|
|
$foreach_context->vars_in_scope[$context_var_id] = clone $context_type;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$foreach_context->inside_loop = true;
|
2020-01-27 18:17:12 +01:00
|
|
|
|
$foreach_context->break_types[] = 'loop';
|
2018-11-10 22:10:59 +01:00
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
if ($codebase->alter_code) {
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$foreach_context->branch_point =
|
|
|
|
|
$foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-29 06:32:12 +01:00
|
|
|
|
if ($stmt->keyVar && $stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
|
2017-02-08 00:09:12 +01:00
|
|
|
|
$key_var_id = '$' . $stmt->keyVar->name;
|
|
|
|
|
$foreach_context->vars_in_scope[$key_var_id] = $key_type ?: Type::getMixed();
|
|
|
|
|
$foreach_context->vars_possibly_in_scope[$key_var_id] = true;
|
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$location = new CodeLocation($statements_analyzer, $stmt->keyVar);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
|
|
|
|
if ($context->collect_references && !isset($foreach_context->byref_constraints[$key_var_id])) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$foreach_context->unreferenced_vars[$key_var_id] = [$location->getHash() => $location];
|
2019-05-21 00:25:11 +02:00
|
|
|
|
unset($foreach_context->referenced_var_ids[$key_var_id]);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
if (!$statements_analyzer->hasVariable($key_var_id)) {
|
|
|
|
|
$statements_analyzer->registerVariable(
|
2017-02-08 00:09:12 +01:00
|
|
|
|
$key_var_id,
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$location,
|
2018-01-21 22:24:20 +01:00
|
|
|
|
$foreach_context->branch_point
|
2017-02-08 00:09:12 +01:00
|
|
|
|
);
|
2018-06-17 06:52:32 +02:00
|
|
|
|
} else {
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->registerVariableAssignment(
|
2018-06-17 06:52:32 +02:00
|
|
|
|
$key_var_id,
|
|
|
|
|
$location
|
|
|
|
|
);
|
2017-02-08 00:09:12 +01:00
|
|
|
|
}
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
|
if ($stmt->byRef && $context->collect_references) {
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->registerVariableUses([$location->getHash() => $location]);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
|
if ($context->collect_references
|
|
|
|
|
&& $stmt->byRef
|
|
|
|
|
&& $stmt->valueVar instanceof PhpParser\Node\Expr\Variable
|
|
|
|
|
&& is_string($stmt->valueVar->name)
|
|
|
|
|
) {
|
|
|
|
|
$foreach_context->byref_constraints['$' . $stmt->valueVar->name]
|
2018-11-06 03:57:36 +01:00
|
|
|
|
= new \Psalm\Internal\ReferenceConstraint($value_type);
|
2018-01-25 07:04:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
AssignmentAnalyzer::analyze(
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer,
|
2016-12-04 20:14:00 +01:00
|
|
|
|
$stmt->valueVar,
|
|
|
|
|
null,
|
|
|
|
|
$value_type ?: Type::getMixed(),
|
|
|
|
|
$foreach_context,
|
2019-06-01 17:53:32 +02:00
|
|
|
|
$doc_comment
|
2016-12-04 20:14:00 +01:00
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
foreach ($var_comments as $var_comment) {
|
2019-08-21 17:25:08 +02:00
|
|
|
|
if (!$var_comment->var_id || !$var_comment->type) {
|
2019-01-19 17:31:51 +01:00
|
|
|
|
continue;
|
2017-11-15 03:43:31 +01:00
|
|
|
|
}
|
2017-05-25 07:32:34 +02:00
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$comment_type = ExpressionAnalyzer::fleshOutType(
|
|
|
|
|
$codebase,
|
|
|
|
|
$var_comment->type,
|
|
|
|
|
$context->self,
|
2019-05-25 17:51:09 +02:00
|
|
|
|
$context->self,
|
|
|
|
|
$statements_analyzer->getParentFQCLN()
|
2019-01-19 17:31:51 +01:00
|
|
|
|
);
|
2017-10-07 16:22:52 +02:00
|
|
|
|
|
2019-01-19 17:31:51 +01:00
|
|
|
|
$foreach_context->vars_in_scope[$var_comment->var_id] = $comment_type;
|
2017-03-02 04:27:52 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
|
$loop_scope = new LoopScope($foreach_context, $context);
|
|
|
|
|
|
2020-01-24 19:55:30 +01:00
|
|
|
|
$loop_scope->protected_var_ids = $context->protected_var_ids;
|
2017-12-17 16:58:03 +01:00
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
LoopAnalyzer::analyze($statements_analyzer, $stmt->stmts, [], [], $loop_scope, $inner_loop_context);
|
2018-11-10 22:10:59 +01:00
|
|
|
|
|
|
|
|
|
if (!$inner_loop_context) {
|
|
|
|
|
throw new \UnexpectedValueException('There should be an inner loop context');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($always_non_empty_array) {
|
|
|
|
|
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
|
|
|
|
|
// if there are break statements in the loop it's not certain
|
|
|
|
|
// that the loop has finished executing, so the assertions at the end
|
|
|
|
|
// the loop in the while conditional may not hold
|
2018-11-06 03:57:36 +01:00
|
|
|
|
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
|
|
|
|
|
|| in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
|
2018-11-10 22:10:59 +01:00
|
|
|
|
) {
|
|
|
|
|
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
|
|
|
|
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
|
|
|
|
$type,
|
|
|
|
|
$loop_scope->possibly_defined_loop_parent_vars[$var_id]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-08-12 22:01:24 +02:00
|
|
|
|
if ($context->collect_references
|
|
|
|
|
&& !isset($context->vars_in_scope[$var_id])
|
|
|
|
|
&& isset($inner_loop_context->unreferenced_vars[$var_id])
|
|
|
|
|
) {
|
|
|
|
|
$context->unreferenced_vars[$var_id] = $inner_loop_context->unreferenced_vars[$var_id];
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$context->vars_in_scope[$var_id] = $type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2018-11-18 17:39:14 +01:00
|
|
|
|
$foreach_context->loop_scope = null;
|
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
|
$foreach_context->vars_possibly_in_scope,
|
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
|
);
|
|
|
|
|
|
2019-05-21 00:25:11 +02:00
|
|
|
|
$context->referenced_var_ids = array_intersect_key(
|
2017-11-24 18:17:28 +01:00
|
|
|
|
$foreach_context->referenced_var_ids,
|
|
|
|
|
$context->referenced_var_ids
|
|
|
|
|
);
|
2017-02-01 05:24:33 +01:00
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
|
if ($context->collect_exceptions) {
|
2019-03-29 00:43:14 +01:00
|
|
|
|
$context->mergeExceptions($foreach_context);
|
2018-06-22 07:13:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
|
if ($context->collect_references) {
|
2018-06-17 03:14:19 +02:00
|
|
|
|
foreach ($foreach_context->unreferenced_vars as $var_id => $locations) {
|
|
|
|
|
if (isset($context->unreferenced_vars[$var_id])) {
|
|
|
|
|
$context->unreferenced_vars[$var_id] += $locations;
|
|
|
|
|
} else {
|
|
|
|
|
$context->unreferenced_vars[$var_id] = $locations;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-25 07:04:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param ?Type\Union $key_type
|
|
|
|
|
* @param ?Type\Union $value_type
|
|
|
|
|
* @return false|null
|
|
|
|
|
*/
|
|
|
|
|
public static function checkIteratorType(
|
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
|
PhpParser\Node\Stmt\Foreach_ $stmt,
|
|
|
|
|
Type\Union $iterator_type,
|
|
|
|
|
Codebase $codebase,
|
|
|
|
|
Context $context,
|
|
|
|
|
&$key_type,
|
|
|
|
|
&$value_type,
|
|
|
|
|
bool &$always_non_empty_array
|
|
|
|
|
) {
|
|
|
|
|
if ($iterator_type->isNull()) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new NullIterator(
|
|
|
|
|
'Cannot iterate over null',
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} elseif ($iterator_type->isNullable() && !$iterator_type->ignore_nullable_issues) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyNullIterator(
|
|
|
|
|
'Cannot iterate over nullable var ' . $iterator_type,
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} elseif ($iterator_type->isFalsable() && !$iterator_type->ignore_falsable_issues) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyFalseIterator(
|
|
|
|
|
'Cannot iterate over falsable var ' . $iterator_type,
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$has_valid_iterator = false;
|
|
|
|
|
$invalid_iterator_types = [];
|
2019-08-20 18:06:02 +02:00
|
|
|
|
$raw_object_types = [];
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($iterator_type->getAtomicTypes() as $iterator_atomic_type) {
|
2019-02-22 03:40:06 +01:00
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$iterator_atomic_type = array_values($iterator_atomic_type->as->getAtomicTypes())[0];
|
2019-01-05 16:32:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 23:56:04 +01:00
|
|
|
|
// if it's an empty array, we cannot iterate over it
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TArray
|
|
|
|
|
&& $iterator_atomic_type->type_params[1]->isEmpty()
|
|
|
|
|
) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
$has_valid_iterator = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TNull
|
|
|
|
|
|| $iterator_atomic_type instanceof Type\Atomic\TFalse
|
|
|
|
|
) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TArray
|
|
|
|
|
|| $iterator_atomic_type instanceof Type\Atomic\ObjectLike
|
2019-10-09 00:44:46 +02:00
|
|
|
|
|| $iterator_atomic_type instanceof Type\Atomic\TList
|
2018-11-21 23:56:04 +01:00
|
|
|
|
) {
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\ObjectLike) {
|
|
|
|
|
if (!$iterator_atomic_type->sealed) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
}
|
|
|
|
|
$iterator_atomic_type = $iterator_atomic_type->getGenericArrayType();
|
2019-10-09 00:44:46 +02:00
|
|
|
|
} elseif ($iterator_atomic_type instanceof Type\Atomic\TList) {
|
|
|
|
|
if (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyList) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$iterator_atomic_type = new Type\Atomic\TArray([
|
|
|
|
|
Type::getInt(),
|
|
|
|
|
$iterator_atomic_type->type_param
|
|
|
|
|
]);
|
2018-11-21 23:56:04 +01:00
|
|
|
|
} elseif (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyArray) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $iterator_atomic_type->type_params[1];
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $iterator_atomic_type->type_params[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key_type_part = $iterator_atomic_type->type_params[0];
|
|
|
|
|
|
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$has_valid_iterator = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\Scalar ||
|
|
|
|
|
$iterator_atomic_type instanceof Type\Atomic\TVoid
|
|
|
|
|
) {
|
|
|
|
|
$invalid_iterator_types[] = $iterator_atomic_type->getKey();
|
|
|
|
|
|
|
|
|
|
$value_type = Type::getMixed();
|
|
|
|
|
} elseif ($iterator_atomic_type instanceof Type\Atomic\TObject ||
|
|
|
|
|
$iterator_atomic_type instanceof Type\Atomic\TMixed ||
|
|
|
|
|
$iterator_atomic_type instanceof Type\Atomic\TEmpty
|
|
|
|
|
) {
|
|
|
|
|
$has_valid_iterator = true;
|
|
|
|
|
$value_type = Type::getMixed();
|
2019-01-04 20:54:40 +01:00
|
|
|
|
} elseif ($iterator_atomic_type instanceof Type\Atomic\TIterable) {
|
2019-05-30 01:58:54 +02:00
|
|
|
|
if ($iterator_atomic_type->extra_types) {
|
|
|
|
|
$iterator_atomic_type_copy = clone $iterator_atomic_type;
|
|
|
|
|
$iterator_atomic_type_copy->extra_types = [];
|
|
|
|
|
$iterator_atomic_types = [$iterator_atomic_type_copy];
|
|
|
|
|
$iterator_atomic_types = array_merge(
|
|
|
|
|
$iterator_atomic_types,
|
|
|
|
|
$iterator_atomic_type->extra_types
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$iterator_atomic_types = [$iterator_atomic_type];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$intersection_value_type = null;
|
|
|
|
|
$intersection_key_type = null;
|
|
|
|
|
|
|
|
|
|
foreach ($iterator_atomic_types as $iat) {
|
|
|
|
|
if (!$iat instanceof Type\Atomic\TIterable) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$value_type_part = $iat->type_params[1];
|
|
|
|
|
$key_type_part = $iat->type_params[0];
|
|
|
|
|
|
|
|
|
|
if (!$intersection_value_type) {
|
|
|
|
|
$intersection_value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$intersection_value_type = Type::intersectUnionTypes(
|
|
|
|
|
$intersection_value_type,
|
|
|
|
|
$value_type_part
|
2019-12-20 02:42:57 +01:00
|
|
|
|
) ?: Type::getMixed();
|
2019-05-30 01:58:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$intersection_key_type) {
|
|
|
|
|
$intersection_key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$intersection_key_type = Type::intersectUnionTypes(
|
|
|
|
|
$intersection_key_type,
|
|
|
|
|
$key_type_part
|
2019-12-20 02:42:57 +01:00
|
|
|
|
) ?: Type::getMixed();
|
2019-05-30 01:58:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$intersection_value_type || !$intersection_key_type) {
|
|
|
|
|
throw new \UnexpectedValueException('Should not happen');
|
|
|
|
|
}
|
2019-01-04 20:54:40 +01:00
|
|
|
|
|
|
|
|
|
if (!$value_type) {
|
2019-05-30 01:58:54 +02:00
|
|
|
|
$value_type = $intersection_value_type;
|
2019-01-04 20:54:40 +01:00
|
|
|
|
} else {
|
2019-05-30 01:58:54 +02:00
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $intersection_value_type);
|
2019-01-04 20:54:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$key_type) {
|
2019-05-30 01:58:54 +02:00
|
|
|
|
$key_type = $intersection_key_type;
|
2019-01-04 20:54:40 +01:00
|
|
|
|
} else {
|
2019-05-30 01:58:54 +02:00
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $intersection_key_type);
|
2019-01-04 20:54:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$has_valid_iterator = true;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
} elseif ($iterator_atomic_type instanceof Type\Atomic\TNamedObject) {
|
|
|
|
|
if ($iterator_atomic_type->value !== 'Traversable' &&
|
|
|
|
|
$iterator_atomic_type->value !== $statements_analyzer->getClassName()
|
|
|
|
|
) {
|
|
|
|
|
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
|
2020-02-11 22:39:33 +01:00
|
|
|
|
$context->self,
|
2018-11-21 23:56:04 +01:00
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (TypeAnalyzer::isAtomicContainedBy(
|
|
|
|
|
$codebase,
|
|
|
|
|
$iterator_atomic_type,
|
2019-01-23 05:42:54 +01:00
|
|
|
|
new Type\Atomic\TIterable([Type::getMixed(), Type::getMixed()])
|
2018-11-21 23:56:04 +01:00
|
|
|
|
)) {
|
|
|
|
|
self::handleIterable(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$iterator_atomic_type,
|
|
|
|
|
$stmt->expr,
|
|
|
|
|
$codebase,
|
|
|
|
|
$context,
|
|
|
|
|
$key_type,
|
|
|
|
|
$value_type,
|
|
|
|
|
$has_valid_iterator
|
|
|
|
|
);
|
|
|
|
|
} else {
|
2019-08-20 18:06:02 +02:00
|
|
|
|
$raw_object_types[] = $iterator_atomic_type->value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($raw_object_types) {
|
|
|
|
|
if ($has_valid_iterator) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossibleRawObjectIteration(
|
|
|
|
|
'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new RawObjectIteration(
|
|
|
|
|
'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($invalid_iterator_types) {
|
|
|
|
|
if ($has_valid_iterator) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyInvalidIterator(
|
|
|
|
|
'Cannot iterate over ' . $invalid_iterator_types[0],
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidIterator(
|
|
|
|
|
'Cannot iterate over ' . $invalid_iterator_types[0],
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param ?Type\Union $key_type
|
|
|
|
|
* @param ?Type\Union $value_type
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public static function handleIterable(
|
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
|
Type\Atomic\TNamedObject $iterator_atomic_type,
|
|
|
|
|
PhpParser\Node\Expr $foreach_expr,
|
|
|
|
|
Codebase $codebase,
|
|
|
|
|
Context $context,
|
|
|
|
|
&$key_type,
|
|
|
|
|
&$value_type,
|
|
|
|
|
bool &$has_valid_iterator
|
|
|
|
|
) {
|
|
|
|
|
if ($iterator_atomic_type->extra_types) {
|
|
|
|
|
$iterator_atomic_type_copy = clone $iterator_atomic_type;
|
|
|
|
|
$iterator_atomic_type_copy->extra_types = [];
|
|
|
|
|
$iterator_atomic_types = [$iterator_atomic_type_copy];
|
|
|
|
|
$iterator_atomic_types = array_merge($iterator_atomic_types, $iterator_atomic_type->extra_types);
|
|
|
|
|
} else {
|
|
|
|
|
$iterator_atomic_types = [$iterator_atomic_type];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($iterator_atomic_types as $iterator_atomic_type) {
|
2019-06-19 18:00:07 +02:00
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam
|
|
|
|
|
|| $iterator_atomic_type instanceof Type\Atomic\TObjectWithProperties
|
|
|
|
|
) {
|
2018-11-21 23:56:04 +01:00
|
|
|
|
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-02 07:10:50 +02:00
|
|
|
|
|
2018-11-21 23:56:04 +01:00
|
|
|
|
$has_valid_iterator = true;
|
|
|
|
|
|
2019-08-23 20:15:20 +02:00
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TNamedObject
|
|
|
|
|
&& strtolower($iterator_atomic_type->value) === 'simplexmlelement'
|
|
|
|
|
) {
|
|
|
|
|
if ($value_type) {
|
|
|
|
|
$value_type = Type::combineUnionTypes(
|
|
|
|
|
$value_type,
|
|
|
|
|
new Type\Union([clone $iterator_atomic_type])
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = new Type\Union([clone $iterator_atomic_type]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($key_type) {
|
|
|
|
|
$key_type = Type::combineUnionTypes(
|
|
|
|
|
$key_type,
|
|
|
|
|
Type::getString()
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::getString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TIterable
|
|
|
|
|
|| (strtolower($iterator_atomic_type->value) === 'traversable'
|
|
|
|
|
|| $codebase->classImplements(
|
2018-11-21 23:56:04 +01:00
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Traversable'
|
2019-01-26 22:58:49 +01:00
|
|
|
|
) ||
|
|
|
|
|
(
|
|
|
|
|
$codebase->interfaceExists($iterator_atomic_type->value)
|
|
|
|
|
&& $codebase->interfaceExtends(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
)
|
2018-11-21 23:56:04 +01:00
|
|
|
|
))
|
|
|
|
|
) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (strtolower($iterator_atomic_type->value) === 'iteratoraggregate'
|
|
|
|
|
|| $codebase->classImplements(
|
2018-11-21 23:56:04 +01:00
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'IteratorAggregate'
|
|
|
|
|
)
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|| ($codebase->interfaceExists($iterator_atomic_type->value)
|
|
|
|
|
&& $codebase->interfaceExtends(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'IteratorAggregate'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$old_data_provider = $statements_analyzer->node_data;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
|
|
|
|
|
$foreach_expr,
|
|
|
|
|
new PhpParser\Node\Identifier('getIterator', $foreach_expr->getAttributes())
|
|
|
|
|
);
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$fake_method_call,
|
|
|
|
|
$context
|
|
|
|
|
);
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = $old_data_provider;
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|
|
|
|
|
if ($iterator_class_type) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($iterator_class_type->getAtomicTypes() as $array_atomic_type) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$key_type_part = null;
|
|
|
|
|
$value_type_part = null;
|
|
|
|
|
|
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TArray
|
|
|
|
|
|| $array_atomic_type instanceof Type\Atomic\ObjectLike
|
|
|
|
|
) {
|
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
|
|
|
|
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key_type_part = $array_atomic_type->type_params[0];
|
|
|
|
|
$value_type_part = $array_atomic_type->type_params[1];
|
|
|
|
|
} else {
|
2019-05-08 15:04:52 +02:00
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TNamedObject
|
2019-05-08 17:03:47 +02:00
|
|
|
|
&& $codebase->classExists($array_atomic_type->value)
|
2019-05-08 15:04:52 +02:00
|
|
|
|
&& $codebase->classImplements(
|
|
|
|
|
$array_atomic_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
$generic_storage = $codebase->classlike_storage_provider->get(
|
|
|
|
|
$array_atomic_type->value
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// The collection might be an iterator, in which case
|
|
|
|
|
// we want to call the iterator function
|
2019-11-11 16:11:42 +01:00
|
|
|
|
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
|
2019-06-25 05:31:06 +02:00
|
|
|
|
if (!isset($generic_storage->template_type_extends['Traversable'])
|
2019-05-08 15:04:52 +02:00
|
|
|
|
|| ($generic_storage
|
2019-06-25 05:31:06 +02:00
|
|
|
|
->template_type_extends['Traversable']['TKey']->isMixed()
|
2019-05-08 15:04:52 +02:00
|
|
|
|
&& $generic_storage
|
2019-06-25 05:31:06 +02:00
|
|
|
|
->template_type_extends['Traversable']['TValue']->isMixed())
|
2019-05-08 15:04:52 +02:00
|
|
|
|
) {
|
|
|
|
|
self::handleIterable(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$array_atomic_type,
|
|
|
|
|
$fake_method_call,
|
|
|
|
|
$codebase,
|
|
|
|
|
$context,
|
|
|
|
|
$key_type,
|
|
|
|
|
$value_type,
|
|
|
|
|
$has_valid_iterator
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TIterable
|
|
|
|
|
|| ($array_atomic_type instanceof Type\Atomic\TNamedObject
|
2019-06-25 05:31:06 +02:00
|
|
|
|
&& ($array_atomic_type->value === 'Traversable'
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|| ($codebase->classOrInterfaceExists($array_atomic_type->value)
|
|
|
|
|
&& $codebase->classImplements(
|
|
|
|
|
$array_atomic_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
))))
|
|
|
|
|
) {
|
|
|
|
|
self::getKeyValueParamsForTraversableObject(
|
|
|
|
|
$array_atomic_type,
|
|
|
|
|
$codebase,
|
|
|
|
|
$key_type_part,
|
|
|
|
|
$value_type_part
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!$key_type_part || !$value_type_part) {
|
|
|
|
|
break;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
2019-01-26 22:58:49 +01:00
|
|
|
|
}
|
|
|
|
|
} elseif ($codebase->classImplements(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Iterator'
|
|
|
|
|
) ||
|
|
|
|
|
(
|
|
|
|
|
$codebase->interfaceExists($iterator_atomic_type->value)
|
|
|
|
|
&& $codebase->interfaceExtends(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Iterator'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$old_data_provider = $statements_analyzer->node_data;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
|
|
|
|
|
$foreach_expr,
|
|
|
|
|
new PhpParser\Node\Identifier('current', $foreach_expr->getAttributes())
|
|
|
|
|
);
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-02 16:41:43 +01:00
|
|
|
|
$was_inside_call = $context->inside_call;
|
|
|
|
|
|
2020-01-02 00:33:12 +01:00
|
|
|
|
$context->inside_call = true;
|
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$fake_method_call,
|
|
|
|
|
$context
|
|
|
|
|
);
|
|
|
|
|
|
2020-01-02 16:41:43 +01:00
|
|
|
|
$context->inside_call = $was_inside_call;
|
2020-01-02 00:33:12 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = $old_data_provider;
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|
|
|
|
|
if ($iterator_class_type && !$iterator_class_type->isMixed()) {
|
2018-11-21 23:56:04 +01:00
|
|
|
|
if (!$value_type) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$value_type = $iterator_class_type;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
} else {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $iterator_class_type);
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|
|
|
|
|
if (!$key_type && !$value_type) {
|
|
|
|
|
self::getKeyValueParamsForTraversableObject(
|
|
|
|
|
$iterator_atomic_type,
|
|
|
|
|
$codebase,
|
|
|
|
|
$key_type,
|
|
|
|
|
$value_type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$codebase->classlikes->classOrInterfaceExists($iterator_atomic_type->value)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param ?Type\Union $key_type
|
|
|
|
|
* @param ?Type\Union $value_type
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public static function getKeyValueParamsForTraversableObject(
|
|
|
|
|
Type\Atomic $iterator_atomic_type,
|
|
|
|
|
Codebase $codebase,
|
|
|
|
|
&$key_type,
|
|
|
|
|
&$value_type
|
|
|
|
|
) {
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TIterable
|
|
|
|
|
|| ($iterator_atomic_type instanceof Type\Atomic\TGenericObject
|
|
|
|
|
&& strtolower($iterator_atomic_type->value) === 'traversable')
|
|
|
|
|
) {
|
|
|
|
|
$value_type_part = $iterator_atomic_type->type_params[1];
|
|
|
|
|
|
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key_type_part = $iterator_atomic_type->type_params[0];
|
|
|
|
|
|
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($iterator_atomic_type instanceof Type\Atomic\TNamedObject
|
2020-02-15 16:47:07 +01:00
|
|
|
|
&& (
|
|
|
|
|
$codebase->classImplements(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
)
|
|
|
|
|
|| $codebase->interfaceExtends(
|
|
|
|
|
$iterator_atomic_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
)
|
2019-01-26 22:58:49 +01:00
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
$generic_storage = $codebase->classlike_storage_provider->get(
|
|
|
|
|
$iterator_atomic_type->value
|
|
|
|
|
);
|
|
|
|
|
|
2019-06-25 05:31:06 +02:00
|
|
|
|
if (!isset($generic_storage->template_type_extends['Traversable'])) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($generic_storage->template_types
|
|
|
|
|
|| $iterator_atomic_type instanceof Type\Atomic\TGenericObject
|
2018-11-21 23:56:04 +01:00
|
|
|
|
) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
// if we're just being passed the non-generic class itself, assume
|
|
|
|
|
// that it's inside the calling class
|
|
|
|
|
$passed_type_params = $iterator_atomic_type instanceof Type\Atomic\TGenericObject
|
|
|
|
|
? $iterator_atomic_type->type_params
|
|
|
|
|
: array_values(
|
|
|
|
|
array_map(
|
2019-03-22 20:59:10 +01:00
|
|
|
|
/** @param array<string, array{0:Type\Union}> $arr */
|
|
|
|
|
function (array $arr) use ($iterator_atomic_type) : Type\Union {
|
|
|
|
|
if (isset($arr[$iterator_atomic_type->value])) {
|
|
|
|
|
return $arr[$iterator_atomic_type->value][0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Type::getMixed();
|
2019-01-26 22:58:49 +01:00
|
|
|
|
},
|
|
|
|
|
$generic_storage->template_types
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$passed_type_params = null;
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$key_type = self::getExtendedType(
|
|
|
|
|
'TKey',
|
2019-06-25 05:31:06 +02:00
|
|
|
|
'Traversable',
|
|
|
|
|
$generic_storage->name,
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$generic_storage->template_type_extends,
|
|
|
|
|
$generic_storage->template_types,
|
|
|
|
|
$passed_type_params
|
|
|
|
|
);
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$value_type = self::getExtendedType(
|
|
|
|
|
'TValue',
|
2019-06-25 05:31:06 +02:00
|
|
|
|
'Traversable',
|
|
|
|
|
$generic_storage->name,
|
2019-01-26 22:58:49 +01:00
|
|
|
|
$generic_storage->template_type_extends,
|
|
|
|
|
$generic_storage->template_types,
|
|
|
|
|
$passed_type_params
|
|
|
|
|
);
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $template_name
|
2019-03-16 16:15:25 +01:00
|
|
|
|
* @param array<string, array<int|string, Type\Union>> $template_type_extends
|
2019-03-22 20:59:10 +01:00
|
|
|
|
* @param array<string, array<string, array{Type\Union}>> $class_template_types
|
2019-01-26 22:58:49 +01:00
|
|
|
|
* @param array<int, Type\Union> $calling_type_params
|
|
|
|
|
* @return Type\Union|null
|
|
|
|
|
*/
|
|
|
|
|
private static function getExtendedType(
|
|
|
|
|
string $template_name,
|
2019-06-25 05:31:06 +02:00
|
|
|
|
string $template_class,
|
|
|
|
|
string $calling_class,
|
2019-01-26 22:58:49 +01:00
|
|
|
|
array $template_type_extends,
|
|
|
|
|
array $class_template_types = null,
|
|
|
|
|
array $calling_type_params = null
|
|
|
|
|
) {
|
2019-06-25 05:31:06 +02:00
|
|
|
|
if ($calling_class === $template_class) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
if (isset($class_template_types[$template_name]) && $calling_type_params) {
|
|
|
|
|
$offset = array_search($template_name, array_keys($class_template_types));
|
|
|
|
|
|
2019-10-21 13:13:27 +02:00
|
|
|
|
if ($offset !== false && isset($calling_type_params[$offset])) {
|
2019-01-26 22:58:49 +01:00
|
|
|
|
return $calling_type_params[$offset];
|
2018-11-26 22:50:47 +01:00
|
|
|
|
}
|
2019-01-26 22:58:49 +01:00
|
|
|
|
}
|
2018-11-26 22:50:47 +01:00
|
|
|
|
|
2019-01-26 22:58:49 +01:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-11-21 23:56:04 +01:00
|
|
|
|
|
2019-06-25 05:31:06 +02:00
|
|
|
|
if (isset($template_type_extends[$template_class][$template_name])) {
|
|
|
|
|
$extended_type = $template_type_extends[$template_class][$template_name];
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|
2019-03-16 16:15:25 +01:00
|
|
|
|
$return_type = null;
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($extended_type->getAtomicTypes() as $extended_atomic_type) {
|
2019-03-16 16:15:25 +01:00
|
|
|
|
if (!$extended_atomic_type instanceof Type\Atomic\TTemplateParam) {
|
|
|
|
|
if (!$return_type) {
|
|
|
|
|
$return_type = $extended_type;
|
|
|
|
|
} else {
|
|
|
|
|
$return_type = Type::combineUnionTypes(
|
|
|
|
|
$return_type,
|
|
|
|
|
$extended_type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-30 13:25:27 +01:00
|
|
|
|
$candidate_type = self::getExtendedType(
|
|
|
|
|
$extended_atomic_type->param_name,
|
|
|
|
|
$extended_atomic_type->defining_class,
|
|
|
|
|
$calling_class,
|
|
|
|
|
$template_type_extends,
|
|
|
|
|
$class_template_types,
|
|
|
|
|
$calling_type_params
|
|
|
|
|
);
|
2019-03-16 16:15:25 +01:00
|
|
|
|
|
2019-12-30 13:25:27 +01:00
|
|
|
|
if ($candidate_type) {
|
|
|
|
|
if (!$return_type) {
|
|
|
|
|
$return_type = $candidate_type;
|
|
|
|
|
} else {
|
|
|
|
|
$return_type = Type::combineUnionTypes(
|
|
|
|
|
$return_type,
|
|
|
|
|
$candidate_type
|
|
|
|
|
);
|
2019-03-16 16:15:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-01-26 22:58:49 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-16 16:15:25 +01:00
|
|
|
|
if ($return_type) {
|
|
|
|
|
return $return_type;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-01-26 22:58:49 +01:00
|
|
|
|
|
|
|
|
|
return null;
|
2018-11-21 23:56:04 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|