1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 02:47:02 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php

501 lines
18 KiB
PHP

<?php
namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment;
use PhpParser;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Context;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TNonEmptyArray;
/**
* @internal
*/
class ArrayAssignmentAnalyzer
{
/**
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
* @param Context $context
* @param PhpParser\Node\Expr|null $assign_value
* @param Type\Union $assignment_value_type
*
* @return void
* @psalm-suppress MixedMethodCall - some funky logic here
*/
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\ArrayDimFetch $stmt,
Context $context,
$assign_value,
Type\Union $assignment_value_type
) {
$nesting = 0;
$var_id = ExpressionAnalyzer::getVarId(
$stmt->var,
$statements_analyzer->getFQCLN(),
$statements_analyzer,
$nesting
);
self::updateArrayType(
$statements_analyzer,
$stmt,
$assign_value,
$assignment_value_type,
$context
);
if (!isset($stmt->var->inferredType) && $var_id) {
$context->vars_in_scope[$var_id] = Type::getMixed();
}
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
* @param Type\Union $assignment_type
* @param PhpParser\Node\Expr|null $assign_value
* @param Context $context
*
* @return false|null
*/
public static function updateArrayType(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\ArrayDimFetch $stmt,
$assign_value,
Type\Union $assignment_type,
Context $context
) {
$root_array_expr = $stmt;
$child_stmts = [];
while ($root_array_expr->var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
$child_stmts[] = $root_array_expr;
$root_array_expr = $root_array_expr->var;
}
$child_stmts[] = $root_array_expr;
$root_array_expr = $root_array_expr->var;
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$root_array_expr,
$context,
true
) === false) {
// fall through
}
$codebase = $statements_analyzer->getCodebase();
$root_type = isset($root_array_expr->inferredType) ? $root_array_expr->inferredType : Type::getMixed();
if ($root_type->hasMixed()) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$stmt->var,
$context,
true
) === false) {
// fall through
}
if ($stmt->dim) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$stmt->dim,
$context
) === false) {
// fall through
}
}
return null;
}
$child_stmts = array_reverse($child_stmts);
$current_type = $root_type;
$current_dim = $stmt->dim;
$reversed_child_stmts = [];
// gets a variable id that *may* contain array keys
$root_var_id = ExpressionAnalyzer::getRootVarId(
$root_array_expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
$var_id_additions = [];
$parent_var_id = null;
$full_var_id = true;
$child_stmt = null;
// First go from the root element up, and go as far as we can to figure out what
// array types there are
while ($child_stmts) {
$child_stmt = array_shift($child_stmts);
if (count($child_stmts)) {
array_unshift($reversed_child_stmts, $child_stmt);
}
if ($child_stmt->dim) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$child_stmt->dim,
$context
) === false) {
return false;
}
if (!isset($child_stmt->dim->inferredType)) {
return null;
}
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\String_
|| ($child_stmt->dim instanceof PhpParser\Node\Expr\ConstFetch
&& $child_stmt->dim->inferredType->isSingleStringLiteral())
) {
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\String_) {
$value = $child_stmt->dim->value;
} else {
$value = $child_stmt->dim->inferredType->getSingleStringLiteral()->value;
}
if (preg_match('/^(0|[1-9][0-9]*)$/', $value)) {
$var_id_additions[] = '[' . $value . ']';
}
$var_id_additions[] = '[\'' . $value . '\']';
} elseif ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber
|| ($child_stmt->dim instanceof PhpParser\Node\Expr\ConstFetch
&& $child_stmt->dim->inferredType->isSingleIntLiteral())
) {
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber) {
$value = $child_stmt->dim->value;
} else {
$value = $child_stmt->dim->inferredType->getSingleIntLiteral()->value;
}
$var_id_additions[] = '[' . $value . ']';
} elseif ($child_stmt->dim instanceof PhpParser\Node\Expr\Variable
&& is_string($child_stmt->dim->name)
) {
$var_id_additions[] = '[$' . $child_stmt->dim->name . ']';
} else {
$var_id_additions[] = '[' . $child_stmt->dim->inferredType . ']';
$full_var_id = false;
}
} else {
$var_id_additions[] = '';
$full_var_id = false;
}
if (!isset($child_stmt->var->inferredType)) {
return null;
}
if ($child_stmt->var->inferredType->isEmpty()) {
$child_stmt->var->inferredType = Type::getEmptyArray();
}
$array_var_id = $root_var_id . implode('', $var_id_additions);
if ($parent_var_id && isset($context->vars_in_scope[$parent_var_id])) {
$child_stmt->var->inferredType = clone $context->vars_in_scope[$parent_var_id];
}
$parent_var_id = $array_var_id;
$child_stmt->inferredType = ArrayFetchAnalyzer::getArrayAccessTypeGivenOffset(
$statements_analyzer,
$child_stmt,
$child_stmt->var->inferredType,
isset($child_stmt->dim->inferredType) ? $child_stmt->dim->inferredType : Type::getInt(),
true,
$array_var_id,
$context,
$assign_value,
$child_stmts ? null : $assignment_type
);
if (!$child_stmts) {
$child_stmt->inferredType = $assignment_type;
}
$current_type = $child_stmt->inferredType;
$current_dim = $child_stmt->dim;
if ($child_stmt->var->inferredType->hasMixed()) {
$full_var_id = false;
break;
}
}
if ($root_var_id
&& $full_var_id
&& isset($child_stmt->var->inferredType)
&& !$child_stmt->var->inferredType->hasObjectType()
) {
$array_var_id = $root_var_id . implode('', $var_id_additions);
$context->vars_in_scope[$array_var_id] = clone $assignment_type;
}
// only update as many child stmts are we were able to process above
foreach ($reversed_child_stmts as $child_stmt) {
if (!isset($child_stmt->inferredType)) {
throw new \InvalidArgumentException('Should never get here');
}
$key_value = null;
if ($current_dim instanceof PhpParser\Node\Scalar\String_
|| $current_dim instanceof PhpParser\Node\Scalar\LNumber
) {
$key_value = $current_dim->value;
} elseif ($current_dim instanceof PhpParser\Node\Expr\ConstFetch
&& isset($current_dim->inferredType)
) {
$is_single_string_literal = $current_dim->inferredType->isSingleStringLiteral();
if ($is_single_string_literal || $current_dim->inferredType->isSingleIntLiteral()) {
if ($is_single_string_literal) {
$key_value = $current_dim->inferredType->getSingleStringLiteral()->value;
} else {
$key_value = $current_dim->inferredType->getSingleIntLiteral()->value;
}
}
}
if ($key_value !== null) {
$has_matching_objectlike_property = false;
foreach ($child_stmt->inferredType->getTypes() as $type) {
if ($type instanceof ObjectLike) {
if (isset($type->properties[$key_value])) {
$has_matching_objectlike_property = true;
$type->properties[$key_value] = clone $current_type;
}
}
}
if (!$has_matching_objectlike_property) {
$array_assignment_type = new Type\Union([
new ObjectLike([$key_value => $current_type]),
]);
$new_child_type = Type::combineUnionTypes(
$child_stmt->inferredType,
$array_assignment_type,
$codebase,
true,
false
);
} else {
$new_child_type = $child_stmt->inferredType; // noop
}
} else {
$array_assignment_type = new Type\Union([
new TArray([
isset($current_dim->inferredType) ? $current_dim->inferredType : Type::getInt(),
$current_type,
]),
]);
$new_child_type = Type::combineUnionTypes(
$child_stmt->inferredType,
$array_assignment_type,
$codebase,
true,
false
);
}
$new_child_type->removeType('null');
$new_child_type->possibly_undefined = false;
if (!$child_stmt->inferredType->hasObjectType()) {
$child_stmt->inferredType = $new_child_type;
}
$current_type = $child_stmt->inferredType;
$current_dim = $child_stmt->dim;
array_pop($var_id_additions);
if ($root_var_id) {
$array_var_id = $root_var_id . implode('', $var_id_additions);
$context->vars_in_scope[$array_var_id] = clone $child_stmt->inferredType;
}
}
$root_is_string = $root_type->isString();
$key_value = null;
if ($current_dim instanceof PhpParser\Node\Scalar\String_
|| ($current_dim instanceof PhpParser\Node\Scalar\LNumber && !$root_is_string)
) {
$key_value = $current_dim->value;
} elseif ($current_dim instanceof PhpParser\Node\Expr\ConstFetch
&& isset($current_dim->inferredType)
&& !$root_is_string
) {
$is_single_string_literal = $current_dim->inferredType->isSingleStringLiteral();
if ($is_single_string_literal || $current_dim->inferredType->isSingleIntLiteral()) {
if ($is_single_string_literal) {
$key_value = $current_dim->inferredType->getSingleStringLiteral()->value;
} else {
$key_value = $current_dim->inferredType->getSingleIntLiteral()->value;
}
}
}
if ($key_value !== null) {
$has_matching_objectlike_property = false;
foreach ($root_type->getTypes() as $type) {
if ($type instanceof ObjectLike) {
if (isset($type->properties[$key_value])) {
$has_matching_objectlike_property = true;
$type->properties[$key_value] = clone $current_type;
}
}
}
if (!$has_matching_objectlike_property) {
$object_like = new ObjectLike([$key_value => $current_type]);
$object_like->sealed = true;
$array_assignment_type = new Type\Union([
$object_like,
]);
$new_child_type = Type::combineUnionTypes(
$root_type,
$array_assignment_type,
$codebase,
true,
false
);
} else {
$new_child_type = $root_type; // noop
}
} elseif (!$root_is_string) {
if ($current_dim) {
if (isset($current_dim->inferredType)) {
$array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts(
$current_dim->inferredType
);
} else {
$array_atomic_key_type = Type::getMixed();
}
} else {
// todo: this can be improved I think
$array_atomic_key_type = Type::getInt();
}
$array_atomic_type = new TNonEmptyArray([
$array_atomic_key_type,
$current_type,
]);
$from_countable_object_like = false;
if (!$current_dim && !$context->inside_loop) {
$atomic_root_types = $root_type->getTypes();
if (isset($atomic_root_types['array'])) {
if ($atomic_root_types['array'] instanceof TNonEmptyArray) {
$array_atomic_type->count = $atomic_root_types['array']->count;
} elseif ($atomic_root_types['array'] instanceof ObjectLike
&& $atomic_root_types['array']->sealed
) {
$array_atomic_type->count = count($atomic_root_types['array']->properties);
$from_countable_object_like = true;
} else {
$array_atomic_type = new TNonEmptyArray(
$array_atomic_type->type_params
);
}
}
}
$array_assignment_type = new Type\Union([
$array_atomic_type,
]);
$new_child_type = Type::combineUnionTypes(
$root_type,
$array_assignment_type,
$codebase,
true,
false
);
if ($from_countable_object_like) {
$atomic_root_types = $new_child_type->getTypes();
if (isset($atomic_root_types['array'])
&& $atomic_root_types['array'] instanceof TNonEmptyArray
&& $atomic_root_types['array']->count !== null
) {
$atomic_root_types['array']->count++;
}
}
} else {
$new_child_type = $root_type;
}
$new_child_type->removeType('null');
if (!$root_type->hasObjectType()) {
$root_type = $new_child_type;
}
$root_array_expr->inferredType = $root_type;
if ($root_array_expr instanceof PhpParser\Node\Expr\PropertyFetch) {
if ($root_array_expr->name instanceof PhpParser\Node\Identifier) {
PropertyAssignmentAnalyzer::analyzeInstance(
$statements_analyzer,
$root_array_expr,
$root_array_expr->name->name,
null,
$root_type,
$context,
false
);
} else {
if (ExpressionAnalyzer::analyze($statements_analyzer, $root_array_expr->name, $context) === false) {
return false;
}
if (ExpressionAnalyzer::analyze($statements_analyzer, $root_array_expr->var, $context) === false) {
return false;
}
}
} elseif ($root_var_id) {
if ($context->hasVariable($root_var_id, $statements_analyzer)) {
$context->vars_in_scope[$root_var_id] = $root_type;
} else {
$context->vars_in_scope[$root_var_id] = $root_type;
}
}
return null;
}
}