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;
|
2020-05-18 21:13:27 +02:00
|
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
2018-11-06 03:57:36 +01:00
|
|
|
|
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;
|
2019-04-26 00:02:19 +02:00
|
|
|
|
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;
|
2018-04-17 20:06:27 +02:00
|
|
|
|
use Psalm\Issue\PossiblyUndefinedArrayOffset;
|
2019-11-11 15:59:56 +01:00
|
|
|
|
use Psalm\Issue\PossiblyUndefinedIntArrayOffset;
|
|
|
|
|
use Psalm\Issue\PossiblyUndefinedStringArrayOffset;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
use Psalm\IssueBuffer;
|
|
|
|
|
use Psalm\Type;
|
|
|
|
|
use Psalm\Type\Atomic\ObjectLike;
|
|
|
|
|
use Psalm\Type\Atomic\TArray;
|
2019-01-05 06:15:53 +01:00
|
|
|
|
use Psalm\Type\Atomic\TArrayKey;
|
2019-12-27 18:49:28 +01:00
|
|
|
|
use Psalm\Type\Atomic\TClassStringMap;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
use Psalm\Type\Atomic\TEmpty;
|
2018-05-05 23:30:18 +02:00
|
|
|
|
use Psalm\Type\Atomic\TLiteralInt;
|
|
|
|
|
use Psalm\Type\Atomic\TLiteralString;
|
2019-02-22 03:40:06 +01:00
|
|
|
|
use Psalm\Type\Atomic\TTemplateParam;
|
2018-05-05 23:30:18 +02:00
|
|
|
|
use Psalm\Type\Atomic\TInt;
|
2019-10-09 00:44:46 +02:00
|
|
|
|
use Psalm\Type\Atomic\TList;
|
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;
|
2019-10-09 01:01:00 +02:00
|
|
|
|
use Psalm\Type\Atomic\TNonEmptyList;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
use Psalm\Type\Atomic\TNull;
|
2018-08-21 17:40:29 +02:00
|
|
|
|
use Psalm\Type\Atomic\TSingleLetter;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
use Psalm\Type\Atomic\TString;
|
2019-06-26 22:52:29 +02:00
|
|
|
|
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;
|
2020-05-22 04:47:58 +02:00
|
|
|
|
use Psalm\Internal\Taint\TaintNode;
|
2019-08-14 06:47:57 +02:00
|
|
|
|
use Psalm\Internal\Taint\Source;
|
2019-12-27 18:49:28 +01:00
|
|
|
|
use Psalm\Internal\Type\TemplateResult;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
class ArrayFetchAnalyzer
|
2018-01-14 18:09:40 +01:00
|
|
|
|
{
|
|
|
|
|
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
|
2020-05-18 21:13:27 +02:00
|
|
|
|
) : bool {
|
|
|
|
|
$array_var_id = ExpressionIdentifier::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
|
|
|
|
);
|
|
|
|
|
|
2020-04-13 02:38:36 +02:00
|
|
|
|
if ($stmt->dim && ExpressionAnalyzer::analyze($statements_analyzer, $stmt->dim, $context) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
|
$keyed_array_var_id = ExpressionIdentifier::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-05-12 05:14:44 +02:00
|
|
|
|
$dim_var_id = null;
|
|
|
|
|
$new_offset_type = null;
|
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
|
if ($stmt->dim) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$used_key_type = $statements_analyzer->node_data->getType($stmt->dim) ?: Type::getMixed();
|
2018-05-12 05:14:44 +02:00
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
|
$dim_var_id = ExpressionIdentifier::getArrayVarId(
|
2018-05-12 05:14:44 +02:00
|
|
|
|
$stmt->dim,
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->getFQCLN(),
|
|
|
|
|
$statements_analyzer
|
2018-05-12 05:14:44 +02:00
|
|
|
|
);
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-12 07:26:11 +02:00
|
|
|
|
$stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
|
|
|
|
|
|
2018-03-18 00:28:01 +01:00
|
|
|
|
if ($keyed_array_var_id
|
2018-04-10 07:27:26 +02:00
|
|
|
|
&& $context->hasVariable($keyed_array_var_id)
|
2020-01-31 23:25:15 +01:00
|
|
|
|
&& !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined
|
2020-04-12 07:26:11 +02:00
|
|
|
|
&& $stmt_var_type
|
|
|
|
|
&& !$stmt_var_type->hasClassStringMap()
|
2018-03-18 00:28:01 +01:00
|
|
|
|
) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$statements_analyzer->node_data->setType(
|
|
|
|
|
$stmt,
|
2020-01-31 23:25:15 +01:00
|
|
|
|
clone $context->vars_in_scope[$keyed_array_var_id]
|
2019-11-25 17:44:54 +01:00
|
|
|
|
);
|
2018-02-17 17:36:20 +01:00
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
|
return true;
|
2018-02-17 17:24:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-10 22:21:48 +02:00
|
|
|
|
$can_store_result = false;
|
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
2018-11-06 03:57:36 +01:00
|
|
|
|
|
2020-04-12 07:26:11 +02:00
|
|
|
|
if ($stmt_var_type) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if ($stmt_var_type->isNull()) {
|
2018-01-14 18:09:40 +01:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) {
|
|
|
|
|
$statements_analyzer->node_data->setType(
|
|
|
|
|
$stmt,
|
|
|
|
|
Type::combineUnionTypes($stmt_type, Type::getNull())
|
|
|
|
|
);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
} else {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getNull());
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
|
return true;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$stmt_type = self::getArrayAccessTypeGivenOffset(
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer,
|
2018-01-14 18:09:40 +01:00
|
|
|
|
$stmt,
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$stmt_var_type,
|
2018-01-14 18:09:40 +01:00
|
|
|
|
$used_key_type,
|
|
|
|
|
false,
|
|
|
|
|
$array_var_id,
|
2019-01-30 21:40:37 +01:00
|
|
|
|
$context,
|
|
|
|
|
null
|
2018-01-14 18:09:40 +01:00
|
|
|
|
);
|
2018-05-12 05:14:44 +02:00
|
|
|
|
|
2020-04-10 22:21:48 +02:00
|
|
|
|
if ($stmt->dim && $stmt_var_type->hasArray()) {
|
|
|
|
|
/**
|
|
|
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
|
|
|
* @var TArray|ObjectLike|TList|Type\Atomic\TClassStringMap
|
|
|
|
|
*/
|
|
|
|
|
$array_type = $stmt_var_type->getAtomicTypes()['array'];
|
|
|
|
|
|
|
|
|
|
if ($array_type instanceof Type\Atomic\TClassStringMap) {
|
|
|
|
|
$array_value_type = Type::getMixed();
|
|
|
|
|
} elseif ($array_type instanceof TArray) {
|
|
|
|
|
$array_value_type = $array_type->type_params[1];
|
|
|
|
|
} elseif ($array_type instanceof TList) {
|
|
|
|
|
$array_value_type = $array_type->type_param;
|
|
|
|
|
} else {
|
|
|
|
|
$array_value_type = $array_type->getGenericValueType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($context->inside_assignment || !$array_value_type->isMixed()) {
|
|
|
|
|
$can_store_result = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
|
|
|
|
|
2020-05-22 04:47:58 +02:00
|
|
|
|
if ($codebase->taint) {
|
|
|
|
|
if ($array_var_id === '$_GET' || $array_var_id === '$_POST' || $array_var_id === '$_COOKIE') {
|
|
|
|
|
$taint_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
|
|
|
|
|
|
|
|
|
|
$server_taint_source = new Source(
|
|
|
|
|
$array_var_id . ':' . $taint_location->file_name . ':' . $taint_location->raw_file_start,
|
2019-10-19 23:59:10 +02:00
|
|
|
|
$array_var_id,
|
2020-05-22 04:47:58 +02:00
|
|
|
|
$taint_location,
|
|
|
|
|
null,
|
|
|
|
|
Type\Union::TAINTED_INPUT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$codebase->taint->addSource($server_taint_source);
|
|
|
|
|
|
|
|
|
|
$stmt_type->parent_nodes = [
|
|
|
|
|
$server_taint_source
|
|
|
|
|
];
|
|
|
|
|
}
|
2019-08-04 16:37:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-12 05:14:44 +02:00
|
|
|
|
if ($context->inside_isset
|
|
|
|
|
&& $stmt->dim
|
2019-11-25 17:44:54 +01:00
|
|
|
|
&& ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))
|
|
|
|
|
&& $stmt_var_type->hasArray()
|
2018-05-12 05:14:44 +02:00
|
|
|
|
&& ($stmt->var instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
|
|
|
|| $stmt->var instanceof PhpParser\Node\Expr\ConstFetch)
|
|
|
|
|
) {
|
2019-10-01 22:13:17 +02:00
|
|
|
|
/**
|
2019-11-11 16:11:42 +01:00
|
|
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
2019-10-11 02:16:43 +02:00
|
|
|
|
* @var TArray|ObjectLike|TList
|
2019-10-01 22:13:17 +02:00
|
|
|
|
*/
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$array_type = $stmt_var_type->getAtomicTypes()['array'];
|
2018-05-12 05:14:44 +02:00
|
|
|
|
|
|
|
|
|
if ($array_type instanceof TArray) {
|
|
|
|
|
$const_array_key_type = $array_type->type_params[0];
|
2019-10-11 02:16:43 +02:00
|
|
|
|
} elseif ($array_type instanceof TList) {
|
2020-04-10 22:21:48 +02:00
|
|
|
|
$const_array_key_type = Type::getInt();
|
2018-05-12 05:14:44 +02:00
|
|
|
|
} else {
|
|
|
|
|
$const_array_key_type = $array_type->getGenericKeyType();
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-15 00:52:29 +01:00
|
|
|
|
if ($dim_var_id
|
|
|
|
|
&& !$const_array_key_type->hasMixed()
|
2019-11-25 17:44:54 +01:00
|
|
|
|
&& !$stmt_dim_type->hasMixed()
|
2018-12-15 00:52:29 +01:00
|
|
|
|
) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$new_offset_type = clone $stmt_dim_type;
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$const_array_key_atomic_types = $const_array_key_type->getAtomicTypes();
|
2018-05-12 05:14:44 +02:00
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) {
|
2018-05-12 05:14:44 +02:00
|
|
|
|
if ($offset_atomic_type instanceof TString
|
|
|
|
|
|| $offset_atomic_type instanceof TInt
|
|
|
|
|
) {
|
2018-05-18 17:02:50 +02:00
|
|
|
|
if (!isset($const_array_key_atomic_types[$offset_key])
|
2018-11-06 03:57:36 +01:00
|
|
|
|
&& !TypeAnalyzer::isContainedBy(
|
|
|
|
|
$codebase,
|
2018-05-18 17:02:50 +02:00
|
|
|
|
new Type\Union([$offset_atomic_type]),
|
|
|
|
|
$const_array_key_type
|
|
|
|
|
)
|
|
|
|
|
) {
|
2018-05-12 05:14:44 +02:00
|
|
|
|
$new_offset_type->removeType($offset_key);
|
|
|
|
|
}
|
2018-11-06 03:57:36 +01:00
|
|
|
|
} elseif (!TypeAnalyzer::isContainedBy(
|
|
|
|
|
$codebase,
|
2018-05-12 17:17:41 +02:00
|
|
|
|
$const_array_key_type,
|
|
|
|
|
new Type\Union([$offset_atomic_type])
|
|
|
|
|
)) {
|
2018-05-12 05:14:44 +02:00
|
|
|
|
$new_offset_type->removeType($offset_key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-24 00:06:22 +02:00
|
|
|
|
if ($keyed_array_var_id
|
|
|
|
|
&& $context->hasVariable($keyed_array_var_id, $statements_analyzer)
|
2019-11-25 17:44:54 +01:00
|
|
|
|
&& (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isVanillaMixed())
|
2019-05-24 00:06:22 +02:00
|
|
|
|
) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$statements_analyzer->node_data->setType($stmt, $context->vars_in_scope[$keyed_array_var_id]);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))) {
|
|
|
|
|
$stmt_type = Type::getMixed();
|
|
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
2018-03-17 22:35:36 +01:00
|
|
|
|
} else {
|
2020-04-09 15:27:14 +02:00
|
|
|
|
if ($stmt_type->possibly_undefined
|
|
|
|
|
&& !$context->inside_isset
|
|
|
|
|
&& !$context->inside_unset
|
|
|
|
|
&& ($stmt_var_type && !$stmt_var_type->hasMixed())
|
|
|
|
|
) {
|
2018-04-17 20:06:27 +02:00
|
|
|
|
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-04-17 20:06:27 +02:00
|
|
|
|
),
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
2018-04-17 20:06:27 +02:00
|
|
|
|
)) {
|
2018-11-28 17:45:54 +01:00
|
|
|
|
// fall through
|
2018-03-17 22:35:36 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-12-06 04:35:08 +01:00
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$stmt_type->possibly_undefined = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
if ($context->inside_isset && $dim_var_id && $new_offset_type && $new_offset_type->getAtomicTypes()) {
|
2018-05-12 05:14:44 +02:00
|
|
|
|
$context->vars_in_scope[$dim_var_id] = $new_offset_type;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-10 22:21:48 +02:00
|
|
|
|
if ($keyed_array_var_id && !$context->inside_isset && $can_store_result) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$context->vars_in_scope[$keyed_array_var_id] = $stmt_type;
|
2018-04-10 07:27:26 +02:00
|
|
|
|
$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-04-10 07:27:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if ($codebase->taint && ($stmt_var_type = $statements_analyzer->node_data->getType($stmt->var))) {
|
2019-10-12 05:28:17 +02:00
|
|
|
|
$sources = [];
|
|
|
|
|
|
2020-05-22 04:47:58 +02:00
|
|
|
|
$sources = \array_merge($sources, $stmt_var_type->parent_nodes ?: []);
|
2019-10-12 05:28:17 +02:00
|
|
|
|
|
|
|
|
|
if ($sources) {
|
2020-05-22 04:47:58 +02:00
|
|
|
|
$stmt_type->parent_nodes = $sources;
|
2019-10-12 05:28:17 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
|
return true;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param Type\Union $array_type
|
|
|
|
|
* @param Type\Union $offset_type
|
|
|
|
|
* @param bool $in_assignment
|
2018-02-18 23:55:11 +01:00
|
|
|
|
* @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,
|
2019-09-25 18:17:37 +02:00
|
|
|
|
bool $in_assignment,
|
|
|
|
|
?string $array_var_id,
|
2019-01-30 21:40:37 +01:00
|
|
|
|
Context $context,
|
2019-01-31 18:45:47 +01:00
|
|
|
|
PhpParser\Node\Expr $assign_value = null,
|
2019-01-30 21:40:37 +01:00
|
|
|
|
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;
|
2018-02-06 17:27:01 +01:00
|
|
|
|
$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;
|
2019-11-25 17:44:54 +01:00
|
|
|
|
} elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($stmt_dim_type->getAtomicTypes() as $possible_value_type) {
|
2018-05-05 23:30:18 +02:00
|
|
|
|
if ($possible_value_type instanceof TLiteralString
|
|
|
|
|
|| $possible_value_type instanceof TLiteralInt
|
|
|
|
|
) {
|
2018-05-18 17:02:50 +02:00
|
|
|
|
if ($key_value !== null) {
|
2018-05-05 23:30:18 +02:00
|
|
|
|
$key_value = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2018-05-18 17:02:50 +02:00
|
|
|
|
|
|
|
|
|
$key_value = $possible_value_type->value;
|
2018-05-05 23:30:18 +02:00
|
|
|
|
} 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
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-25 18:17:37 +02:00
|
|
|
|
if ($in_assignment) {
|
|
|
|
|
$offset_type->removeType('null');
|
|
|
|
|
$offset_type->addType(new TLiteralInt(0));
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-25 18:17:37 +02:00
|
|
|
|
if ($offset_type->isNullable() && !$context->inside_isset) {
|
|
|
|
|
if (!$offset_type->ignore_nullable_issues) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyNullArrayOffset(
|
|
|
|
|
'Cannot access value on variable ' . $array_var_id
|
|
|
|
|
. ' using possibly null offset ' . $offset_type,
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt->var)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($in_assignment) {
|
|
|
|
|
$offset_type->removeType('null');
|
2019-09-25 19:12:29 +02:00
|
|
|
|
|
|
|
|
|
if (!$offset_type->ignore_nullable_issues) {
|
|
|
|
|
$offset_type->addType(new TLiteralInt(0));
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($array_type->getAtomicTypes() as $type_string => $type) {
|
2019-05-24 05:53:48 +02:00
|
|
|
|
$original_type = $type;
|
|
|
|
|
|
2019-05-24 00:06:22 +02:00
|
|
|
|
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;
|
2020-04-09 16:42:54 +02:00
|
|
|
|
if (!$array_access_type) {
|
2020-04-12 07:26:11 +02:00
|
|
|
|
$array_access_type = Type::getMixed(
|
|
|
|
|
$type instanceof TEmpty
|
|
|
|
|
);
|
2020-04-09 16:42:54 +02:00
|
|
|
|
} else {
|
|
|
|
|
$array_access_type = Type::combineUnionTypes(
|
|
|
|
|
$array_access_type,
|
2020-04-12 07:26:11 +02:00
|
|
|
|
Type::getMixed($type instanceof TEmpty)
|
2020-04-09 16:42:54 +02:00
|
|
|
|
);
|
|
|
|
|
}
|
2020-04-12 07:26:11 +02:00
|
|
|
|
|
2020-04-09 16:42:54 +02:00
|
|
|
|
continue;
|
2019-05-24 00:06:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$type = clone array_values($type->as->getAtomicTypes())[0];
|
2019-05-24 00:06:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2019-01-30 21:40:37 +01:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-27 18:49:28 +01:00
|
|
|
|
if ($type instanceof TArray
|
|
|
|
|
|| $type instanceof ObjectLike
|
|
|
|
|
|| $type instanceof TList
|
|
|
|
|
|| $type instanceof TClassStringMap
|
|
|
|
|
) {
|
2018-01-14 18:09:40 +01:00
|
|
|
|
$has_array_access = true;
|
|
|
|
|
|
|
|
|
|
if ($in_assignment
|
|
|
|
|
&& $type instanceof TArray
|
2019-08-27 16:18:58 +02:00
|
|
|
|
&& (($type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty())
|
2020-05-24 05:23:50 +02:00
|
|
|
|
|| ($type->type_params[1]->hasMixed() && \is_string($key_value)))
|
2018-01-14 18:09:40 +01:00
|
|
|
|
) {
|
2019-09-08 16:23:12 +02:00
|
|
|
|
$from_empty_array = $type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty();
|
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
if ($key_value !== null) {
|
|
|
|
|
$from_mixed_array = $type->type_params[1]->isMixed();
|
2019-10-01 22:13:17 +02:00
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
$previous_key_type = $type->type_params[0];
|
|
|
|
|
$previous_value_type = $type->type_params[1];
|
2019-08-27 20:16:34 +02:00
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
// ok, type becomes an ObjectLike
|
|
|
|
|
$array_type->removeType($type_string);
|
|
|
|
|
$type = new ObjectLike([$key_value => $from_mixed_array ? Type::getMixed() : Type::getEmpty()]);
|
2019-09-08 16:23:12 +02:00
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
$type->sealed = $from_empty_array;
|
2019-08-27 20:16:34 +02:00
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
if (!$from_empty_array) {
|
|
|
|
|
$type->previous_value_type = clone $previous_value_type;
|
|
|
|
|
$type->previous_key_type = clone $previous_key_type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$array_type->addType($type);
|
|
|
|
|
} elseif (!$stmt->dim && $from_empty_array && $replacement_type) {
|
|
|
|
|
$array_type->removeType($type_string);
|
|
|
|
|
$array_type->addType(new Type\Atomic\TNonEmptyList($replacement_type));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-05-24 05:23:50 +02:00
|
|
|
|
} elseif ($in_assignment
|
|
|
|
|
&& $type instanceof ObjectLike
|
|
|
|
|
&& $type->previous_value_type
|
|
|
|
|
&& $type->previous_value_type->isMixed()
|
|
|
|
|
&& $key_value !== null
|
|
|
|
|
) {
|
|
|
|
|
$type->properties[$key_value] = Type::getMixed();
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-03 19:56:30 +02:00
|
|
|
|
$offset_type = self::replaceOffsetTypeWithInts($offset_type);
|
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
if ($type instanceof TList
|
|
|
|
|
&& (($in_assignment && $stmt->dim)
|
|
|
|
|
|| $original_type instanceof TTemplateParam
|
|
|
|
|
|| !$offset_type->isInt())
|
|
|
|
|
) {
|
|
|
|
|
$type = new TArray([Type::getInt(), $type->type_param]);
|
|
|
|
|
}
|
|
|
|
|
|
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()) {
|
2020-02-22 18:12:40 +01:00
|
|
|
|
$type->type_params[0] = $offset_type->isMixed()
|
|
|
|
|
? Type::getArrayKey()
|
|
|
|
|
: $offset_type;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
} elseif (!$type->type_params[0]->isEmpty()) {
|
2018-12-14 21:10:10 +01:00
|
|
|
|
$expected_offset_type = $type->type_params[0]->hasMixed()
|
2019-01-05 06:15:53 +01:00
|
|
|
|
? new Type\Union([ new TArrayKey ])
|
2018-12-14 21:10:10 +01:00
|
|
|
|
: $type->type_params[0];
|
|
|
|
|
|
2019-06-08 03:27:50 +02:00
|
|
|
|
$templated_offset_type = null;
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($offset_type->getAtomicTypes() as $offset_atomic_type) {
|
2019-06-08 03:27:50 +02:00
|
|
|
|
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) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($templated_offset_type->as->getAtomicTypes() as $offset_as) {
|
2019-06-08 03:27:50 +02:00
|
|
|
|
if ($offset_as instanceof Type\Atomic\TTemplateKeyOf
|
|
|
|
|
&& $offset_as->param_name === $original_type->param_name
|
|
|
|
|
&& $offset_as->defining_class === $original_type->defining_class
|
|
|
|
|
) {
|
2019-10-09 16:04:34 +02:00
|
|
|
|
/** @psalm-suppress PropertyTypeCoercion */
|
2019-06-08 03:27:50 +02:00
|
|
|
|
$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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-18 20:21:06 +02:00
|
|
|
|
} else {
|
|
|
|
|
$offset_type_contained_by_expected = TypeAnalyzer::isContainedBy(
|
|
|
|
|
$codebase,
|
|
|
|
|
$offset_type,
|
|
|
|
|
$expected_offset_type,
|
|
|
|
|
true,
|
|
|
|
|
$offset_type->ignore_falsable_issues,
|
|
|
|
|
$union_comparison_results
|
|
|
|
|
);
|
|
|
|
|
|
2019-09-26 01:02:49 +02:00
|
|
|
|
if ($codebase->config->ensure_array_string_offsets_exist
|
|
|
|
|
&& $offset_type_contained_by_expected
|
2019-07-10 07:35:57 +02:00
|
|
|
|
) {
|
2019-10-04 17:08:08 +02:00
|
|
|
|
self::checkLiteralStringArrayOffset(
|
|
|
|
|
$offset_type,
|
|
|
|
|
$expected_offset_type,
|
|
|
|
|
$array_var_id,
|
|
|
|
|
$stmt,
|
|
|
|
|
$context,
|
|
|
|
|
$statements_analyzer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($codebase->config->ensure_array_int_offsets_exist
|
|
|
|
|
&& $offset_type_contained_by_expected
|
|
|
|
|
) {
|
|
|
|
|
self::checkLiteralIntArrayOffset(
|
2019-10-04 03:34:56 +02:00
|
|
|
|
$offset_type,
|
|
|
|
|
$expected_offset_type,
|
|
|
|
|
$array_var_id,
|
|
|
|
|
$stmt,
|
|
|
|
|
$context,
|
|
|
|
|
$statements_analyzer
|
|
|
|
|
);
|
2019-01-05 06:15:53 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-18 20:21:06 +02:00
|
|
|
|
if ((!$offset_type_contained_by_expected
|
|
|
|
|
&& !$union_comparison_results->type_coerced_from_scalar)
|
|
|
|
|
|| $union_comparison_results->to_string_cast
|
|
|
|
|
) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2018-12-14 21:10:10 +01:00
|
|
|
|
$has_valid_offset = true;
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-09 16:56:27 +01:00
|
|
|
|
if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) {
|
2018-05-18 17:02:50 +02:00
|
|
|
|
$type->count++;
|
2018-05-03 19:56:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
|
if ($in_assignment && $replacement_type) {
|
2019-10-09 16:04:34 +02:00
|
|
|
|
/** @psalm-suppress PropertyTypeCoercion */
|
2018-01-14 18:09:40 +01:00
|
|
|
|
$type->type_params[1] = Type::combineUnionTypes(
|
|
|
|
|
$type->type_params[1],
|
2019-03-03 00:02:11 +01:00
|
|
|
|
$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]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-12 18:08:15 +02:00
|
|
|
|
if ($array_access_type->isEmpty()
|
2018-12-14 21:10:10 +01:00
|
|
|
|
&& !$array_type->hasMixed()
|
2018-07-12 18:08:15 +02:00
|
|
|
|
&& !$in_assignment
|
2019-01-30 21:40:37 +01:00
|
|
|
|
&& !$context->inside_isset
|
2018-07-12 18:08:15 +02:00
|
|
|
|
) {
|
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
|
|
|
|
)) {
|
2018-04-07 00:28:22 +02:00
|
|
|
|
return Type::getMixed(true);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IssueBuffer::isRecording()) {
|
2018-05-03 02:10:08 +02:00
|
|
|
|
$array_access_type = Type::getMixed(true);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-10-09 00:44:46 +02:00
|
|
|
|
} elseif ($type instanceof TList) {
|
|
|
|
|
// if we're assigning to an empty array with a key offset, refashion that array
|
|
|
|
|
if (!$in_assignment) {
|
2019-11-26 22:34:13 +01:00
|
|
|
|
if (!$type instanceof TNonEmptyList
|
|
|
|
|
|| ($key_value > 0 && $key_value > ($type->count - 1))
|
|
|
|
|
) {
|
2019-10-09 01:01:00 +02:00
|
|
|
|
$expected_offset_type = Type::getInt();
|
2019-10-09 00:44:46 +02:00
|
|
|
|
|
2019-10-09 01:01:00 +02:00
|
|
|
|
if ($codebase->config->ensure_array_int_offsets_exist) {
|
|
|
|
|
self::checkLiteralIntArrayOffset(
|
|
|
|
|
$offset_type,
|
|
|
|
|
$expected_offset_type,
|
|
|
|
|
$array_var_id,
|
|
|
|
|
$stmt,
|
|
|
|
|
$context,
|
|
|
|
|
$statements_analyzer
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-10-09 00:44:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$has_valid_offset = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($in_assignment && $type instanceof Type\Atomic\TNonEmptyList && $type->count !== null) {
|
|
|
|
|
$type->count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($in_assignment && $replacement_type) {
|
|
|
|
|
$type->type_param = Type::combineUnionTypes(
|
|
|
|
|
$type->type_param,
|
|
|
|
|
$replacement_type,
|
|
|
|
|
$codebase
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$array_access_type) {
|
|
|
|
|
$array_access_type = $type->type_param;
|
|
|
|
|
} else {
|
|
|
|
|
$array_access_type = Type::combineUnionTypes(
|
|
|
|
|
$array_access_type,
|
|
|
|
|
$type->type_param
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-12-27 18:49:28 +01:00
|
|
|
|
} elseif ($type instanceof TClassStringMap) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$offset_type_parts = array_values($offset_type->getAtomicTypes());
|
2019-12-27 18:49:28 +01:00
|
|
|
|
|
|
|
|
|
foreach ($offset_type_parts as $offset_type_part) {
|
|
|
|
|
if ($offset_type_part instanceof Type\Atomic\TClassString) {
|
|
|
|
|
if ($offset_type_part instanceof Type\Atomic\TTemplateParamClass) {
|
|
|
|
|
$template_result_get = new TemplateResult(
|
|
|
|
|
[],
|
|
|
|
|
[
|
|
|
|
|
$type->param_name => [
|
|
|
|
|
'class-string-map' => [
|
|
|
|
|
new Type\Union([
|
|
|
|
|
new TTemplateParam(
|
|
|
|
|
$offset_type_part->param_name,
|
|
|
|
|
$offset_type_part->as_type
|
|
|
|
|
? new Type\Union([$offset_type_part->as_type])
|
|
|
|
|
: Type::getObject(),
|
|
|
|
|
$offset_type_part->defining_class
|
|
|
|
|
)
|
|
|
|
|
])
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$template_result_set = new TemplateResult(
|
|
|
|
|
[],
|
|
|
|
|
[
|
|
|
|
|
$offset_type_part->param_name => [
|
2019-12-29 00:37:55 +01:00
|
|
|
|
$offset_type_part->defining_class => [
|
2019-12-27 18:49:28 +01:00
|
|
|
|
new Type\Union([
|
|
|
|
|
new TTemplateParam(
|
|
|
|
|
$type->param_name,
|
|
|
|
|
$type->as_type
|
|
|
|
|
? new Type\Union([$type->as_type])
|
|
|
|
|
: Type::getObject(),
|
|
|
|
|
'class-string-map'
|
|
|
|
|
)
|
|
|
|
|
])
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$template_result_get = new TemplateResult(
|
|
|
|
|
[],
|
|
|
|
|
[
|
|
|
|
|
$type->param_name => [
|
|
|
|
|
'class-string-map' => [
|
|
|
|
|
new Type\Union([
|
|
|
|
|
$offset_type_part->as_type
|
|
|
|
|
?: new Type\Atomic\TObject()
|
|
|
|
|
])
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
$template_result_set = new TemplateResult(
|
|
|
|
|
[],
|
|
|
|
|
[]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$expected_value_param_get = clone $type->value_param;
|
|
|
|
|
|
|
|
|
|
$expected_value_param_get->replaceTemplateTypesWithArgTypes(
|
2020-04-07 06:13:56 +02:00
|
|
|
|
$template_result_get,
|
2019-12-27 18:49:28 +01:00
|
|
|
|
$codebase
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($replacement_type) {
|
|
|
|
|
$expected_value_param_set = clone $type->value_param;
|
|
|
|
|
|
|
|
|
|
$replacement_type->replaceTemplateTypesWithArgTypes(
|
2020-04-07 06:13:56 +02:00
|
|
|
|
$template_result_set,
|
2019-12-27 18:49:28 +01:00
|
|
|
|
$codebase
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$type->value_param = Type::combineUnionTypes(
|
|
|
|
|
$replacement_type,
|
|
|
|
|
$expected_value_param_set,
|
|
|
|
|
$codebase
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$array_access_type) {
|
|
|
|
|
$array_access_type = $expected_value_param_get;
|
|
|
|
|
} else {
|
|
|
|
|
$array_access_type = Type::combineUnionTypes(
|
|
|
|
|
$array_access_type,
|
|
|
|
|
$expected_value_param_get,
|
|
|
|
|
$codebase
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-07 19:57:45 +01:00
|
|
|
|
} else {
|
2018-09-01 02:24:50 +02:00
|
|
|
|
$generic_key_type = $type->getGenericKeyType();
|
|
|
|
|
|
2019-10-09 00:44:46 +02:00
|
|
|
|
if (!$stmt->dim && $type->sealed && $type->is_list) {
|
|
|
|
|
$key_value = count($type->properties);
|
|
|
|
|
}
|
|
|
|
|
|
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]
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-10-01 22:13:17 +02:00
|
|
|
|
} elseif ($type->previous_value_type) {
|
2019-10-04 03:34:56 +02:00
|
|
|
|
if ($codebase->config->ensure_array_string_offsets_exist) {
|
2019-10-04 17:08:08 +02:00
|
|
|
|
self::checkLiteralStringArrayOffset(
|
|
|
|
|
$offset_type,
|
|
|
|
|
$type->getGenericKeyType(),
|
|
|
|
|
$array_var_id,
|
|
|
|
|
$stmt,
|
|
|
|
|
$context,
|
|
|
|
|
$statements_analyzer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($codebase->config->ensure_array_int_offsets_exist) {
|
|
|
|
|
self::checkLiteralIntArrayOffset(
|
2019-10-04 03:34:56 +02:00
|
|
|
|
$offset_type,
|
|
|
|
|
$type->getGenericKeyType(),
|
|
|
|
|
$array_var_id,
|
|
|
|
|
$stmt,
|
|
|
|
|
$context,
|
|
|
|
|
$statements_analyzer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-01 22:13:17 +02:00
|
|
|
|
$type->properties[$key_value] = clone $type->previous_value_type;
|
2019-03-11 04:38:30 +01:00
|
|
|
|
|
2019-10-01 22:13:17 +02:00
|
|
|
|
$array_access_type = clone $type->previous_value_type;
|
2020-04-09 15:27:14 +02:00
|
|
|
|
} elseif ($array_type->hasMixed()) {
|
|
|
|
|
$has_valid_offset = true;
|
|
|
|
|
|
|
|
|
|
$array_access_type = Type::getMixed();
|
2018-01-14 18:09:40 +01:00
|
|
|
|
} else {
|
2020-02-22 06:29:59 +01:00
|
|
|
|
if ($type->sealed || !$context->inside_isset) {
|
2018-05-05 23:30:18 +02:00
|
|
|
|
$object_like_keys = array_keys($type->properties);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
2018-05-05 23:30:18 +02: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
|
|
|
|
|
2018-05-05 23:30:18 +02: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 {
|
2018-12-08 19:18:55 +01:00
|
|
|
|
$key_type = $generic_key_type->hasMixed()
|
2019-01-05 06:15:53 +01:00
|
|
|
|
? 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
|
|
|
|
);
|
|
|
|
|
|
2019-01-30 21:40:37 +01:00
|
|
|
|
if ($context->inside_isset && !$is_contained) {
|
2020-02-13 22:38:58 +01:00
|
|
|
|
$is_contained = TypeAnalyzer::isContainedBy(
|
2018-11-02 04:31:40 +01:00
|
|
|
|
$codebase,
|
|
|
|
|
$key_type,
|
2020-02-13 22:38:58 +01:00
|
|
|
|
$offset_type,
|
2018-11-02 04:31:40 +01:00
|
|
|
|
true,
|
|
|
|
|
$offset_type->ignore_falsable_issues
|
2020-02-13 23:58:15 +01:00
|
|
|
|
)
|
|
|
|
|
|| TypeAnalyzer::canBeContainedBy(
|
|
|
|
|
$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-05-03 19:56:30 +02:00
|
|
|
|
|
2018-11-02 04:31:40 +01:00
|
|
|
|
$new_key_type = Type::combineUnionTypes(
|
|
|
|
|
$generic_key_type,
|
2020-02-22 18:12:40 +01:00
|
|
|
|
$offset_type->isMixed() ? Type::getArrayKey() : $offset_type
|
2018-11-02 04:31:40 +01:00
|
|
|
|
);
|
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-05-03 19:56:30 +02:00
|
|
|
|
|
2018-11-02 04:31:40 +01:00
|
|
|
|
if (!$stmt->dim && $property_count) {
|
|
|
|
|
++$property_count;
|
2019-05-24 00:06:22 +02:00
|
|
|
|
$array_type->removeType($type_string);
|
2018-11-09 16:56:27 +01:00
|
|
|
|
$type = new TNonEmptyArray([
|
|
|
|
|
$new_key_type,
|
|
|
|
|
$generic_params,
|
|
|
|
|
]);
|
2019-05-24 00:06:22 +02:00
|
|
|
|
$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 {
|
2019-05-24 00:06:22 +02:00
|
|
|
|
$array_type->removeType($type_string);
|
2018-11-09 16:56:27 +01:00
|
|
|
|
$type = new TArray([
|
|
|
|
|
$new_key_type,
|
|
|
|
|
$generic_params,
|
|
|
|
|
]);
|
2019-05-24 00:06:22 +02:00
|
|
|
|
$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 {
|
2019-12-04 18:23:26 +01:00
|
|
|
|
if (!$context->inside_isset
|
|
|
|
|
|| ($type->sealed && !$union_comparison_results->type_coerced)
|
|
|
|
|
) {
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$expected_offset_types[] = $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) {
|
2018-01-31 22:08:52 +01:00
|
|
|
|
if ($in_assignment && $replacement_type) {
|
2018-12-08 19:18:55 +01:00
|
|
|
|
if ($replacement_type->hasMixed()) {
|
2019-03-23 14:50:47 +01:00
|
|
|
|
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-31 22:08:52 +01:00
|
|
|
|
|
|
|
|
|
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-01-31 22:08:52 +01:00
|
|
|
|
),
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
2018-01-31 22:08:52 +01:00
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-03-23 14:50:47 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-21 17:40:29 +02:00
|
|
|
|
if ($type instanceof TSingleLetter) {
|
|
|
|
|
$valid_offset_type = Type::getInt(false, 0);
|
|
|
|
|
} elseif ($type instanceof TLiteralString) {
|
2019-02-25 01:41:53 +01:00
|
|
|
|
if (!strlen($type->value)) {
|
|
|
|
|
$valid_offset_type = Type::getEmpty();
|
2019-03-12 17:58:04 +01:00
|
|
|
|
} elseif (strlen($type->value) < 10) {
|
2019-02-25 01:41:53 +01:00
|
|
|
|
$valid_offsets = [];
|
2018-08-21 17:40:29 +02:00
|
|
|
|
|
2019-02-25 01:41:53 +01:00
|
|
|
|
for ($i = -strlen($type->value), $l = strlen($type->value); $i < $l; $i++) {
|
|
|
|
|
$valid_offsets[] = new TLiteralInt($i);
|
|
|
|
|
}
|
2018-08-21 17:40:29 +02:00
|
|
|
|
|
2020-01-30 05:41:17 +01:00
|
|
|
|
if (!$valid_offsets) {
|
|
|
|
|
throw new \UnexpectedValueException('This is weird');
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-25 01:41:53 +01:00
|
|
|
|
$valid_offset_type = new Type\Union($valid_offsets);
|
2019-03-12 17:58:04 +01:00
|
|
|
|
} else {
|
|
|
|
|
$valid_offset_type = Type::getInt();
|
2019-02-25 01:41:53 +01:00
|
|
|
|
}
|
2018-08-21 17:40:29 +02:00
|
|
|
|
} 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,
|
2018-08-21 17:40:29 +02:00
|
|
|
|
$valid_offset_type,
|
2018-01-14 18:09:40 +01:00
|
|
|
|
true
|
|
|
|
|
)) {
|
2018-08-21 17:40:29 +02:00
|
|
|
|
$expected_offset_types[] = $valid_offset_type->getId();
|
2018-12-15 00:52:29 +01:00
|
|
|
|
|
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;
|
|
|
|
|
|
2018-12-15 00:52:29 +01:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-23 14:50:47 +01:00
|
|
|
|
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-31 22:08:52 +01:00
|
|
|
|
|
2018-04-16 22:03:04 +02:00
|
|
|
|
if ($type instanceof Type\Atomic\TFalse && $array_type->ignore_falsable_issues) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-31 18:45:47 +01:00
|
|
|
|
if ($type instanceof TNamedObject) {
|
2019-01-30 21:40:37 +01:00
|
|
|
|
if (strtolower($type->value) === 'simplexmlelement') {
|
2020-04-09 17:45:15 +02:00
|
|
|
|
$call_array_access_type = new Type\Union([new TNamedObject('SimpleXMLElement')]);
|
2019-01-31 18:45:47 +01:00
|
|
|
|
} elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$old_data_provider = $statements_analyzer->node_data;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
|
|
2019-01-30 21:40:37 +01:00
|
|
|
|
$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']);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 17:45:15 +02:00
|
|
|
|
if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->addSuppressedIssues(['MixedMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-30 21:40:37 +01:00
|
|
|
|
\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']);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 17:45:15 +02:00
|
|
|
|
if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->removeSuppressedIssues(['MixedMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$call_array_access_type = $statements_analyzer->node_data->getType(
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$fake_method_call
|
|
|
|
|
) ?: Type::getMixed();
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = $old_data_provider;
|
2019-02-27 03:24:30 +01:00
|
|
|
|
} else {
|
2019-04-10 23:25:25 +02:00
|
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
|
|
|
|
|
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 17:45:15 +02:00
|
|
|
|
if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->addSuppressedIssues(['MixedMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-31 18:45:47 +01:00
|
|
|
|
if ($in_assignment) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$old_node_data = $statements_analyzer->node_data;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
|
|
2019-04-10 23:25:25 +02:00
|
|
|
|
$fake_set_method_call = new PhpParser\Node\Expr\MethodCall(
|
2019-01-31 18:45:47 +01:00
|
|
|
|
$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()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
);
|
2019-04-10 23:25:25 +02:00
|
|
|
|
|
|
|
|
|
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$fake_set_method_call,
|
|
|
|
|
$context
|
|
|
|
|
);
|
2019-11-25 17:44:54 +01:00
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = $old_node_data;
|
2019-04-10 23:25:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($stmt->dim) {
|
2019-11-25 17:44:54 +01:00
|
|
|
|
$old_node_data = $statements_analyzer->node_data;
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
|
|
2019-04-10 23:25:25 +02:00
|
|
|
|
$fake_get_method_call = new PhpParser\Node\Expr\MethodCall(
|
2019-01-31 18:45:47 +01:00
|
|
|
|
$stmt->var,
|
|
|
|
|
new PhpParser\Node\Identifier('offsetGet', $stmt->var->getAttributes()),
|
|
|
|
|
[
|
2019-02-27 03:24:30 +01:00
|
|
|
|
new PhpParser\Node\Arg(
|
|
|
|
|
$stmt->dim
|
|
|
|
|
)
|
2019-01-31 18:45:47 +01:00
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
2019-04-10 23:25:25 +02:00
|
|
|
|
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$fake_get_method_call,
|
|
|
|
|
$context
|
|
|
|
|
);
|
2019-01-30 21:40:37 +01:00
|
|
|
|
|
2020-04-09 17:45:15 +02:00
|
|
|
|
$call_array_access_type = $statements_analyzer->node_data->getType($fake_get_method_call)
|
2019-11-25 17:44:54 +01:00
|
|
|
|
?: Type::getMixed();
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->node_data = $old_node_data;
|
2019-04-10 23:25:25 +02:00
|
|
|
|
} else {
|
2020-04-09 17:45:15 +02:00
|
|
|
|
$call_array_access_type = Type::getVoid();
|
2019-01-30 21:40:37 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-04-10 23:25:25 +02:00
|
|
|
|
$has_array_access = true;
|
|
|
|
|
|
2019-01-30 21:40:37 +01:00
|
|
|
|
if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
|
|
|
|
|
}
|
2020-04-09 17:45:15 +02:00
|
|
|
|
|
|
|
|
|
if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
|
|
|
|
|
$statements_analyzer->removeSuppressedIssues(['MixedMethodCall']);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$array_access_type) {
|
|
|
|
|
$array_access_type = $call_array_access_type;
|
|
|
|
|
} else {
|
|
|
|
|
$array_access_type = Type::combineUnionTypes(
|
|
|
|
|
$array_access_type,
|
|
|
|
|
$call_array_access_type
|
|
|
|
|
);
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
2019-02-19 17:42:24 +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
|
|
|
|
|
}
|
2020-04-04 17:15:13 +02:00
|
|
|
|
} elseif (!$context->inside_isset) {
|
2018-01-14 18:09:40 +01:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-08 19:18:55 +01:00
|
|
|
|
if ($offset_type->hasMixed()) {
|
2019-03-23 14:50:47 +01:00
|
|
|
|
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-31 22:08:52 +01:00
|
|
|
|
|
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
|
|
|
|
|
}
|
2018-01-31 22:08:52 +01:00
|
|
|
|
} else {
|
2019-03-23 14:50:47 +01:00
|
|
|
|
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-31 22:08:52 +01:00
|
|
|
|
|
2018-02-06 17:27:01 +01:00
|
|
|
|
if ($expected_offset_types) {
|
|
|
|
|
$invalid_offset_type = $expected_offset_types[0];
|
|
|
|
|
|
2018-05-05 23:30:18 +02:00
|
|
|
|
$used_offset = 'using a ' . $offset_type->getId() . ' offset';
|
2018-02-06 17:27:01 +01:00
|
|
|
|
|
|
|
|
|
if ($key_value !== null) {
|
|
|
|
|
$used_offset = 'using offset value of '
|
|
|
|
|
. (is_int($key_value) ? $key_value : '\'' . $key_value . '\'');
|
|
|
|
|
}
|
2018-01-31 22:08:52 +01:00
|
|
|
|
|
2019-01-30 21:40:37 +01:00
|
|
|
|
if ($has_valid_offset && $context->inside_isset) {
|
2018-12-15 00:52:29 +01:00
|
|
|
|
// do nothing
|
|
|
|
|
} elseif ($has_valid_offset) {
|
2019-08-19 00:06:41 +02:00
|
|
|
|
if (!$context->inside_unset) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyInvalidArrayOffset(
|
|
|
|
|
'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
|
|
|
|
|
. ', expecting ' . $invalid_offset_type,
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
2018-01-31 22:08:52 +01:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidArrayOffset(
|
2018-02-06 17:27:01 +01:00
|
|
|
|
'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-01-31 22:08:52 +01:00
|
|
|
|
),
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
2018-01-31 22:08:52 +01:00
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($array_access_type === null) {
|
2019-05-10 01:34:38 +02:00
|
|
|
|
// shouldn’t happen, but don’t crash
|
|
|
|
|
return Type::getMixed();
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-15 19:25:04 +01:00
|
|
|
|
if ($in_assignment) {
|
|
|
|
|
$array_type->bustCache();
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
|
return $array_access_type;
|
|
|
|
|
}
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
2019-10-04 17:08:08 +02:00
|
|
|
|
private static function checkLiteralIntArrayOffset(
|
2019-10-04 03:34:56 +02:00
|
|
|
|
Type\Union $offset_type,
|
|
|
|
|
Type\Union $expected_offset_type,
|
|
|
|
|
?string $array_var_id,
|
|
|
|
|
PhpParser\Node\Expr\ArrayDimFetch $stmt,
|
|
|
|
|
Context $context,
|
|
|
|
|
StatementsAnalyzer $statements_analyzer
|
|
|
|
|
) : void {
|
2019-10-04 17:08:08 +02:00
|
|
|
|
if ($context->inside_isset || $context->inside_unset) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($offset_type->hasLiteralInt()) {
|
|
|
|
|
$found_match = false;
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
|
2019-10-04 17:08:08 +02:00
|
|
|
|
if ($array_var_id
|
|
|
|
|
&& $offset_type_part instanceof TLiteralInt
|
|
|
|
|
&& isset(
|
|
|
|
|
$context->vars_in_scope[
|
|
|
|
|
$array_var_id . '[' . $offset_type_part->value . ']'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
&& !$context->vars_in_scope[
|
|
|
|
|
$array_var_id . '[' . $offset_type_part->value . ']'
|
|
|
|
|
]->possibly_undefined
|
|
|
|
|
) {
|
|
|
|
|
$found_match = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$found_match) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
2019-11-11 15:59:56 +01:00
|
|
|
|
new PossiblyUndefinedIntArrayOffset(
|
2019-10-04 17:08:08 +02:00
|
|
|
|
'Possibly undefined array offset \''
|
|
|
|
|
. $offset_type->getId() . '\' '
|
|
|
|
|
. 'is risky given expected type \''
|
|
|
|
|
. $expected_offset_type->getId() . '\'.'
|
|
|
|
|
. ' Consider using isset beforehand.',
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function checkLiteralStringArrayOffset(
|
|
|
|
|
Type\Union $offset_type,
|
|
|
|
|
Type\Union $expected_offset_type,
|
|
|
|
|
?string $array_var_id,
|
|
|
|
|
PhpParser\Node\Expr\ArrayDimFetch $stmt,
|
|
|
|
|
Context $context,
|
|
|
|
|
StatementsAnalyzer $statements_analyzer
|
|
|
|
|
) : void {
|
|
|
|
|
if ($context->inside_isset || $context->inside_unset) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($offset_type->hasLiteralString() && !$expected_offset_type->hasLiteralClassString()) {
|
2019-10-04 03:34:56 +02:00
|
|
|
|
$found_match = false;
|
|
|
|
|
|
2020-01-04 18:20:26 +01:00
|
|
|
|
foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
|
2019-10-04 03:34:56 +02:00
|
|
|
|
if ($array_var_id
|
|
|
|
|
&& $offset_type_part instanceof TLiteralString
|
|
|
|
|
&& isset(
|
|
|
|
|
$context->vars_in_scope[
|
|
|
|
|
$array_var_id . '[\'' . $offset_type_part->value . '\']'
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
&& !$context->vars_in_scope[
|
|
|
|
|
$array_var_id . '[\'' . $offset_type_part->value . '\']'
|
|
|
|
|
]->possibly_undefined
|
|
|
|
|
) {
|
|
|
|
|
$found_match = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$found_match) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
2019-11-11 15:59:56 +01:00
|
|
|
|
new PossiblyUndefinedStringArrayOffset(
|
2019-10-04 03:34:56 +02:00
|
|
|
|
'Possibly undefined array offset \''
|
|
|
|
|
. $offset_type->getId() . '\' '
|
|
|
|
|
. 'is risky given expected type \''
|
|
|
|
|
. $expected_offset_type->getId() . '\'.'
|
|
|
|
|
. ' Consider using isset beforehand.',
|
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
|
|
|
|
),
|
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-03 19:56:30 +02:00
|
|
|
|
/**
|
|
|
|
|
* @return Type\Union
|
|
|
|
|
*/
|
|
|
|
|
public static function replaceOffsetTypeWithInts(Type\Union $offset_type)
|
|
|
|
|
{
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$offset_types = $offset_type->getAtomicTypes();
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
2019-09-20 21:21:38 +02:00
|
|
|
|
$cloned = false;
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
2019-09-20 21:21:38 +02:00
|
|
|
|
foreach ($offset_types as $key => $offset_type_part) {
|
|
|
|
|
if ($offset_type_part instanceof Type\Atomic\TLiteralString) {
|
|
|
|
|
if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) {
|
|
|
|
|
if (!$cloned) {
|
|
|
|
|
$offset_type = clone $offset_type;
|
|
|
|
|
$cloned = true;
|
|
|
|
|
}
|
|
|
|
|
$offset_type->addType(new Type\Atomic\TLiteralInt((int) $offset_type_part->value));
|
|
|
|
|
$offset_type->removeType($key);
|
|
|
|
|
}
|
|
|
|
|
} elseif ($offset_type_part instanceof Type\Atomic\TBool) {
|
|
|
|
|
if (!$cloned) {
|
|
|
|
|
$offset_type = clone $offset_type;
|
|
|
|
|
$cloned = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($offset_type_part instanceof Type\Atomic\TFalse) {
|
2019-09-20 21:30:29 +02:00
|
|
|
|
if (!$offset_type->ignore_falsable_issues) {
|
|
|
|
|
$offset_type->addType(new Type\Atomic\TLiteralInt(0));
|
|
|
|
|
$offset_type->removeType($key);
|
|
|
|
|
}
|
2019-09-20 21:21:38 +02:00
|
|
|
|
} elseif ($offset_type_part instanceof Type\Atomic\TTrue) {
|
|
|
|
|
$offset_type->addType(new Type\Atomic\TLiteralInt(1));
|
|
|
|
|
$offset_type->removeType($key);
|
|
|
|
|
} else {
|
|
|
|
|
$offset_type->addType(new Type\Atomic\TLiteralInt(0));
|
|
|
|
|
$offset_type->addType(new Type\Atomic\TLiteralInt(1));
|
|
|
|
|
$offset_type->removeType($key);
|
|
|
|
|
}
|
2018-05-03 19:56:30 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $offset_type;
|
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
}
|