mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Fix issue with nested object-like array updating
This commit is contained in:
parent
1c25ca1a0c
commit
39baa00fd3
@ -40,6 +40,7 @@ use Psalm\Issue\UndefinedPropertyFetch;
|
||||
use Psalm\Issue\UndefinedThisPropertyFetch;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\ObjectLike;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TBool;
|
||||
@ -243,15 +244,15 @@ class FetchChecker
|
||||
if (isset($class_storage->pseudo_property_get_types['$' . $stmt->name])) {
|
||||
$stmt->inferredType = clone $class_storage->pseudo_property_get_types['$' . $stmt->name];
|
||||
continue;
|
||||
} else {
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
/*
|
||||
* If we have an explicit list of all allowed magic properties on the class, and we're
|
||||
* not in that list, fall through
|
||||
*/
|
||||
if (!$class_storage->sealed_properties) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
/*
|
||||
* If we have an explicit list of all allowed magic properties on the class, and we're
|
||||
* not in that list, fall through
|
||||
*/
|
||||
if (!$class_storage->sealed_properties) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -827,7 +828,7 @@ class FetchChecker
|
||||
if (!$keyed_assignment_type || $keyed_assignment_type->isEmpty()) {
|
||||
if (!$assignment_key_type->isMixed() && !$assignment_key_type->hasInt() && $assignment_key_value) {
|
||||
$keyed_assignment_type = new Type\Union([
|
||||
new Type\Atomic\ObjectLike([
|
||||
new ObjectLike([
|
||||
$assignment_key_value => $assignment_value_type,
|
||||
]),
|
||||
]);
|
||||
@ -887,14 +888,35 @@ class FetchChecker
|
||||
}
|
||||
|
||||
$type = $refined_type;
|
||||
} elseif ($type instanceof Type\Atomic\ObjectLike && $assignment_key_value) {
|
||||
if (isset($type->properties[$assignment_key_value])) {
|
||||
$type->properties[$assignment_key_value] = Type::combineUnionTypes(
|
||||
$type->properties[$assignment_key_value],
|
||||
$assignment_value_type
|
||||
);
|
||||
} elseif ($type instanceof ObjectLike) {
|
||||
if ($assignment_key_value) {
|
||||
if (isset($type->properties[$assignment_key_value])) {
|
||||
$type->properties[$assignment_key_value] = Type::combineUnionTypes(
|
||||
$type->properties[$assignment_key_value],
|
||||
$assignment_value_type
|
||||
);
|
||||
} else {
|
||||
$type->properties[$assignment_key_value] = $assignment_value_type;
|
||||
}
|
||||
} else {
|
||||
$type->properties[$assignment_key_value] = $assignment_value_type;
|
||||
$refined_type = self::refineArrayType(
|
||||
$statements_checker,
|
||||
$type,
|
||||
$assignment_key_type,
|
||||
$assignment_value_type,
|
||||
$var_id,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
);
|
||||
|
||||
if ($refined_type === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($refined_type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $refined_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -967,11 +989,11 @@ class FetchChecker
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type instanceof Type\Atomic\TArray || $type instanceof Type\Atomic\ObjectLike) {
|
||||
if ($type instanceof TArray || $type instanceof ObjectLike) {
|
||||
$value_index = null;
|
||||
$has_array_access = true;
|
||||
|
||||
if ($type instanceof Type\Atomic\TArray) {
|
||||
if ($type instanceof TArray) {
|
||||
// create a union type to pass back to the statement
|
||||
$value_index = count($type->type_params) - 1;
|
||||
|
||||
@ -1017,7 +1039,7 @@ class FetchChecker
|
||||
}
|
||||
|
||||
if ($array_var_id === $var_id) {
|
||||
if ($type instanceof Type\Atomic\ObjectLike ||
|
||||
if ($type instanceof ObjectLike ||
|
||||
(
|
||||
$type instanceof TArray &&
|
||||
!$used_key_type->hasInt() &&
|
||||
@ -1030,7 +1052,7 @@ class FetchChecker
|
||||
|
||||
if ($properties) {
|
||||
$assignment_type = new Type\Union([
|
||||
new Type\Atomic\ObjectLike($properties),
|
||||
new ObjectLike($properties),
|
||||
]);
|
||||
} else {
|
||||
if (!$keyed_assignment_type) {
|
||||
@ -1143,7 +1165,7 @@ class FetchChecker
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
}
|
||||
}
|
||||
} elseif ($type instanceof Type\Atomic\ObjectLike) {
|
||||
} elseif ($type instanceof ObjectLike) {
|
||||
if ($string_key_value || $int_key_value !== null) {
|
||||
if ($string_key_value && isset($type->properties[$string_key_value])) {
|
||||
$has_valid_offset = true;
|
||||
@ -1366,8 +1388,9 @@ class FetchChecker
|
||||
|
||||
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
||||
|
||||
if (!$type instanceof TArray &&
|
||||
(!$type instanceof TNamedObject
|
||||
if (!$type instanceof TArray
|
||||
&& !$type instanceof ObjectLike
|
||||
&& (!$type instanceof TNamedObject
|
||||
|| !ClassChecker::classImplements($project_checker, $type->value, 'ArrayAccess'))
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
@ -1384,6 +1407,13 @@ class FetchChecker
|
||||
return $type;
|
||||
}
|
||||
|
||||
if ($type instanceof ObjectLike) {
|
||||
$type = new TArray([
|
||||
Type::getString(),
|
||||
$type->getGenericTypeParam(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($type instanceof Type\Atomic\Generic && $type instanceof TArray) {
|
||||
if ($type->type_params[1]->isEmpty()) {
|
||||
$type->type_params[0] = $assignment_key_type;
|
||||
|
@ -566,6 +566,34 @@ abstract class Type
|
||||
|
||||
foreach ($combination->type_params as $generic_type => $generic_type_params) {
|
||||
if ($generic_type === 'array') {
|
||||
if ($combination->objectlike_entries) {
|
||||
$object_like_generic_type = null;
|
||||
|
||||
foreach ($combination->objectlike_entries as $property_key => $property_type) {
|
||||
if ($object_like_generic_type) {
|
||||
$object_like_generic_type = Type::combineUnionTypes(
|
||||
$property_type,
|
||||
$object_like_generic_type
|
||||
);
|
||||
} else {
|
||||
$object_like_generic_type = $property_type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$object_like_generic_type) {
|
||||
throw new \InvalidArgumentException('Cannot be null');
|
||||
}
|
||||
|
||||
$generic_type_params[0] = Type::combineUnionTypes(
|
||||
$generic_type_params[0],
|
||||
Type::getString()
|
||||
);
|
||||
$generic_type_params[1] = Type::combineUnionTypes(
|
||||
$generic_type_params[1],
|
||||
$object_like_generic_type
|
||||
);
|
||||
}
|
||||
|
||||
$new_types[] = new TArray($generic_type_params);
|
||||
} elseif (!isset($combination->value_types[$generic_type])) {
|
||||
$new_types[] = new TGenericObject($generic_type, $generic_type_params);
|
||||
|
@ -101,7 +101,7 @@ class ArrayAccessTest extends TestCase
|
||||
takesBool($b[2]);
|
||||
}',
|
||||
],
|
||||
'updateObjectLike' => [
|
||||
'updateStringIntKey' => [
|
||||
'<?php
|
||||
$string = "c";
|
||||
$int = 5;
|
||||
@ -138,6 +138,94 @@ class ArrayAccessTest extends TestCase
|
||||
'$e' => 'array<int|string, int>',
|
||||
],
|
||||
],
|
||||
'updateStringIntKeyWithIntRootAndNumberOffset' => [
|
||||
'<?php
|
||||
$string = "c";
|
||||
$int = 5;
|
||||
|
||||
$a = [];
|
||||
|
||||
$a[0]["a"] = 5;
|
||||
$a[0][0] = 3;',
|
||||
'assertions' => [
|
||||
'$a' => 'array<int, array<string|int, int>>',
|
||||
],
|
||||
],
|
||||
'updateStringIntKeyWithIntRoot' => [
|
||||
'<?php
|
||||
$string = "c";
|
||||
$int = 5;
|
||||
|
||||
$b = [];
|
||||
|
||||
$b[0][$string] = 5;
|
||||
$b[0][0] = 3;
|
||||
|
||||
$c = [];
|
||||
|
||||
$c[0][0] = 3;
|
||||
$c[0][$string] = 5;
|
||||
|
||||
$d = [];
|
||||
|
||||
$d[0][$int] = 3;
|
||||
$d[0]["a"] = 5;
|
||||
|
||||
$e = [];
|
||||
|
||||
$e[0][$int] = 3;
|
||||
$e[0][$string] = 5;',
|
||||
'assertions' => [
|
||||
'$b' => 'array<int, array<string|int, int>>',
|
||||
'$c' => 'array<int, array<int|string, int>>',
|
||||
'$d' => 'array<int, array<int|string, int>>',
|
||||
'$e' => 'array<int, array<int|string, int>>',
|
||||
],
|
||||
],
|
||||
'updateStringIntKeyWithObjectLikeRootAndNumberOffset' => [
|
||||
'<?php
|
||||
$string = "c";
|
||||
$int = 5;
|
||||
|
||||
$a = [];
|
||||
|
||||
$a["root"]["a"] = 5;
|
||||
$a["root"][0] = 3;',
|
||||
'assertions' => [
|
||||
'$a' => 'array{root:array<string|int, int>}',
|
||||
],
|
||||
],
|
||||
'updateStringIntKeyWithObjectLikeRoot' => [
|
||||
'<?php
|
||||
$string = "c";
|
||||
$int = 5;
|
||||
|
||||
$b = [];
|
||||
|
||||
$b["root"][$string] = 5;
|
||||
$b["root"][0] = 3;
|
||||
|
||||
$c = [];
|
||||
|
||||
$c["root"][0] = 3;
|
||||
$c["root"][$string] = 5;
|
||||
|
||||
$d = [];
|
||||
|
||||
$d["root"][$int] = 3;
|
||||
$d["root"]["a"] = 5;
|
||||
|
||||
$e = [];
|
||||
|
||||
$e["root"][$int] = 3;
|
||||
$e["root"][$string] = 5;',
|
||||
'assertions' => [
|
||||
'$b' => 'array{root:array<string|int, int>}',
|
||||
'$c' => 'array{root:array<int|string, int>}',
|
||||
'$d' => 'array{root:array<int|string, int>}',
|
||||
'$e' => 'array{root:array<int|string, int>}',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -154,6 +154,34 @@ class TypeCombinationTest extends TestCase
|
||||
'array{a:string,b:string}',
|
||||
],
|
||||
],
|
||||
'combineObjectTypeWithIntKeyedArray' => [
|
||||
'array<int|string, string|int>',
|
||||
[
|
||||
'array{a:int}',
|
||||
'array<int, string>',
|
||||
],
|
||||
],
|
||||
'combineNestedObjectTypeWithObjectLikeIntKeyedArray' => [
|
||||
'array{a:array<int|string, string|int>}',
|
||||
[
|
||||
'array{a:array{a:int}}',
|
||||
'array{a:array<int, string>}',
|
||||
],
|
||||
],
|
||||
'combineIntKeyedObjectTypeWithNestedIntKeyedArray' => [
|
||||
'array<int, array<int|string, string|int>>',
|
||||
[
|
||||
'array<int, array{a:int}>',
|
||||
'array<int, array<int, string>>',
|
||||
],
|
||||
],
|
||||
'combineNestedObjectTypeWithNestedIntKeyedArray' => [
|
||||
'array<int|string, array<int|string, string|int>>',
|
||||
[
|
||||
'array{a:array{a:int}}',
|
||||
'array<int, array<int, string>>',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user