1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 11:26:55 +01:00
psalm/src/Psalm/Checker/Statements/Block/ForeachChecker.php

469 lines
19 KiB
PHP
Raw Normal View History

2016-10-22 19:23:18 +02:00
<?php
namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\Checker\ClassLikeChecker;
use Psalm\Checker\CommentChecker;
use Psalm\Checker\Statements\Expression\AssignmentChecker;
use Psalm\Checker\Statements\ExpressionChecker;
2016-11-02 07:29:00 +01:00
use Psalm\Checker\StatementsChecker;
use Psalm\CodeLocation;
use Psalm\Context;
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;
use Psalm\Issue\PossiblyFalseIterator;
use Psalm\Issue\PossiblyInvalidIterator;
2017-05-22 17:59:58 +02:00
use Psalm\Issue\PossiblyNullIterator;
use Psalm\Issue\RawObjectIteration;
2017-05-25 04:07:49 +02:00
use Psalm\IssueBuffer;
2017-12-03 00:28:18 +01:00
use Psalm\Scope\LoopScope;
2016-10-22 19:23:18 +02:00
use Psalm\Type;
class ForeachChecker
{
/**
2016-11-02 07:29:00 +01:00
* @param StatementsChecker $statements_checker
* @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
*/
public static function analyze(
2016-11-02 07:29:00 +01:00
StatementsChecker $statements_checker,
PhpParser\Node\Stmt\Foreach_ $stmt,
Context $context
) {
if (ExpressionChecker::analyze($statements_checker, $stmt->expr, $context) === false) {
2016-10-22 19:23:18 +02:00
return false;
}
$foreach_context = clone $context;
$foreach_context->inside_loop = true;
$project_checker = $statements_checker->getFileChecker()->project_checker;
2018-02-01 06:50:01 +01:00
$codebase = $project_checker->codebase;
if ($project_checker->alter_code) {
$foreach_context->branch_point =
$foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
2016-10-22 19:23:18 +02:00
$key_type = null;
$value_type = null;
$var_id = ExpressionChecker::getVarId(
$stmt->expr,
2016-11-08 01:16:51 +01:00
$statements_checker->getFQCLN(),
2017-01-07 20:35:07 +01:00
$statements_checker
2016-10-22 19:23:18 +02:00
);
if (isset($stmt->expr->inferredType)) {
$iterator_type = $stmt->expr->inferredType;
} elseif ($var_id && $foreach_context->hasVariable($var_id, $statements_checker)) {
2016-10-22 19:23:18 +02:00
$iterator_type = $foreach_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) {
2017-05-22 17:59:58 +02:00
if ($iterator_type->isNull()) {
if (IssueBuffer::accepts(
new NullIterator(
'Cannot iterate over null',
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->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_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
2018-03-17 05:19:55 +01:00
} elseif ($iterator_type->isFalsable() && !$iterator_type->ignore_falsable_issues) {
if (IssueBuffer::accepts(
new PossiblyFalseIterator(
2018-03-17 05:19:55 +01:00
'Cannot iterate over falsable var ' . $iterator_type,
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
2017-05-22 17:59:58 +02:00
}
$has_valid_iterator = false;
$invalid_iterator_types = [];
foreach ($iterator_type->getTypes() as $iterator_type) {
2016-10-22 19:23:18 +02:00
// if it's an empty array, we cannot iterate over it
2017-12-10 22:17:27 +01:00
if ($iterator_type instanceof Type\Atomic\TArray
&& $iterator_type->type_params[1]->isEmpty()
) {
$has_valid_iterator = true;
2016-10-22 19:23:18 +02:00
continue;
}
2018-03-17 05:19:55 +01:00
if ($iterator_type instanceof Type\Atomic\TNull || $iterator_type instanceof Type\Atomic\TFalse) {
2017-05-22 17:59:58 +02:00
continue;
}
if ($iterator_type instanceof Type\Atomic\TArray
|| $iterator_type instanceof Type\Atomic\ObjectLike
) {
if ($iterator_type instanceof Type\Atomic\ObjectLike) {
$iterator_type = $iterator_type->getGenericArrayType();
}
2016-10-22 19:23:18 +02:00
if (!$value_type) {
$value_type = $iterator_type->type_params[1];
2016-11-02 07:29:00 +01:00
} else {
$value_type = Type::combineUnionTypes($value_type, $iterator_type->type_params[1]);
2016-10-22 19:23:18 +02:00
}
$key_type_part = $iterator_type->type_params[0];
2016-10-22 19:23:18 +02:00
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
2016-10-22 19:23:18 +02:00
}
$has_valid_iterator = true;
2016-10-22 19:23:18 +02:00
continue;
}
if ($iterator_type instanceof Type\Atomic\Scalar ||
$iterator_type instanceof Type\Atomic\TVoid
) {
$invalid_iterator_types[] = $iterator_type->getKey();
2016-10-22 19:23:18 +02:00
$value_type = Type::getMixed();
} elseif ($iterator_type instanceof Type\Atomic\TObject ||
$iterator_type instanceof Type\Atomic\TMixed ||
$iterator_type instanceof Type\Atomic\TEmpty
) {
$has_valid_iterator = true;
$value_type = Type::getMixed();
} elseif ($iterator_type instanceof Type\Atomic\TNamedObject) {
if ($iterator_type->value !== 'Traversable' &&
$iterator_type->value !== $statements_checker->getClassName()
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$statements_checker,
$iterator_type->value,
new CodeLocation($statements_checker->getSource(), $stmt->expr),
2016-10-22 19:23:18 +02:00
$statements_checker->getSuppressedIssues()
) === false) {
2016-10-22 19:23:18 +02:00
return false;
}
}
2016-11-02 07:29:00 +01:00
$has_valid_iterator = true;
if ($iterator_type instanceof Type\Atomic\TGenericObject &&
(strtolower($iterator_type->value) === 'iterable' ||
strtolower($iterator_type->value) === 'traversable' ||
2018-02-01 06:50:01 +01:00
$codebase->classImplements(
$iterator_type->value,
'Traversable'
))
) {
$value_index = count($iterator_type->type_params) - 1;
$value_type_part = $iterator_type->type_params[$value_index];
if (!$value_type) {
$value_type = $value_type_part;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
if ($value_index) {
$key_type_part = $iterator_type->type_params[0];
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
}
}
continue;
}
2018-03-03 22:52:04 +01:00
if (!$codebase->classlikes->classOrInterfaceExists($iterator_type->value)) {
continue;
}
2018-02-01 06:50:01 +01:00
if ($codebase->classImplements(
$iterator_type->value,
'IteratorAggregate'
) ||
(
$codebase->interfaceExists($iterator_type->value)
&& $codebase->interfaceExtends(
$iterator_type->value,
'IteratorAggregate'
)
)
) {
$iterator_method = $iterator_type->value . '::getIterator';
$self_class = $iterator_type->value;
$iterator_class_type = $codebase->methods->getMethodReturnType(
$iterator_method,
$self_class
);
if ($iterator_class_type) {
$array_type = ExpressionChecker::fleshOutType(
$project_checker,
$iterator_class_type,
$self_class,
$self_class
);
foreach ($array_type->getTypes() as $array_atomic_type) {
if ($array_atomic_type instanceof Type\Atomic\TArray
|| $array_atomic_type instanceof Type\Atomic\ObjectLike
) {
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
2018-04-19 05:49:09 +02:00
$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];
} elseif ($array_atomic_type instanceof Type\Atomic\TGenericObject) {
$type_param_count = count($array_atomic_type->type_params);
$value_type_part = $array_atomic_type->type_params[$type_param_count - 1];
$key_type_part = $type_param_count > 1
? $array_atomic_type->type_params[0]
: Type::getMixed();
} else {
$key_type = Type::getMixed();
$value_type = Type::getMixed();
break;
}
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
}
if (!$value_type) {
$value_type = $value_type_part;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
}
} else {
$value_type = Type::getMixed();
}
} elseif ($codebase->classImplements(
$iterator_type->value,
'Iterator'
) ||
2017-12-01 01:00:09 +01:00
(
2018-02-01 06:50:01 +01:00
$codebase->interfaceExists($iterator_type->value)
&& $codebase->interfaceExtends(
$iterator_type->value,
'Iterator'
)
)
) {
$iterator_method = $iterator_type->value . '::current';
2018-01-26 16:59:30 +01:00
$self_class = $iterator_type->value;
2018-02-04 00:52:35 +01:00
$iterator_class_type = $codebase->methods->getMethodReturnType(
2018-01-26 16:59:30 +01:00
$iterator_method,
$self_class
);
if ($iterator_class_type) {
$value_type_part = ExpressionChecker::fleshOutType(
$project_checker,
$iterator_class_type,
2018-01-26 16:59:30 +01:00
$self_class,
2018-01-26 19:51:00 +01:00
$self_class
);
2017-01-09 06:56:51 +01:00
if (!$value_type) {
$value_type = $value_type_part;
2016-11-02 07:29:00 +01:00
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
2016-10-22 19:23:18 +02:00
}
} else {
$value_type = Type::getMixed();
2016-10-22 19:23:18 +02:00
}
2018-02-01 06:50:01 +01:00
} elseif ($codebase->classImplements(
2017-11-15 17:58:46 +01:00
$iterator_type->value,
'Traversable'
) ||
2017-12-01 01:00:09 +01:00
(
2018-02-01 06:50:01 +01:00
$codebase->interfaceExists($iterator_type->value)
&& $codebase->interfaceExtends(
$iterator_type->value,
'Traversable'
)
)
) {
2017-11-15 17:58:46 +01:00
// @todo try and get value type
} elseif (!in_array(
strtolower($iterator_type->value),
['iterator', 'iterable', 'traversable'],
true
)) {
if (IssueBuffer::accepts(
new RawObjectIteration(
'Possibly undesired iteration over regular object ' . $iterator_type->value,
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
2016-10-22 19:23:18 +02:00
}
}
if ($invalid_iterator_types) {
if ($has_valid_iterator) {
if (IssueBuffer::accepts(
new PossiblyInvalidIterator(
'Cannot iterate over ' . $invalid_iterator_types[0],
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new InvalidIterator(
'Cannot iterate over ' . $invalid_iterator_types[0],
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
}
2016-10-22 19:23:18 +02:00
}
if ($stmt->keyVar && $stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
$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;
$location = new CodeLocation($statements_checker, $stmt->keyVar);
if ($context->collect_references && !isset($foreach_context->byref_constraints[$key_var_id])) {
$foreach_context->unreferenced_vars[$key_var_id] = $location;
}
if (!$statements_checker->hasVariable($key_var_id)) {
$statements_checker->registerVariable(
$key_var_id,
$location,
$foreach_context->branch_point
);
}
if ($stmt->byRef) {
$statements_checker->registerVariableUse($location);
}
2016-10-22 19:23:18 +02: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]
= new \Psalm\ReferenceConstraint($value_type);
}
AssignmentChecker::analyze(
$statements_checker,
$stmt->valueVar,
null,
$value_type ?: Type::getMixed(),
$foreach_context,
(string)$stmt->getDocComment()
);
2016-10-22 19:23:18 +02:00
2017-03-02 04:27:52 +01:00
$doc_comment_text = (string)$stmt->getDocComment();
if ($doc_comment_text) {
$var_comments = [];
try {
$var_comments = CommentChecker::getTypeFromComment(
$doc_comment_text,
$statements_checker->getSource(),
$statements_checker->getSource()->getAliases()
);
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_checker, $stmt)
)
)) {
// fall through
}
}
foreach ($var_comments as $var_comment) {
if (!$var_comment->var_id) {
continue;
}
$comment_type = ExpressionChecker::fleshOutType(
$project_checker,
$var_comment->type,
2018-01-26 19:51:00 +01:00
$context->self,
$context->self
);
$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);
$protected_var_ids = $context->protected_var_ids;
if ($var_id) {
$protected_var_ids[$var_id] = true;
}
$loop_scope->protected_var_ids = $protected_var_ids;
2017-12-03 00:28:18 +01:00
LoopChecker::analyze($statements_checker, $stmt->stmts, [], [], $loop_scope);
2016-10-22 19:23:18 +02:00
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
);
$context->referenced_var_ids = array_merge(
$foreach_context->referenced_var_ids,
$context->referenced_var_ids
);
if ($context->collect_references) {
$context->unreferenced_vars = $foreach_context->unreferenced_vars;
}
2016-11-02 07:29:00 +01:00
return null;
2016-10-22 19:23:18 +02:00
}
}