1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 19:36:59 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php

1082 lines
46 KiB
PHP
Raw Normal View History

2018-01-14 18:09:40 +01:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer\Statements\Expression\Fetch;
2018-01-14 18:09:40 +01:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
2018-01-14 18:09:40 +01:00
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\EmptyArrayAccess;
use Psalm\Issue\InvalidArrayAccess;
use Psalm\Issue\InvalidArrayAssignment;
use Psalm\Issue\InvalidArrayOffset;
use Psalm\Issue\MixedArrayAccess;
use Psalm\Issue\MixedArrayAssignment;
use Psalm\Issue\MixedArrayOffset;
use Psalm\Issue\MixedStringOffsetAssignment;
use Psalm\Issue\MixedArrayTypeCoercion;
2018-01-14 18:09:40 +01:00
use Psalm\Issue\NullArrayAccess;
use Psalm\Issue\NullArrayOffset;
use Psalm\Issue\PossiblyInvalidArrayAccess;
use Psalm\Issue\PossiblyInvalidArrayAssignment;
use Psalm\Issue\PossiblyInvalidArrayOffset;
use Psalm\Issue\PossiblyNullArrayAccess;
use Psalm\Issue\PossiblyNullArrayAssignment;
use Psalm\Issue\PossiblyNullArrayOffset;
use Psalm\Issue\PossiblyUndefinedArrayOffset;
2018-01-14 18:09:40 +01:00
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
2018-01-14 18:09:40 +01:00
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
2019-02-22 03:40:06 +01:00
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TInt;
2018-01-14 18:09:40 +01:00
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
2018-11-09 16:56:27 +01:00
use Psalm\Type\Atomic\TNonEmptyArray;
2018-01-14 18:09:40 +01:00
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TSingleLetter;
2018-01-14 18:09:40 +01:00
use Psalm\Type\Atomic\TString;
use function array_values;
use function array_keys;
use function count;
use function array_pop;
use function implode;
use function strlen;
use function strtolower;
use function in_array;
use function is_int;
use function preg_match;
use Psalm\Internal\Taint\TypeSource;
2018-01-14 18:09:40 +01:00
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class ArrayFetchAnalyzer
2018-01-14 18:09:40 +01:00
{
/**
2018-11-11 18:01:14 +01:00
* @param StatementsAnalyzer $statements_analyzer
2018-01-14 18:09:40 +01:00
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
* @param Context $context
*
* @return false|null
*/
public static function analyze(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2018-01-14 18:09:40 +01:00
PhpParser\Node\Expr\ArrayDimFetch $stmt,
Context $context
) {
2018-11-06 03:57:36 +01:00
$array_var_id = ExpressionAnalyzer::getArrayVarId(
2018-01-14 18:09:40 +01:00
$stmt->var,
2018-11-11 18:01:14 +01:00
$statements_analyzer->getFQCLN(),
$statements_analyzer
2018-01-14 18:09:40 +01:00
);
2018-11-06 03:57:36 +01:00
$keyed_array_var_id = ExpressionAnalyzer::getArrayVarId(
2018-01-14 18:09:40 +01:00
$stmt,
2018-11-11 18:01:14 +01:00
$statements_analyzer->getFQCLN(),
$statements_analyzer
2018-01-14 18:09:40 +01:00
);
2018-11-11 18:01:14 +01:00
if ($stmt->dim && ExpressionAnalyzer::analyze($statements_analyzer, $stmt->dim, $context) === false) {
2018-01-14 18:09:40 +01:00
return false;
}
$dim_var_id = null;
$new_offset_type = null;
2018-01-14 18:09:40 +01:00
if ($stmt->dim) {
if (isset($stmt->dim->inferredType)) {
$used_key_type = $stmt->dim->inferredType;
} else {
$used_key_type = Type::getMixed();
}
2018-11-06 03:57:36 +01:00
$dim_var_id = ExpressionAnalyzer::getArrayVarId(
$stmt->dim,
2018-11-11 18:01:14 +01:00
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
2018-01-14 18:09:40 +01:00
} else {
$used_key_type = Type::getInt();
}
2018-11-06 03:57:36 +01:00
if (ExpressionAnalyzer::analyze(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
2018-01-14 18:09:40 +01:00
$stmt->var,
$context
) === false) {
return false;
}
if ($keyed_array_var_id
&& $context->hasVariable($keyed_array_var_id)
&& !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined
&& !$context->vars_in_scope[$keyed_array_var_id]->isVanillaMixed()
) {
$stmt->inferredType = clone $context->vars_in_scope[$keyed_array_var_id];
return;
}
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-11-06 03:57:36 +01:00
2018-01-14 18:09:40 +01:00
if (isset($stmt->var->inferredType)) {
$var_type = $stmt->var->inferredType;
if ($var_type->isNull()) {
if (!$context->inside_isset) {
if (IssueBuffer::accepts(
new NullArrayAccess(
'Cannot access array value on null variable ' . $array_var_id,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
}
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, Type::getNull());
} else {
$stmt->inferredType = Type::getNull();
}
return;
}
$stmt->inferredType = self::getArrayAccessTypeGivenOffset(
2018-11-11 18:01:14 +01:00
$statements_analyzer,
2018-01-14 18:09:40 +01:00
$stmt,
$stmt->var->inferredType,
$used_key_type,
false,
$array_var_id,
$context,
null
2018-01-14 18:09:40 +01:00
);
if ($array_var_id === '$_GET' || $array_var_id === '$_POST' || $array_var_id === '$_COOKIE') {
$stmt->inferredType->tainted = (int) Type\Union::TAINTED_INPUT;
$stmt->inferredType->sources = [
new TypeSource(
'$_GET',
new CodeLocation($statements_analyzer->getSource(), $stmt),
(int) Type\Union::TAINTED_INPUT
)
];
}
if ($context->inside_isset
&& $stmt->dim
&& isset($stmt->dim->inferredType)
2018-12-20 07:06:43 +01:00
&& $stmt->var->inferredType->hasArray()
&& ($stmt->var instanceof PhpParser\Node\Expr\ClassConstFetch
|| $stmt->var instanceof PhpParser\Node\Expr\ConstFetch)
) {
/** @var TArray|ObjectLike */
$array_type = $stmt->var->inferredType->getTypes()['array'];
if ($array_type instanceof TArray) {
$const_array_key_type = $array_type->type_params[0];
} else {
$const_array_key_type = $array_type->getGenericKeyType();
}
if ($dim_var_id
&& !$const_array_key_type->hasMixed()
&& !$stmt->dim->inferredType->hasMixed()
) {
$new_offset_type = clone $stmt->dim->inferredType;
$const_array_key_atomic_types = $const_array_key_type->getTypes();
foreach ($new_offset_type->getTypes() as $offset_key => $offset_atomic_type) {
if ($offset_atomic_type instanceof TString
|| $offset_atomic_type instanceof TInt
) {
if (!isset($const_array_key_atomic_types[$offset_key])
2018-11-06 03:57:36 +01:00
&& !TypeAnalyzer::isContainedBy(
$codebase,
new Type\Union([$offset_atomic_type]),
$const_array_key_type
)
) {
$new_offset_type->removeType($offset_key);
}
2018-11-06 03:57:36 +01:00
} elseif (!TypeAnalyzer::isContainedBy(
$codebase,
$const_array_key_type,
new Type\Union([$offset_atomic_type])
)) {
$new_offset_type->removeType($offset_key);
}
}
}
}
2018-01-14 18:09:40 +01:00
}
if ($keyed_array_var_id
&& $context->hasVariable($keyed_array_var_id, $statements_analyzer)
&& (!isset($stmt->inferredType) || $stmt->inferredType->isVanillaMixed())
) {
2018-01-14 18:09:40 +01:00
$stmt->inferredType = $context->vars_in_scope[$keyed_array_var_id];
}
if (!isset($stmt->inferredType)) {
$stmt->inferredType = Type::getMixed();
} else {
if ($stmt->inferredType->possibly_undefined && !$context->inside_isset && !$context->inside_unset) {
if (IssueBuffer::accepts(
new PossiblyUndefinedArrayOffset(
'Possibly undefined array key ' . $keyed_array_var_id,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
$stmt->inferredType->possibly_undefined = false;
2018-01-14 18:09:40 +01:00
}
if ($context->inside_isset && $dim_var_id && $new_offset_type && $new_offset_type->getTypes()) {
$context->vars_in_scope[$dim_var_id] = $new_offset_type;
}
if ($keyed_array_var_id && !$context->inside_isset) {
$context->vars_in_scope[$keyed_array_var_id] = $stmt->inferredType;
$context->vars_possibly_in_scope[$keyed_array_var_id] = true;
// reference the variable too
2018-11-11 18:01:14 +01:00
$context->hasVariable($keyed_array_var_id, $statements_analyzer);
}
2018-01-14 18:09:40 +01:00
return null;
}
/**
* @param Type\Union $array_type
* @param Type\Union $offset_type
* @param bool $in_assignment
* @param null|string $array_var_id
2018-01-14 18:09:40 +01:00
*
* @return Type\Union
*/
public static function getArrayAccessTypeGivenOffset(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2018-01-14 18:09:40 +01:00
PhpParser\Node\Expr\ArrayDimFetch $stmt,
Type\Union $array_type,
Type\Union $offset_type,
$in_assignment,
$array_var_id,
Context $context,
PhpParser\Node\Expr $assign_value = null,
Type\Union $replacement_type = null
2018-01-14 18:09:40 +01:00
) {
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-01-14 18:09:40 +01:00
$has_array_access = false;
$non_array_types = [];
$has_valid_offset = false;
$expected_offset_types = [];
2018-01-14 18:09:40 +01:00
$key_value = null;
if ($stmt->dim instanceof PhpParser\Node\Scalar\String_
|| $stmt->dim instanceof PhpParser\Node\Scalar\LNumber
) {
$key_value = $stmt->dim->value;
} elseif (isset($stmt->dim->inferredType)) {
foreach ($stmt->dim->inferredType->getTypes() as $possible_value_type) {
if ($possible_value_type instanceof TLiteralString
|| $possible_value_type instanceof TLiteralInt
) {
if ($key_value !== null) {
$key_value = null;
break;
}
$key_value = $possible_value_type->value;
} elseif ($possible_value_type instanceof TString
|| $possible_value_type instanceof TInt
) {
$key_value = null;
break;
}
}
2018-01-14 18:09:40 +01:00
}
$array_access_type = null;
if ($offset_type->isNull()) {
if (IssueBuffer::accepts(
new NullArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' using null offset',
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
return Type::getMixed();
}
if ($offset_type->isNullable() && !$offset_type->ignore_nullable_issues && !$context->inside_isset) {
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new PossiblyNullArrayOffset(
'Cannot access value on variable ' . $array_var_id
. ' using possibly null offset ' . $offset_type,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt->var)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
}
foreach ($array_type->getTypes() as $type_string => $type) {
$original_type = $type;
if ($type instanceof TMixed || $type instanceof TTemplateParam || $type instanceof TEmpty) {
if (!$type instanceof TTemplateParam || $type->as->isMixed() || !$type->as->isSingle()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
if (!$context->inside_isset) {
if ($in_assignment) {
if (IssueBuffer::accepts(
new MixedArrayAssignment(
'Cannot access array value on mixed variable ' . $array_var_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new MixedArrayAccess(
'Cannot access array value on mixed variable ' . $array_var_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
$has_valid_offset = true;
$array_access_type = Type::getMixed();
break;
}
$type = clone array_values($type->as->getTypes())[0];
}
2018-01-14 18:09:40 +01:00
if ($type instanceof TNull) {
if ($array_type->ignore_nullable_issues) {
continue;
}
if ($in_assignment) {
if ($replacement_type) {
if ($array_access_type) {
$array_access_type = Type::combineUnionTypes($array_access_type, $replacement_type);
} else {
$array_access_type = clone $replacement_type;
}
} else {
if (IssueBuffer::accepts(
new PossiblyNullArrayAssignment(
'Cannot access array value on possibly null variable ' . $array_var_id .
' of type ' . $array_type,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
$array_access_type = new Type\Union([new TEmpty]);
}
} else {
if (!$context->inside_isset) {
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new PossiblyNullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id .
' of type ' . $array_type,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
}
if ($array_access_type) {
$array_access_type = Type::combineUnionTypes($array_access_type, Type::getNull());
} else {
$array_access_type = Type::getNull();
}
}
continue;
}
if ($type instanceof TArray || $type instanceof ObjectLike) {
$has_array_access = true;
if ($in_assignment
&& $type instanceof TArray
&& $type->type_params[0]->isEmpty()
&& $key_value !== null
) {
// ok, type becomes an ObjectLike
$array_type->removeType($type_string);
2018-01-14 18:09:40 +01:00
$type = new ObjectLike([$key_value => new Type\Union([new TEmpty])]);
$array_type->addType($type);
2018-01-14 18:09:40 +01:00
}
$offset_type = self::replaceOffsetTypeWithInts($offset_type);
2018-01-14 18:09:40 +01:00
if ($type instanceof TArray) {
// if we're assigning to an empty array with a key offset, refashion that array
if ($in_assignment) {
if ($type->type_params[0]->isEmpty()) {
$type->type_params[0] = $offset_type;
}
} elseif (!$type->type_params[0]->isEmpty()) {
$expected_offset_type = $type->type_params[0]->hasMixed()
? new Type\Union([ new TArrayKey ])
: $type->type_params[0];
2019-06-08 03:27:50 +02:00
$templated_offset_type = null;
foreach ($offset_type->getTypes() as $offset_atomic_type) {
if ($offset_atomic_type instanceof TTemplateParam) {
$templated_offset_type = $offset_atomic_type;
}
}
2019-07-10 07:35:57 +02:00
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
2019-06-08 03:27:50 +02:00
if ($original_type instanceof TTemplateParam && $templated_offset_type) {
foreach ($templated_offset_type->as->getTypes() as $offset_as) {
if ($offset_as instanceof Type\Atomic\TTemplateKeyOf
&& $offset_as->param_name === $original_type->param_name
&& $offset_as->defining_class === $original_type->defining_class
) {
$type->type_params[1] = new Type\Union([
new Type\Atomic\TTemplateIndexedAccess(
$offset_as->param_name,
$templated_offset_type->param_name,
$offset_as->defining_class
)
]);
$has_valid_offset = true;
}
}
} elseif ((!TypeAnalyzer::isContainedBy(
2018-11-06 03:57:36 +01:00
$codebase,
2018-01-14 18:09:40 +01:00
$offset_type,
$expected_offset_type,
true,
$offset_type->ignore_falsable_issues,
2019-07-10 07:35:57 +02:00
$union_comparison_results
) && !$union_comparison_results->type_coerced_from_scalar)
|| $union_comparison_results->to_string_cast
) {
2019-07-10 07:35:57 +02:00
if ($union_comparison_results->type_coerced_from_mixed
&& !$offset_type->isMixed()
) {
if (IssueBuffer::accepts(
new MixedArrayTypeCoercion(
'Coercion from array offset type \'' . $offset_type->getId() . '\' '
. 'to the expected type \'' . $expected_offset_type->getId() . '\'',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
$expected_offset_types[] = $expected_offset_type->getId();
}
if (TypeAnalyzer::canExpressionTypesBeIdentical(
$codebase,
$offset_type,
$expected_offset_type
)) {
$has_valid_offset = true;
}
2018-01-14 18:09:40 +01:00
} else {
$has_valid_offset = true;
}
}
2018-11-09 16:56:27 +01:00
if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) {
$type->count++;
}
2018-01-14 18:09:40 +01:00
if ($in_assignment && $replacement_type) {
$type->type_params[1] = Type::combineUnionTypes(
$type->type_params[1],
$replacement_type,
$codebase
2018-01-14 18:09:40 +01:00
);
}
if (!$array_access_type) {
$array_access_type = $type->type_params[1];
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$type->type_params[1]
);
}
if ($array_access_type->isEmpty()
&& !$array_type->hasMixed()
&& !$in_assignment
&& !$context->inside_isset
) {
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new EmptyArrayAccess(
'Cannot access value on empty array variable ' . $array_var_id,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
return Type::getMixed(true);
2018-01-14 18:09:40 +01:00
}
if (!IssueBuffer::isRecording()) {
$array_access_type = Type::getMixed(true);
2018-01-14 18:09:40 +01:00
}
}
2018-02-07 19:57:45 +01:00
} else {
$generic_key_type = $type->getGenericKeyType();
2018-01-14 18:09:40 +01:00
if ($key_value !== null) {
if (isset($type->properties[$key_value]) || $replacement_type) {
$has_valid_offset = true;
if ($replacement_type) {
if (isset($type->properties[$key_value])) {
$type->properties[$key_value] = Type::combineUnionTypes(
$type->properties[$key_value],
$replacement_type
);
} else {
$type->properties[$key_value] = $replacement_type;
}
}
if (!$array_access_type) {
$array_access_type = clone $type->properties[$key_value];
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$type->properties[$key_value]
);
}
} elseif ($in_assignment) {
$type->properties[$key_value] = new Type\Union([new TEmpty]);
if (!$array_access_type) {
$array_access_type = clone $type->properties[$key_value];
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$type->properties[$key_value]
);
}
} elseif ($type->had_mixed_value) {
$has_valid_offset = true;
$type->properties[$key_value] = new Type\Union([new TMixed]);
$array_access_type = Type::getMixed();
2018-01-14 18:09:40 +01:00
} else {
if (!$context->inside_isset || $type->sealed) {
$object_like_keys = array_keys($type->properties);
2018-01-14 18:09:40 +01:00
if (count($object_like_keys) === 1) {
$expected_keys_string = '\'' . $object_like_keys[0] . '\'';
} else {
$last_key = array_pop($object_like_keys);
$expected_keys_string = '\'' . implode('\', \'', $object_like_keys) .
'\' or \'' . $last_key . '\'';
}
2018-01-14 18:09:40 +01:00
$expected_offset_types[] = $expected_keys_string;
}
2018-01-14 18:09:40 +01:00
$array_access_type = Type::getMixed();
}
2018-11-02 04:31:40 +01:00
} else {
$key_type = $generic_key_type->hasMixed()
? Type::getArrayKey()
2018-11-02 04:31:40 +01:00
: $generic_key_type;
2018-01-14 18:09:40 +01:00
2019-07-10 07:35:57 +02:00
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
2018-11-06 03:57:36 +01:00
$is_contained = TypeAnalyzer::isContainedBy(
2018-11-02 04:31:40 +01:00
$codebase,
$offset_type,
$key_type,
true,
$offset_type->ignore_falsable_issues,
2019-07-10 07:35:57 +02:00
$union_comparison_results
2018-11-02 04:31:40 +01:00
);
if ($context->inside_isset && !$is_contained) {
2018-11-06 03:57:36 +01:00
$is_contained = TypeAnalyzer::canBeContainedBy(
2018-11-02 04:31:40 +01:00
$codebase,
$offset_type,
$key_type,
true,
$offset_type->ignore_falsable_issues
2018-01-14 18:09:40 +01:00
);
2018-11-02 04:31:40 +01:00
}
2018-01-14 18:09:40 +01:00
2018-11-02 04:31:40 +01:00
if (($is_contained
2019-07-10 07:35:57 +02:00
|| $union_comparison_results->type_coerced_from_scalar
|| $union_comparison_results->type_coerced_from_mixed
2018-11-02 04:31:40 +01:00
|| $in_assignment)
2019-07-10 07:35:57 +02:00
&& !$union_comparison_results->to_string_cast
2018-11-02 04:31:40 +01:00
) {
if ($replacement_type) {
$generic_params = Type::combineUnionTypes(
$type->getGenericValueType(),
$replacement_type
);
2018-11-02 04:31:40 +01:00
$new_key_type = Type::combineUnionTypes(
$generic_key_type,
$offset_type
);
2018-01-14 18:09:40 +01:00
2018-11-02 04:31:40 +01:00
$property_count = $type->sealed ? count($type->properties) : null;
2018-11-02 04:31:40 +01:00
if (!$stmt->dim && $property_count) {
++$property_count;
$array_type->removeType($type_string);
2018-11-09 16:56:27 +01:00
$type = new TNonEmptyArray([
$new_key_type,
$generic_params,
]);
$array_type->addType($type);
2018-11-02 04:31:40 +01:00
$type->count = $property_count;
2018-11-09 16:56:27 +01:00
} else {
$array_type->removeType($type_string);
2018-11-09 16:56:27 +01:00
$type = new TArray([
$new_key_type,
$generic_params,
]);
$array_type->addType($type);
2018-11-02 04:31:40 +01:00
}
if (!$array_access_type) {
$array_access_type = clone $generic_params;
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$generic_params
);
}
2018-01-14 18:09:40 +01:00
} else {
2018-11-02 04:31:40 +01:00
if (!$array_access_type) {
$array_access_type = $type->getGenericValueType();
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$type->getGenericValueType()
);
}
2018-01-14 18:09:40 +01:00
}
2018-11-02 04:31:40 +01:00
$has_valid_offset = true;
2018-01-14 18:09:40 +01:00
} else {
if (!$context->inside_isset || $type->sealed) {
2018-11-02 04:31:40 +01:00
$expected_offset_types[] = (string)$generic_key_type->getId();
2018-01-14 18:09:40 +01:00
}
2018-11-02 04:31:40 +01:00
$array_access_type = Type::getMixed();
2018-05-05 23:50:19 +02:00
}
2018-01-14 18:09:40 +01:00
}
}
continue;
}
if ($type instanceof TString) {
if ($in_assignment && $replacement_type) {
if ($replacement_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
if (IssueBuffer::accepts(
new MixedStringOffsetAssignment(
'Right-hand-side of string offset assignment cannot be mixed',
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
2018-01-14 18:09:40 +01:00
}
}
if ($type instanceof TSingleLetter) {
$valid_offset_type = Type::getInt(false, 0);
} elseif ($type instanceof TLiteralString) {
if (!strlen($type->value)) {
$valid_offset_type = Type::getEmpty();
} elseif (strlen($type->value) < 10) {
$valid_offsets = [];
for ($i = -strlen($type->value), $l = strlen($type->value); $i < $l; $i++) {
$valid_offsets[] = new TLiteralInt($i);
}
$valid_offset_type = new Type\Union($valid_offsets);
} else {
$valid_offset_type = Type::getInt();
}
} else {
$valid_offset_type = Type::getInt();
}
2018-11-06 03:57:36 +01:00
if (!TypeAnalyzer::isContainedBy(
$codebase,
2018-01-14 18:09:40 +01:00
$offset_type,
$valid_offset_type,
2018-01-14 18:09:40 +01:00
true
)) {
$expected_offset_types[] = $valid_offset_type->getId();
2018-12-20 07:06:43 +01:00
$array_access_type = Type::getMixed();
2018-01-14 18:09:40 +01:00
} else {
$has_valid_offset = true;
if (!$array_access_type) {
$array_access_type = Type::getSingleLetter();
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
Type::getSingleLetter()
);
}
2018-01-14 18:09:40 +01:00
}
continue;
}
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
2018-04-16 22:03:04 +02:00
if ($type instanceof Type\Atomic\TFalse && $array_type->ignore_falsable_issues) {
continue;
}
if ($type instanceof TNamedObject) {
if (strtolower($type->value) === 'simplexmlelement') {
$array_access_type = Type::getMixed();
} elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) {
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
$stmt->var,
new PhpParser\Node\Identifier('item', $stmt->var->getAttributes()),
[
new PhpParser\Node\Arg($stmt->dim)
]
);
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call,
$context
);
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
}
$iterator_class_type = $fake_method_call->inferredType ?? null;
$array_access_type = $iterator_class_type ?: Type::getMixed();
} else {
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}
if ($in_assignment) {
$fake_set_method_call = new PhpParser\Node\Expr\MethodCall(
$stmt->var,
new PhpParser\Node\Identifier('offsetSet', $stmt->var->getAttributes()),
[
new PhpParser\Node\Arg(
$stmt->dim
? $stmt->dim
: new PhpParser\Node\Expr\ConstFetch(
new PhpParser\Node\Name('null'),
$stmt->var->getAttributes()
)
),
new PhpParser\Node\Arg(
$assign_value
?: new PhpParser\Node\Expr\ConstFetch(
new PhpParser\Node\Name('null'),
$stmt->var->getAttributes()
)
),
]
);
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_set_method_call,
$context
);
}
if ($stmt->dim) {
$fake_get_method_call = new PhpParser\Node\Expr\MethodCall(
$stmt->var,
new PhpParser\Node\Identifier('offsetGet', $stmt->var->getAttributes()),
[
new PhpParser\Node\Arg(
$stmt->dim
)
]
);
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_get_method_call,
$context
);
$iterator_class_type = $fake_get_method_call->inferredType ?? null;
} else {
$iterator_class_type = Type::getVoid();
}
$has_array_access = true;
$array_access_type = $iterator_class_type ?: Type::getMixed();
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
}
2018-01-14 18:09:40 +01:00
}
} elseif (!$array_type->hasMixed()) {
2018-01-14 18:09:40 +01:00
$non_array_types[] = (string)$type;
}
}
if ($non_array_types) {
if ($has_array_access) {
if ($in_assignment) {
if (IssueBuffer::accepts(
new PossiblyInvalidArrayAssignment(
'Cannot access array value on non-array variable ' .
$array_var_id . ' of type ' . $non_array_types[0],
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)
) {
// do nothing
}
} else {
if (IssueBuffer::accepts(
new PossiblyInvalidArrayAccess(
'Cannot access array value on non-array variable ' .
$array_var_id . ' of type ' . $non_array_types[0],
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)
) {
// do nothing
}
}
} else {
if ($in_assignment) {
if (IssueBuffer::accepts(
new InvalidArrayAssignment(
'Cannot access array value on non-array variable ' .
$array_var_id . ' of type ' . $non_array_types[0],
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidArrayAccess(
'Cannot access array value on non-array variable ' .
$array_var_id . ' of type ' . $non_array_types[0],
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
}
$array_access_type = Type::getMixed();
}
}
if ($offset_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new MixedArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' using mixed offset',
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
2018-01-14 18:09:40 +01:00
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
2018-01-14 18:09:40 +01:00
)) {
// fall through
}
} else {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
if ($expected_offset_types) {
$invalid_offset_type = $expected_offset_types[0];
$used_offset = 'using a ' . $offset_type->getId() . ' offset';
if ($key_value !== null) {
$used_offset = 'using offset value of '
. (is_int($key_value) ? $key_value : '\'' . $key_value . '\'');
}
if ($has_valid_offset && $context->inside_isset) {
// do nothing
} elseif ($has_valid_offset) {
if (IssueBuffer::accepts(
new PossiblyInvalidArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
. ', expecting ' . $invalid_offset_type,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
. ', expecting ' . $invalid_offset_type,
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
2018-01-14 18:09:40 +01:00
}
}
}
if ($array_access_type === null) {
2019-05-10 01:34:38 +02:00
// shouldnt happen, but dont crash
return Type::getMixed();
2018-01-14 18:09:40 +01:00
}
if ($in_assignment) {
$array_type->bustCache();
}
2018-01-14 18:09:40 +01:00
return $array_access_type;
}
/**
* @return Type\Union
*/
public static function replaceOffsetTypeWithInts(Type\Union $offset_type)
{
$offset_string_types = $offset_type->getLiteralStrings();
$offset_type = clone $offset_type;
foreach ($offset_string_types as $key => $offset_string_type) {
if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_string_type->value)) {
$offset_type->addType(new Type\Atomic\TLiteralInt((int) $offset_string_type->value));
$offset_type->removeType($key);
}
}
return $offset_type;
}
2018-01-14 18:09:40 +01:00
}