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

655 lines
26 KiB
PHP
Raw Normal View History

2018-01-14 18:09:40 +01:00
<?php
namespace Psalm\Checker\Statements\Expression\Fetch;
use PhpParser;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TypeChecker;
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\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\PossiblyUndefinedGlobalVariable;
use Psalm\Issue\PossiblyUndefinedVariable;
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\TEmpty;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TString;
class ArrayFetchChecker
{
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
* @param Context $context
*
* @return false|null
*/
public static function analyze(
StatementsChecker $statements_checker,
PhpParser\Node\Expr\ArrayDimFetch $stmt,
Context $context
) {
$array_var_id = ExpressionChecker::getArrayVarId(
$stmt->var,
$statements_checker->getFQCLN(),
$statements_checker
);
$keyed_array_var_id = ExpressionChecker::getArrayVarId(
$stmt,
$statements_checker->getFQCLN(),
$statements_checker
);
if ($stmt->dim && ExpressionChecker::analyze($statements_checker, $stmt->dim, $context) === false) {
return false;
}
if ($stmt->dim) {
if (isset($stmt->dim->inferredType)) {
$used_key_type = $stmt->dim->inferredType;
} else {
$used_key_type = Type::getMixed();
}
} else {
$used_key_type = Type::getInt();
}
if (ExpressionChecker::analyze(
$statements_checker,
$stmt->var,
$context
) === false) {
return false;
}
if ($keyed_array_var_id
&& isset($context->vars_in_scope[$keyed_array_var_id])
&& !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined
) {
$stmt->inferredType = clone $context->vars_in_scope[$keyed_array_var_id];
return;
}
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,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, Type::getNull());
} else {
$stmt->inferredType = Type::getNull();
}
return;
}
$stmt->inferredType = self::getArrayAccessTypeGivenOffset(
$statements_checker,
$stmt,
$stmt->var->inferredType,
$used_key_type,
false,
$array_var_id,
null,
$context->inside_isset
);
}
if ($keyed_array_var_id && $context->hasVariable($keyed_array_var_id, $statements_checker)) {
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 ($context->is_global) {
if (IssueBuffer::accepts(
new PossiblyUndefinedGlobalVariable(
'Possibly undefined array key ' . $keyed_array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new PossiblyUndefinedVariable(
'Possibly undefined array key ' . $keyed_array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
}
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
* @param bool $inside_isset
*
* @return Type\Union
*/
public static function getArrayAccessTypeGivenOffset(
StatementsChecker $statements_checker,
PhpParser\Node\Expr\ArrayDimFetch $stmt,
Type\Union $array_type,
Type\Union $offset_type,
$in_assignment,
$array_var_id,
Type\Union $replacement_type = null,
$inside_isset = false
) {
$project_checker = $statements_checker->getFileChecker()->project_checker;
2018-02-01 06:50:01 +01:00
$codebase = $project_checker->codebase;
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;
}
$array_access_type = null;
if ($offset_type->isNull()) {
if (IssueBuffer::accepts(
new NullArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' using null offset',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
if ($offset_type->isNullable() && !$offset_type->ignore_nullable_issues && !$inside_isset) {
if (IssueBuffer::accepts(
new PossiblyNullArrayOffset(
'Cannot access value on variable ' . $array_var_id
. ' using possibly null offset ' . $offset_type,
new CodeLocation($statements_checker->getSource(), $stmt->var)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
foreach ($array_type->getTypes() as &$type) {
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,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
$array_access_type = new Type\Union([new TEmpty]);
}
} else {
if (!$inside_isset) {
if (IssueBuffer::accepts(
new PossiblyNullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id .
' of type ' . $array_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// 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
$type = new ObjectLike([$key_value => new Type\Union([new TEmpty])]);
}
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()) {
if (!TypeChecker::isContainedBy(
2018-02-01 06:50:01 +01:00
$project_checker->codebase,
2018-01-14 18:09:40 +01:00
$offset_type,
$type->type_params[0],
true,
$offset_type->ignore_falsable_issues
2018-01-14 18:09:40 +01:00
)) {
$expected_offset_types[] = (string)$type->type_params[0];
2018-01-14 18:09:40 +01:00
} else {
$has_valid_offset = true;
}
}
if ($in_assignment && $replacement_type) {
$type->type_params[1] = Type::combineUnionTypes(
$type->type_params[1],
$replacement_type
);
}
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() && !$in_assignment) {
if (IssueBuffer::accepts(
new EmptyArrayAccess(
'Cannot access value on empty array variable ' . $array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return Type::getMixed(true);
2018-01-14 18:09:40 +01:00
}
if (!IssueBuffer::isRecording()) {
$array_access_type = Type::getMixed();
}
}
2018-02-07 19:57:45 +01:00
} else {
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]
);
}
} else {
$object_like_keys = array_keys($type->properties);
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 . '\'';
}
$expected_offset_types[] = $expected_keys_string;
2018-01-14 18:09:40 +01:00
$array_access_type = Type::getMixed();
}
} elseif (TypeChecker::isContainedBy(
2018-02-01 06:50:01 +01:00
$codebase,
2018-01-14 18:09:40 +01:00
$offset_type,
$type->getGenericKeyType(),
true,
$offset_type->ignore_falsable_issues
2018-01-14 18:09:40 +01:00
) || $in_assignment
) {
if ($replacement_type) {
$generic_params = Type::combineUnionTypes(
$type->getGenericValueType(),
$replacement_type
);
$new_key_type = Type::combineUnionTypes(
$type->getGenericKeyType(),
$offset_type
);
$type = new TArray([
$new_key_type,
$generic_params,
]);
if (!$array_access_type) {
$array_access_type = clone $generic_params;
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$generic_params
);
}
} else {
if (!$array_access_type) {
$array_access_type = $type->getGenericValueType();
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$type->getGenericValueType()
);
}
}
$has_valid_offset = true;
} else {
$expected_offset_types[] = (string)$type->getGenericKeyType();
2018-01-14 18:09:40 +01:00
$array_access_type = Type::getMixed();
}
}
continue;
}
if ($type instanceof TString) {
if ($in_assignment && $replacement_type) {
if ($replacement_type->isMixed()) {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementMixedCount($statements_checker->getCheckedFilePath());
if (IssueBuffer::accepts(
new MixedStringOffsetAssignment(
'Right-hand-side of string offset assignment cannot be mixed',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementNonMixedCount($statements_checker->getCheckedFilePath());
2018-01-14 18:09:40 +01:00
}
}
if (!TypeChecker::isContainedBy(
2018-02-01 06:50:01 +01:00
$project_checker->codebase,
2018-01-14 18:09:40 +01:00
$offset_type,
Type::getInt(),
true
)) {
$expected_offset_types[] = 'int';
2018-01-14 18:09:40 +01:00
} else {
$has_valid_offset = true;
}
if (!$array_access_type) {
$array_access_type = Type::getString();
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
Type::getString()
);
}
continue;
}
if ($type instanceof TMixed || $type instanceof TEmpty) {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementMixedCount($statements_checker->getCheckedFilePath());
2018-01-14 18:09:40 +01:00
if ($in_assignment) {
if (IssueBuffer::accepts(
new MixedArrayAssignment(
'Cannot access array value on mixed variable ' . $array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new MixedArrayAccess(
'Cannot access array value on mixed variable ' . $array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
$array_access_type = Type::getMixed();
break;
}
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementNonMixedCount($statements_checker->getCheckedFilePath());
2018-01-14 18:09:40 +01:00
if ($type instanceof TNamedObject) {
if (strtolower($type->value) !== 'simplexmlelement'
2018-02-01 06:50:01 +01:00
&& $codebase->classExists($type->value)
&& !$codebase->classImplements($type->value, 'ArrayAccess')
2018-01-14 18:09:40 +01:00
) {
$non_array_types[] = (string)$type;
} else {
$array_access_type = Type::getMixed();
}
} else {
$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],
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)
) {
// 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],
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)
) {
// 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],
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// 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],
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
$array_access_type = Type::getMixed();
}
}
if ($offset_type->isMixed()) {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementMixedCount($statements_checker->getCheckedFilePath());
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new MixedArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' using mixed offset',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
2018-02-04 00:52:35 +01:00
$codebase->analyzer->incrementNonMixedCount($statements_checker->getCheckedFilePath());
if ($expected_offset_types) {
$invalid_offset_type = $expected_offset_types[0];
$used_offset = 'using a ' . $offset_type . ' offset';
if ($key_value !== null) {
$used_offset = 'using offset value of '
. (is_int($key_value) ? $key_value : '\'' . $key_value . '\'');
}
if ($has_valid_offset) {
if (IssueBuffer::accepts(
new PossiblyInvalidArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
. ', expecting ' . $invalid_offset_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidArrayOffset(
'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
. ', expecting ' . $invalid_offset_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
2018-01-14 18:09:40 +01:00
}
}
}
if ($array_access_type === null) {
throw new \InvalidArgumentException('This is a bad place');
}
if ($in_assignment) {
$array_type->bustCache();
}
2018-01-14 18:09:40 +01:00
return $array_access_type;
}
}