1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 19:07:00 +01:00
psalm/src/Psalm/Checker/Statements/Expression/ArrayChecker.php
Matthew Brown b0733254bb
Use individual type objects for each string (#757)
* Experiment with individual types for each string

* Fix bunch of errors

* Fix a few more issues

* Fix a whole bunch of bugs

* Fix most remaining bugs

* Fix isset warnings

* Fix psalm errors in psalm

* Limit big string size

* Fix falsiness of ints

* Fix issue with type widening, allowing value set in nested if to be altered

* Don’t complain if type is mixed

* Add skipped-for-now test

* Add specific test to address issue
2018-05-18 11:02:50 -04:00

168 lines
5.5 KiB
PHP

<?php
namespace Psalm\Checker\Statements\Expression;
use PhpParser;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\DuplicateArrayKey;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TString;
class ArrayChecker
{
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr\Array_ $stmt
* @param Context $context
*
* @return false|null
*/
public static function analyze(
StatementsChecker $statements_checker,
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 = [];
$can_create_objectlike = true;
$array_keys = [];
/** @var int $int_offset */
foreach ($stmt->items as $int_offset => $item) {
if ($item === null) {
continue;
}
$item_key_value = null;
if ($item->key) {
if (ExpressionChecker::analyze($statements_checker, $item->key, $context) === false) {
return false;
}
if ($item->key instanceof PhpParser\Node\Scalar\String_
|| $item->key instanceof PhpParser\Node\Scalar\LNumber
) {
$item_key_value = $item->key->value;
}
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);
}
if ($item_key_type) {
$item_key_type = Type::combineUnionTypes($key_type, $item_key_type);
} else {
$item_key_type = $key_type;
}
if ($item_key_value === null) {
if ($item->key->inferredType->isSingleStringLiteral()) {
$item_key_value = $item->key->inferredType->getSingleStringLiteral();
} elseif ($item->key->inferredType->isSingleIntLiteral()) {
$item_key_value = $item->key->inferredType->getSingleIntLiteral();
}
}
}
} else {
$item_key_value = $int_offset;
$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',
new CodeLocation($statements_checker->getSource(), $item)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
$array_keys[$item_key_value] = true;
}
if (ExpressionChecker::analyze($statements_checker, $item->value, $context) === false) {
return false;
}
if ($item_value_type && $item_value_type->isMixed() && !$can_create_objectlike) {
continue;
}
if (isset($item->value->inferredType)) {
if ($item_key_value !== null) {
$property_types[$item_key_value] = $item->value->inferredType;
} else {
$can_create_objectlike = false;
}
if ($item_value_type) {
$item_value_type = Type::combineUnionTypes($item->value->inferredType, clone $item_value_type);
} else {
$item_value_type = $item->value->inferredType;
}
} else {
$item_value_type = Type::getMixed();
if ($item_key_value !== null) {
$property_types[$item_key_value] = $item_value_type;
} 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);
$object_like->sealed = true;
$stmt->inferredType = new Type\Union([$object_like]);
return null;
}
$array_type = new Type\Atomic\TArray([
$item_key_type ?: new Type\Union([new TInt, new TString]),
$item_value_type ?: Type::getMixed(),
]);
$array_type->count = count($stmt->items);
$stmt->inferredType = new Type\Union([
$array_type,
]);
return null;
}
}