1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 19:07:00 +01:00
psalm/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php

548 lines
20 KiB
PHP
Raw Normal View History

2016-11-01 16:37:58 +01:00
<?php
namespace Psalm\Checker\Statements\Expression;
use PhpParser;
use Psalm\Checker\CommentChecker;
2018-01-14 18:09:40 +01:00
use Psalm\Checker\Statements\Expression\Assignment\ArrayAssignmentChecker;
use Psalm\Checker\Statements\Expression\Assignment\PropertyAssignmentChecker;
2016-11-01 16:37:58 +01:00
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation;
2016-11-01 16:37:58 +01:00
use Psalm\Context;
use Psalm\Exception\DocblockParseException;
use Psalm\Exception\IncorrectDocblockException;
use Psalm\Issue\AssignmentToVoid;
use Psalm\Issue\InvalidDocblock;
2016-11-02 07:29:00 +01:00
use Psalm\Issue\InvalidScope;
use Psalm\Issue\LoopInvalidation;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\MixedAssignment;
use Psalm\Issue\ReferenceConstraintViolation;
2016-11-02 07:29:00 +01:00
use Psalm\IssueBuffer;
2016-11-01 16:37:58 +01:00
use Psalm\Type;
class AssignmentChecker
{
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr $assign_var
2016-12-07 00:27:22 +01:00
* @param PhpParser\Node\Expr|null $assign_value This has to be null to support list destructuring
* @param Type\Union|null $assign_value_type
* @param Context $context
* @param string $doc_comment
* @param int|null $came_from_line_number
2017-05-27 02:16:18 +02:00
*
2016-11-01 16:37:58 +01:00
* @return false|Type\Union
*/
public static function analyze(
2016-11-01 16:37:58 +01:00
StatementsChecker $statements_checker,
PhpParser\Node\Expr $assign_var,
2017-05-05 03:57:26 +02:00
$assign_value,
$assign_value_type,
2016-11-01 16:37:58 +01:00
Context $context,
$doc_comment,
$came_from_line_number = null
2016-11-01 16:37:58 +01:00
) {
$var_id = ExpressionChecker::getVarId(
$assign_var,
2016-11-08 01:16:51 +01:00
$statements_checker->getFQCLN(),
2017-01-07 20:35:07 +01:00
$statements_checker
2016-11-01 16:37:58 +01:00
);
// gets a variable id that *may* contain array keys
2016-11-01 16:37:58 +01:00
$array_var_id = ExpressionChecker::getArrayVarId(
$assign_var,
2016-11-08 01:16:51 +01:00
$statements_checker->getFQCLN(),
2017-01-07 20:35:07 +01:00
$statements_checker
2016-11-01 16:37:58 +01:00
);
$var_comment = null;
$comment_type = null;
2017-03-02 04:27:52 +01:00
if ($doc_comment) {
try {
$var_comment = CommentChecker::getTypeFromComment(
$doc_comment,
$statements_checker->getSource(),
$statements_checker->getAliases(),
null,
$came_from_line_number
);
} catch (IncorrectDocblockException $e) {
if (IssueBuffer::accepts(
new MissingDocblockType(
(string)$e->getMessage(),
new CodeLocation($statements_checker->getSource(), $assign_var)
)
)) {
// fall through
}
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_checker->getSource(), $assign_var)
)
)) {
// fall through
}
}
if ($var_comment) {
$comment_type = ExpressionChecker::fleshOutType(
$statements_checker->getFileChecker()->project_checker,
$var_comment->type,
2018-01-26 19:51:00 +01:00
$context->self,
$context->self
);
$comment_type->setFromDocblock();
if ($var_comment->var_id && $var_comment->var_id !== $var_id) {
$context->vars_in_scope[$var_comment->var_id] = $comment_type;
}
}
2017-03-02 04:27:52 +01:00
}
2016-11-01 16:37:58 +01:00
if ($assign_value && ExpressionChecker::analyze($statements_checker, $assign_value, $context) === false) {
if ($var_id) {
if ($array_var_id) {
2017-04-02 21:26:10 +02:00
$context->removeDescendents($array_var_id, null, $assign_value_type);
}
// if we're not exiting immediately, make everything mixed
$context->vars_in_scope[$var_id] =
$var_comment && (!$var_comment->var_id || $var_comment->var_id === $var_id) && $comment_type
? $comment_type
: Type::getMixed();
}
2016-11-01 16:37:58 +01:00
return false;
}
if ($var_comment && (!$var_comment->var_id || $var_comment->var_id === $var_id) && $comment_type) {
$assign_value_type = $comment_type;
2016-12-17 01:22:30 +01:00
} elseif (!$assign_value_type) {
if (isset($assign_value->inferredType)) {
/** @var Type\Union */
$assign_value_type = $assign_value->inferredType;
} else {
$assign_value_type = Type::getMixed();
}
2016-11-01 16:37:58 +01:00
}
if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) {
2017-04-02 21:26:10 +02:00
// removes dependennt vars from $context
$context->removeDescendents(
$array_var_id,
$context->vars_in_scope[$array_var_id],
$assign_value_type,
$statements_checker
2017-04-02 21:26:10 +02:00
);
} else {
$root_var_id = ExpressionChecker::getRootVarId(
$assign_var,
$statements_checker->getFQCLN(),
$statements_checker
);
if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) {
$context->removeVarFromConflictingClauses(
$root_var_id,
$context->vars_in_scope[$root_var_id],
$statements_checker
);
}
}
$project_checker = $statements_checker->getFileChecker()->project_checker;
2018-02-01 06:50:01 +01:00
$codebase = $project_checker->codebase;
if ($assign_value_type->isMixed()) {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementMixedCount($statements_checker->getCheckedFilePath());
if (IssueBuffer::accepts(
new MixedAssignment(
'Cannot assign ' . $var_id . ' to a mixed type',
new CodeLocation($statements_checker->getSource(), $assign_var)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementNonMixedCount($statements_checker->getCheckedFilePath());
if ($var_id
&& isset($context->byref_constraints[$var_id])
&& ($outer_constraint_type = $context->byref_constraints[$var_id]->type)
) {
if (!TypeChecker::isContainedBy(
2018-02-01 06:50:01 +01:00
$codebase,
$assign_value_type,
$outer_constraint_type,
$assign_value_type->ignore_nullable_issues,
$assign_value_type->ignore_falsable_issues
)
) {
if (IssueBuffer::accepts(
new ReferenceConstraintViolation(
'Variable ' . $var_id . ' is limited to values of type '
. $context->byref_constraints[$var_id]->type
. ' because it is passed by reference',
new CodeLocation($statements_checker->getSource(), $assign_var)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
}
}
if ($var_id === '$this' && IssueBuffer::accepts(
new InvalidScope(
'Cannot re-assign ' . $var_id,
new CodeLocation($statements_checker->getSource(), $assign_var)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
if (isset($context->protected_var_ids[$var_id])) {
if (IssueBuffer::accepts(
new LoopInvalidation(
'Variable ' . $var_id . ' has already been assigned in a for/foreach loop',
new CodeLocation($statements_checker->getSource(), $assign_var)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
2016-11-01 16:37:58 +01:00
if ($assign_var instanceof PhpParser\Node\Expr\Variable && is_string($assign_var->name) && $var_id) {
$context->vars_in_scope[$var_id] = $assign_value_type;
2016-11-01 16:37:58 +01:00
$context->vars_possibly_in_scope[$var_id] = true;
2017-11-25 17:21:45 +01:00
$context->assigned_var_ids[$var_id] = true;
$location = new CodeLocation($statements_checker, $assign_var);
if ($context->collect_references) {
$context->unreferenced_vars[$var_id] = $location;
}
if (!$statements_checker->hasVariable($var_id)) {
$statements_checker->registerVariable(
$var_id,
$location,
$context->branch_point
);
} else {
$statements_checker->registerVariableAssignment(
$var_id,
$location
);
}
if (isset($context->byref_constraints[$var_id])) {
$statements_checker->registerVariableUse($location);
}
2016-12-04 19:35:38 +01:00
} elseif ($assign_var instanceof PhpParser\Node\Expr\List_
|| $assign_var instanceof PhpParser\Node\Expr\Array_
2016-12-04 19:35:38 +01:00
) {
/** @var int $offset */
foreach ($assign_var->items as $offset => $assign_var_item) {
// $assign_var_item can be null e.g. list($a, ) = ['a', 'b']
if (!$assign_var_item) {
2016-11-01 16:37:58 +01:00
continue;
}
$var = $assign_var_item->value;
2016-11-01 19:32:19 +01:00
if ($assign_value instanceof PhpParser\Node\Expr\Array_
2018-01-14 00:33:32 +01:00
&& isset($assign_var_item->value->inferredType)
2016-11-01 19:32:19 +01:00
) {
self::analyze(
2016-11-02 07:29:00 +01:00
$statements_checker,
$var,
2018-01-14 00:33:32 +01:00
$assign_var_item->value,
null,
2016-11-02 07:29:00 +01:00
$context,
$doc_comment
);
continue;
}
if (isset($assign_value_type->getTypes()['array'])
&& ($array_atomic_type = $assign_value_type->getTypes()['array'])
&& $array_atomic_type instanceof Type\Atomic\ObjectLike
&& !$assign_var_item->key
&& isset($array_atomic_type->properties[$offset]) // if object-like has int offsets
) {
self::analyze(
$statements_checker,
$var,
null,
$array_atomic_type->properties[(string)$offset],
$context,
$doc_comment
);
2016-11-01 19:32:19 +01:00
continue;
}
if ($var instanceof PhpParser\Node\Expr\List_
|| $var instanceof PhpParser\Node\Expr\Array_
) {
self::analyze(
$statements_checker,
$var,
null,
Type::getMixed(),
$context,
$doc_comment
);
}
2016-11-01 16:37:58 +01:00
$list_var_id = ExpressionChecker::getVarId(
$var,
2016-11-08 01:16:51 +01:00
$statements_checker->getFQCLN(),
2017-01-07 20:35:07 +01:00
$statements_checker
2016-11-01 16:37:58 +01:00
);
if ($list_var_id) {
$context->vars_possibly_in_scope[$list_var_id] = true;
if (strpos($list_var_id, '-') === false && strpos($list_var_id, '[') === false) {
$location = new CodeLocation($statements_checker, $assign_var);
if ($context->collect_references) {
$context->unreferenced_vars[$list_var_id] = $location;
}
if (!$statements_checker->hasVariable($list_var_id)) {
$statements_checker->registerVariable(
$list_var_id,
$location,
$context->branch_point
);
} else {
$statements_checker->registerVariableAssignment(
$list_var_id,
$location
);
}
if (isset($context->byref_constraints[$list_var_id])) {
$statements_checker->registerVariableUse($location);
}
}
$new_assign_type = null;
if (isset($assign_value_type->getTypes()['array'])) {
$array_atomic_type = $assign_value_type->getTypes()['array'];
if ($array_atomic_type instanceof Type\Atomic\TArray) {
$new_assign_type = clone $array_atomic_type->type_params[1];
} elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
if ($assign_var_item->key
&& ($assign_var_item->key instanceof PhpParser\Node\Scalar\String_
|| $assign_var_item->key instanceof PhpParser\Node\Scalar\LNumber)
&& isset($array_atomic_type->properties[$assign_var_item->key->value])
) {
$new_assign_type =
clone $array_atomic_type->properties[$assign_var_item->key->value];
}
}
}
if ($context->hasVariable($list_var_id)) {
// removes dependennt vars from $context
$context->removeDescendents(
$list_var_id,
$context->vars_in_scope[$list_var_id],
$new_assign_type,
$statements_checker
);
}
$context->vars_in_scope[$list_var_id] = $new_assign_type ?: Type::getMixed();
2016-11-01 16:37:58 +01:00
}
}
2016-11-02 07:29:00 +01:00
} elseif ($assign_var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
2018-01-14 18:09:40 +01:00
if (ArrayAssignmentChecker::analyze(
$statements_checker,
$assign_var,
$context,
$assign_value_type
) === false
) {
2016-11-01 16:37:58 +01:00
return false;
}
} elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) {
if (is_string($assign_var->name)) {
2018-01-14 18:09:40 +01:00
PropertyAssignmentChecker::analyzeInstance(
$statements_checker,
$assign_var,
$assign_var->name,
$assign_value,
$assign_value_type,
$context
);
} else {
if (ExpressionChecker::analyze($statements_checker, $assign_var->name, $context) === false) {
return false;
}
if (ExpressionChecker::analyze($statements_checker, $assign_var->var, $context) === false) {
return false;
}
}
2016-11-01 16:37:58 +01:00
if ($var_id) {
$context->vars_possibly_in_scope[$var_id] = true;
}
2016-11-02 07:29:00 +01:00
} elseif ($assign_var instanceof PhpParser\Node\Expr\StaticPropertyFetch &&
$assign_var->class instanceof PhpParser\Node\Name &&
is_string($assign_var->name)
2016-11-01 16:37:58 +01:00
) {
if (ExpressionChecker::analyze($statements_checker, $assign_var, $context) === false) {
2016-11-01 16:37:58 +01:00
return false;
}
if ($context->check_classes) {
2018-01-14 18:09:40 +01:00
PropertyAssignmentChecker::analyzeStatic(
$statements_checker,
$assign_var,
$assign_value,
$assign_value_type,
$context
);
}
2016-11-01 16:37:58 +01:00
if ($var_id) {
$context->vars_possibly_in_scope[$var_id] = true;
}
2016-11-01 16:37:58 +01:00
}
if ($var_id && isset($context->vars_in_scope[$var_id]) && $context->vars_in_scope[$var_id]->isVoid()) {
2016-11-01 16:37:58 +01:00
if (IssueBuffer::accepts(
new AssignmentToVoid(
2016-11-02 07:29:00 +01:00
'Cannot assign ' . $var_id . ' to type void',
new CodeLocation($statements_checker->getSource(), $assign_var)
2016-11-02 07:29:00 +01:00
),
2016-11-01 16:37:58 +01:00
$statements_checker->getSuppressedIssues()
)) {
return false;
}
$context->vars_in_scope[$var_id] = Type::getMixed();
return Type::getMixed();
2016-11-01 16:37:58 +01:00
}
return $assign_value_type;
2016-11-01 16:37:58 +01:00
}
/**
2016-11-02 07:29:00 +01:00
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr\AssignOp $stmt
* @param Context $context
2017-05-27 02:16:18 +02:00
*
2016-11-02 07:29:00 +01:00
* @return false|null
2016-11-01 16:37:58 +01:00
*/
public static function analyzeAssignmentOperation(
2016-11-01 16:37:58 +01:00
StatementsChecker $statements_checker,
PhpParser\Node\Expr\AssignOp $stmt,
Context $context
) {
if (ExpressionChecker::analyze($statements_checker, $stmt->var, $context) === false) {
2016-11-01 16:37:58 +01:00
return false;
}
if (ExpressionChecker::analyze($statements_checker, $stmt->expr, $context) === false) {
2016-11-13 21:39:16 +01:00
return false;
}
$array_var_id = ExpressionChecker::getArrayVarId(
2016-11-13 21:39:16 +01:00
$stmt->var,
$statements_checker->getFQCLN(),
2017-01-07 20:35:07 +01:00
$statements_checker
2016-11-13 21:39:16 +01:00
);
$var_type = isset($stmt->var->inferredType) ? clone $stmt->var->inferredType : null;
$expr_type = isset($stmt->expr->inferredType) ? $stmt->expr->inferredType : null;
if ($stmt instanceof PhpParser\Node\Expr\AssignOp\Plus ||
$stmt instanceof PhpParser\Node\Expr\AssignOp\Minus ||
$stmt instanceof PhpParser\Node\Expr\AssignOp\Mod ||
$stmt instanceof PhpParser\Node\Expr\AssignOp\Mul ||
$stmt instanceof PhpParser\Node\Expr\AssignOp\Pow
) {
2018-01-14 18:09:40 +01:00
BinaryOpChecker::analyzeNonDivArithmenticOp(
$statements_checker,
$stmt->var,
$stmt->expr,
$stmt,
2017-09-07 03:44:26 +02:00
$result_type,
$context
);
2016-11-13 21:39:16 +01:00
if ($result_type && $array_var_id) {
$context->vars_in_scope[$array_var_id] = $result_type;
2016-11-13 21:39:16 +01:00
}
} elseif ($stmt instanceof PhpParser\Node\Expr\AssignOp\Div
&& $var_type
&& $expr_type
&& $var_type->hasNumericType()
&& $expr_type->hasNumericType()
&& $array_var_id
) {
$context->vars_in_scope[$array_var_id] = Type::combineUnionTypes(Type::getFloat(), Type::getInt());
} elseif ($stmt instanceof PhpParser\Node\Expr\AssignOp\Concat) {
2018-01-14 18:09:40 +01:00
BinaryOpChecker::analyzeConcatOp(
$statements_checker,
$stmt->var,
$stmt->expr,
$context,
$result_type
);
if ($result_type && $array_var_id) {
$context->vars_in_scope[$array_var_id] = $result_type;
}
2016-11-13 21:39:16 +01:00
}
return null;
2016-11-01 16:37:58 +01:00
}
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr\AssignRef $stmt
* @param Context $context
2017-05-27 02:16:18 +02:00
*
* @return false|null
*/
public static function analyzeAssignmentRef(
StatementsChecker $statements_checker,
PhpParser\Node\Expr\AssignRef $stmt,
Context $context
) {
if (self::analyze(
$statements_checker,
$stmt->var,
$stmt->expr,
null,
$context,
2017-11-09 05:27:51 +01:00
(string)$stmt->getDocComment()
) === false) {
return false;
}
}
2016-11-01 16:37:58 +01:00
}