1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 03:17:02 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php

181 lines
6.0 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;
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\CodeLocation;
2018-01-14 18:09:40 +01:00
use Psalm\Context;
use Psalm\Issue\DuplicateArrayKey;
use Psalm\IssueBuffer;
2018-01-14 18:09:40 +01:00
use Psalm\Type;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TString;
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class ArrayAnalyzer
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\Array_ $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\Array_ $stmt,
Context $context
) {
// if the array is empty, this special type allows us to match any other array type against it
if (empty($stmt->items)) {
$stmt->inferredType = Type::getEmptyArray();
return null;
}
$item_key_type = null;
$item_value_type = null;
$property_types = [];
$class_strings = [];
2018-01-14 18:09:40 +01:00
$can_create_objectlike = true;
$array_keys = [];
$int_offset_diff = 0;
/** @var int $int_offset */
2018-01-14 18:09:40 +01:00
foreach ($stmt->items as $int_offset => $item) {
if ($item === null) {
continue;
}
$item_key_value = null;
2018-01-14 18:09:40 +01:00
if ($item->key) {
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $item->key, $context) === false) {
2018-01-14 18:09:40 +01:00
return false;
}
if (isset($item->key->inferredType)) {
$key_type = $item->key->inferredType;
if ($item->key instanceof PhpParser\Node\Scalar\String_
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
) {
$key_type = Type::getInt(false, (int) $item->key->value);
}
2018-01-14 18:09:40 +01:00
if ($item_key_type) {
$item_key_type = Type::combineUnionTypes($key_type, $item_key_type, false, true, 30);
2018-01-14 18:09:40 +01:00
} else {
$item_key_type = $key_type;
2018-01-14 18:09:40 +01:00
}
if ($item->key->inferredType->isSingleStringLiteral()) {
$item_key_literal_type = $item->key->inferredType->getSingleStringLiteral();
$item_key_value = $item_key_literal_type->value;
if ($item_key_literal_type instanceof Type\Atomic\TLiteralClassString) {
$class_strings[$item_key_value] = true;
}
} elseif ($item->key->inferredType->isSingleIntLiteral()) {
$item_key_value = $item->key->inferredType->getSingleIntLiteral()->value;
if ($item_key_value > $int_offset + $int_offset_diff) {
$int_offset_diff = $item_key_value - ($int_offset + $int_offset_diff);
}
}
2018-01-14 18:09:40 +01:00
}
} else {
$item_key_value = $int_offset + $int_offset_diff;
2018-01-14 18:09:40 +01:00
$item_key_type = Type::getInt();
}
if ($item_key_value !== null) {
if (isset($array_keys[$item_key_value])) {
if (IssueBuffer::accepts(
new DuplicateArrayKey(
'Key \'' . $item_key_value . '\' already exists on array',
2018-11-11 18:01:14 +01:00
new CodeLocation($statements_analyzer->getSource(), $item)
),
2018-11-11 18:01:14 +01:00
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
$array_keys[$item_key_value] = true;
}
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $item->value, $context) === false) {
return false;
}
if ($item_value_type && $item_value_type->hasMixed() && !$can_create_objectlike) {
2018-01-14 18:09:40 +01:00
continue;
}
if (isset($item->value->inferredType)) {
if ($item_key_value !== null && count($property_types) <= 50) {
$property_types[$item_key_value] = $item->value->inferredType;
2018-01-14 18:09:40 +01:00
} else {
$can_create_objectlike = false;
}
if ($item_value_type) {
$item_value_type = Type::combineUnionTypes(
$item->value->inferredType,
clone $item_value_type,
false,
true,
30
);
2018-01-14 18:09:40 +01:00
} else {
$item_value_type = $item->value->inferredType;
}
} else {
$item_value_type = Type::getMixed();
if ($item_key_value !== null && count($property_types) <= 50) {
$property_types[$item_key_value] = $item_value_type;
2018-01-14 18:09:40 +01:00
} else {
$can_create_objectlike = false;
}
}
}
// if this array looks like an object-like array, let's return that instead
if ($item_value_type
&& $item_key_type
&& ($item_key_type->hasString() || $item_key_type->hasInt())
&& $can_create_objectlike
) {
$object_like = new Type\Atomic\ObjectLike($property_types, $class_strings);
$object_like->sealed = true;
$stmt->inferredType = new Type\Union([$object_like]);
2018-01-14 18:09:40 +01:00
return null;
}
2018-11-09 16:56:27 +01:00
$array_type = new Type\Atomic\TNonEmptyArray([
$item_key_type ?: new Type\Union([new TInt, new TString]),
$item_value_type ?: Type::getMixed(),
]);
$array_type->count = count($stmt->items);
2018-01-14 18:09:40 +01:00
$stmt->inferredType = new Type\Union([
$array_type,
2018-01-14 18:09:40 +01:00
]);
return null;
}
}