mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 12:24:49 +01:00
Merge pull request #10439 from nicelocal/fix_literal_union_key
Use keyed arrays when assigning literal union keys & assertion fixes
This commit is contained in:
commit
c620f6e80d
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="5.x-dev@18a6c0b6e9aade82a2f3cc36e3a644ba70eaf539">
|
||||
<files psalm-version="5.x-dev@5acde045f126440ded206b406cf37b649ede84fc">
|
||||
<file src="examples/TemplateChecker.php">
|
||||
<PossiblyUndefinedIntArrayOffset>
|
||||
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
|
||||
@ -216,13 +216,15 @@
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php">
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code>$token_list[$iter]</code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
<PossiblyUndefinedIntArrayOffset>
|
||||
<code>$token_list[$iter]</code>
|
||||
<code>$token_list[$iter]</code>
|
||||
<code>$token_list[$iter]</code>
|
||||
<code>$token_list[$iter]</code>
|
||||
<code>$token_list[0]</code>
|
||||
<code>$token_list[1]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Analyzer/StatementsAnalyzer.php">
|
||||
@ -230,6 +232,11 @@
|
||||
<code><![CDATA[$stmt->expr->getArgs()[0]]]></code>
|
||||
</PossiblyUndefinedArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Cli/Psalm.php">
|
||||
<PossiblyInvalidArgument>
|
||||
<code><![CDATA[$options['f'] ?? null]]></code>
|
||||
</PossiblyInvalidArgument>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Cli/Refactor.php">
|
||||
<PossiblyUndefinedIntArrayOffset>
|
||||
<code>$identifier_name</code>
|
||||
|
@ -34,14 +34,11 @@ use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -272,20 +269,6 @@ final class IfAnalyzer
|
||||
array_keys($if_scope->negated_types),
|
||||
);
|
||||
|
||||
$extra_vars_to_update = [];
|
||||
|
||||
// if there's an object-like array in there, we also need to update the root array variable
|
||||
foreach ($vars_to_update as $var_id) {
|
||||
$bracked_pos = strpos($var_id, '[');
|
||||
if ($bracked_pos !== false) {
|
||||
$extra_vars_to_update[] = substr($var_id, 0, $bracked_pos);
|
||||
}
|
||||
}
|
||||
|
||||
if ($extra_vars_to_update) {
|
||||
$vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update));
|
||||
}
|
||||
|
||||
$outer_context->update(
|
||||
$old_if_context,
|
||||
$if_context,
|
||||
|
@ -349,29 +349,23 @@ final class ArrayAssignmentAnalyzer
|
||||
}
|
||||
|
||||
if (!$has_matching_objectlike_property && !$has_matching_string) {
|
||||
if (count($key_values) === 1) {
|
||||
$key_value = $key_values[0];
|
||||
|
||||
$object_like = new TKeyedArray(
|
||||
[$key_value->value => $current_type],
|
||||
$key_value instanceof TLiteralClassString
|
||||
? [$key_value->value => true]
|
||||
: null,
|
||||
);
|
||||
|
||||
$array_assignment_type = new Union([
|
||||
$object_like,
|
||||
]);
|
||||
} else {
|
||||
$array_assignment_literals = $key_values;
|
||||
|
||||
$array_assignment_type = new Union([
|
||||
new TNonEmptyArray([
|
||||
new Union($array_assignment_literals),
|
||||
$current_type,
|
||||
]),
|
||||
]);
|
||||
$properties = [];
|
||||
$classStrings = [];
|
||||
$current_type = $current_type->setPossiblyUndefined(count($key_values) > 1);
|
||||
foreach ($key_values as $key_value) {
|
||||
$properties[$key_value->value] = $current_type;
|
||||
if ($key_value instanceof TLiteralClassString) {
|
||||
$classStrings[$key_value->value] = true;
|
||||
}
|
||||
}
|
||||
$object_like = new TKeyedArray(
|
||||
$properties,
|
||||
$classStrings ?: null,
|
||||
);
|
||||
|
||||
$array_assignment_type = new Union([
|
||||
$object_like,
|
||||
]);
|
||||
|
||||
return Type::combineUnionTypes(
|
||||
$child_stmt_type,
|
||||
|
@ -315,11 +315,9 @@ final class LanguageServer
|
||||
|
||||
$path_to_config = CliUtils::getPathToConfig($options);
|
||||
|
||||
if (isset($options['tcp'])) {
|
||||
if (!is_string($options['tcp'])) {
|
||||
fwrite(STDERR, 'tcp url should be a string' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
if (isset($options['tcp']) && !is_string($options['tcp'])) {
|
||||
fwrite(STDERR, 'tcp url should be a string' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$config = CliUtils::initializeConfig(
|
||||
|
@ -20,6 +20,7 @@ use Psalm\Issue\TypeDoesNotContainNull;
|
||||
use Psalm\Issue\TypeDoesNotContainType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Storage\Assertion;
|
||||
use Psalm\Storage\Assertion\ArrayKeyDoesNotExist;
|
||||
use Psalm\Storage\Assertion\ArrayKeyExists;
|
||||
use Psalm\Storage\Assertion\Empty_;
|
||||
use Psalm\Storage\Assertion\Falsy;
|
||||
@ -45,6 +46,8 @@ use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNever;
|
||||
@ -197,7 +200,9 @@ class Reconciler
|
||||
$is_equality = $is_equality
|
||||
&& $new_type_part_part instanceof IsIdentical;
|
||||
|
||||
$has_inverted_isset = $has_inverted_isset || $new_type_part_part instanceof IsNotIsset;
|
||||
$has_inverted_isset = $has_inverted_isset
|
||||
|| $new_type_part_part instanceof IsNotIsset
|
||||
|| $new_type_part_part instanceof ArrayKeyDoesNotExist;
|
||||
|
||||
$has_count_check = $has_count_check
|
||||
|| $new_type_part_part instanceof NonEmptyCountable;
|
||||
@ -1105,88 +1110,106 @@ class Reconciler
|
||||
throw new UnexpectedValueException('Not expecting null array key');
|
||||
}
|
||||
|
||||
$array_key_offsets = [];
|
||||
if ($array_key[0] === '$') {
|
||||
return;
|
||||
if (!isset($existing_types[$array_key])) {
|
||||
return;
|
||||
}
|
||||
$t = $existing_types[$array_key];
|
||||
foreach ($t->getAtomicTypes() as $lit) {
|
||||
if ($lit instanceof TLiteralInt || $lit instanceof TLiteralString) {
|
||||
$array_key_offsets []= $lit->value;
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"'
|
||||
? substr($array_key, 1, -1)
|
||||
: $array_key
|
||||
;
|
||||
}
|
||||
|
||||
$array_key_offset = $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key;
|
||||
|
||||
$base_key = implode($key_parts);
|
||||
|
||||
if (isset($existing_types[$base_key]) && $array_key_offset !== false) {
|
||||
foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) {
|
||||
if ($base_atomic_type instanceof TList) {
|
||||
$base_atomic_type = $base_atomic_type->getKeyedArray();
|
||||
}
|
||||
if ($base_atomic_type instanceof TKeyedArray
|
||||
$result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1);
|
||||
|
||||
foreach ($array_key_offsets as $array_key_offset) {
|
||||
if (isset($existing_types[$base_key]) && $array_key_offset !== false) {
|
||||
foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) {
|
||||
if ($base_atomic_type instanceof TList) {
|
||||
$base_atomic_type = $base_atomic_type->getKeyedArray();
|
||||
}
|
||||
if ($base_atomic_type instanceof TKeyedArray
|
||||
|| ($base_atomic_type instanceof TArray
|
||||
&& !$base_atomic_type->isEmptyArray())
|
||||
|| $base_atomic_type instanceof TClassStringMap
|
||||
) {
|
||||
$new_base_type = $existing_types[$base_key];
|
||||
) {
|
||||
$new_base_type = $existing_types[$base_key];
|
||||
|
||||
if ($base_atomic_type instanceof TArray) {
|
||||
$fallback_key_type = $base_atomic_type->type_params[0];
|
||||
$fallback_value_type = $base_atomic_type->type_params[1];
|
||||
if ($base_atomic_type instanceof TArray) {
|
||||
$fallback_key_type = $base_atomic_type->type_params[0];
|
||||
$fallback_value_type = $base_atomic_type->type_params[1];
|
||||
|
||||
$base_atomic_type = new TKeyedArray(
|
||||
[
|
||||
$base_atomic_type = new TKeyedArray(
|
||||
[
|
||||
$array_key_offset => $result_type,
|
||||
],
|
||||
null,
|
||||
$fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type],
|
||||
);
|
||||
} elseif ($base_atomic_type instanceof TClassStringMap) {
|
||||
// do nothing
|
||||
} else {
|
||||
$properties = $base_atomic_type->properties;
|
||||
$properties[$array_key_offset] = $result_type;
|
||||
if ($base_atomic_type->is_list
|
||||
],
|
||||
null,
|
||||
$fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type],
|
||||
);
|
||||
} elseif ($base_atomic_type instanceof TClassStringMap) {
|
||||
// do nothing
|
||||
} else {
|
||||
$properties = $base_atomic_type->properties;
|
||||
$properties[$array_key_offset] = $result_type;
|
||||
if ($base_atomic_type->is_list
|
||||
&& (!is_numeric($array_key_offset)
|
||||
|| ($array_key_offset
|
||||
&& !isset($properties[$array_key_offset-1])
|
||||
)
|
||||
)
|
||||
) {
|
||||
if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) {
|
||||
$fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined(
|
||||
$result_type->isNever(),
|
||||
);
|
||||
for ($x = 0; $x < $array_key_offset; $x++) {
|
||||
$properties[$x] ??= $fallback;
|
||||
) {
|
||||
if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) {
|
||||
$fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined(
|
||||
$result_type->isNever(),
|
||||
);
|
||||
for ($x = 0; $x < $array_key_offset; $x++) {
|
||||
$properties[$x] ??= $fallback;
|
||||
}
|
||||
ksort($properties);
|
||||
$base_atomic_type = $base_atomic_type->setProperties($properties);
|
||||
} else {
|
||||
// This should actually be a paradox
|
||||
$base_atomic_type = new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$base_atomic_type->fallback_params,
|
||||
false,
|
||||
$base_atomic_type->from_docblock,
|
||||
);
|
||||
}
|
||||
ksort($properties);
|
||||
$base_atomic_type = $base_atomic_type->setProperties($properties);
|
||||
} else {
|
||||
// This should actually be a paradox
|
||||
$base_atomic_type = new TKeyedArray(
|
||||
$properties,
|
||||
null,
|
||||
$base_atomic_type->fallback_params,
|
||||
false,
|
||||
$base_atomic_type->from_docblock,
|
||||
);
|
||||
$base_atomic_type = $base_atomic_type->setProperties($properties);
|
||||
}
|
||||
} else {
|
||||
$base_atomic_type = $base_atomic_type->setProperties($properties);
|
||||
}
|
||||
|
||||
$new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze();
|
||||
|
||||
$changed_var_ids[$base_key . '[' . $array_key . ']'] = true;
|
||||
|
||||
if ($key_parts[count($key_parts) - 1] === ']') {
|
||||
self::adjustTKeyedArrayType(
|
||||
$key_parts,
|
||||
$existing_types,
|
||||
$changed_var_ids,
|
||||
$new_base_type,
|
||||
);
|
||||
}
|
||||
|
||||
$existing_types[$base_key] = $new_base_type;
|
||||
break;
|
||||
}
|
||||
|
||||
$new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze();
|
||||
|
||||
$changed_var_ids[$base_key . '[' . $array_key . ']'] = true;
|
||||
|
||||
if ($key_parts[count($key_parts) - 1] === ']') {
|
||||
self::adjustTKeyedArrayType(
|
||||
$key_parts,
|
||||
$existing_types,
|
||||
$changed_var_ids,
|
||||
$new_base_type,
|
||||
);
|
||||
}
|
||||
|
||||
$existing_types[$base_key] = $new_base_type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,27 @@ class ArrayAssignmentTest extends TestCase
|
||||
public function providerValidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'assignUnionOfLiterals' => [
|
||||
'code' => '<?php
|
||||
$result = [];
|
||||
|
||||
foreach (["a", "b"] as $k) {
|
||||
$result[$k] = true;
|
||||
}
|
||||
|
||||
$resultOpt = [];
|
||||
|
||||
foreach (["a", "b"] as $k) {
|
||||
if (random_int(0, 1)) {
|
||||
continue;
|
||||
}
|
||||
$resultOpt[$k] = true;
|
||||
}',
|
||||
'assertions' => [
|
||||
'$result===' => 'array{a: true, b: true}',
|
||||
'$resultOpt===' => 'array{a?: true, b?: true}',
|
||||
],
|
||||
],
|
||||
'genericArrayCreationWithSingleIntValue' => [
|
||||
'code' => '<?php
|
||||
$out = [];
|
||||
@ -192,7 +213,7 @@ class ArrayAssignmentTest extends TestCase
|
||||
'assertions' => [
|
||||
'$foo' => 'array{0: string, 1: string, 2: string}',
|
||||
'$bar' => 'list{int, int, int}',
|
||||
'$bat' => 'non-empty-array<string, int>',
|
||||
'$bat' => 'array{a: int, b: int, c: int}',
|
||||
],
|
||||
],
|
||||
'implicitStringArrayCreation' => [
|
||||
@ -979,6 +1000,7 @@ class ArrayAssignmentTest extends TestCase
|
||||
$a = [];
|
||||
|
||||
foreach (["one", "two", "three"] as $key) {
|
||||
$a[$key] ??= 0;
|
||||
$a[$key] += rand(0, 10);
|
||||
}
|
||||
|
||||
|
@ -1027,9 +1027,7 @@ class ForeachTest extends TestCase
|
||||
$arr = [];
|
||||
|
||||
foreach ([1, 2, 3] as $i) {
|
||||
if (!isset($arr[$i]["a"])) {
|
||||
$arr[$i]["a"] = 0;
|
||||
}
|
||||
$arr[$i]["a"] ??= 0;
|
||||
|
||||
$arr[$i]["a"] += 5;
|
||||
}
|
||||
|
@ -46,6 +46,28 @@ class ArrayKeyExistsTest extends TestCase
|
||||
echo $a["a"];
|
||||
echo $a["b"];
|
||||
}',
|
||||
],
|
||||
'arrayKeyExistsNegation' => [
|
||||
'code' => '<?php
|
||||
function getMethodName(array $data = []): void {
|
||||
if (\array_key_exists("custom_name", $data) && $data["custom_name"] !== null) {
|
||||
}
|
||||
/** @psalm-check-type-exact $data = array<array-key, mixed> */
|
||||
}
|
||||
',
|
||||
],
|
||||
'arrayKeyExistsNoSideEffects' => [
|
||||
'code' => '<?php
|
||||
function getMethodName(array $ddata = []): void {
|
||||
if (\array_key_exists("redirect", $ddata)) {
|
||||
return;
|
||||
}
|
||||
if (random_int(0, 1)) {
|
||||
$ddata["type"] = "test";
|
||||
}
|
||||
/** @psalm-check-type-exact $ddata = array<array-key, mixed> */
|
||||
}
|
||||
',
|
||||
],
|
||||
'arrayKeyExistsTwice' => [
|
||||
'code' => '<?php
|
||||
|
@ -41,6 +41,50 @@ class IssetTest extends TestCase
|
||||
'assertions' => [],
|
||||
'ignored_issues' => ['MixedArrayAccess'],
|
||||
],
|
||||
'issetWithArrayAssignment' => [
|
||||
'code'=> '<?php
|
||||
|
||||
/**
|
||||
* @param array{0?: 0} $arr
|
||||
* @param 0 $i
|
||||
* @return array{0: 0|1}
|
||||
*/
|
||||
function t2(array $arr, int $i): array {
|
||||
if (!isset($arr[$i])) {
|
||||
$arr[$i] = 1;
|
||||
}
|
||||
return $arr;
|
||||
}',
|
||||
],
|
||||
'issetWithArrayAssignment2' => [
|
||||
'code'=> '<?php
|
||||
|
||||
/**
|
||||
* @param array{0?: 0, 1?: 0} $arr
|
||||
* @param 0|1 $i
|
||||
* @return array{0?: 0|1, 1?: 0|1}
|
||||
*/
|
||||
function t2(array $arr, int $i): array {
|
||||
if (!isset($arr[$i])) {
|
||||
$arr[$i] = 1;
|
||||
}
|
||||
return $arr;
|
||||
}',
|
||||
],
|
||||
'issetWithArrayAssignmentSubVar' => [
|
||||
'code'=> '<?php
|
||||
|
||||
/**
|
||||
* @param array{0?: 0, v: 0} $arr
|
||||
* @return array{0: 0|1, v: 0}
|
||||
*/
|
||||
function t2(array $arr): array {
|
||||
if (!isset($arr[$arr["v"]])) {
|
||||
$arr[$arr["v"]] = 1;
|
||||
}
|
||||
return $arr;
|
||||
}',
|
||||
],
|
||||
'isset' => [
|
||||
'code' => '<?php
|
||||
$a = isset($b) ? $b : null;',
|
||||
|
@ -287,12 +287,12 @@ class TypeAlgebraTest extends TestCase
|
||||
$arr = [];
|
||||
|
||||
foreach ([0, 1, 2, 3] as $i) {
|
||||
$a = rand(0, 1) ? 5 : "010";
|
||||
$a = (int) (rand(0, 1) ? 5 : "010");
|
||||
|
||||
if (!isset($arr[(int) $a])) {
|
||||
$arr[(int) $a] = 5;
|
||||
if (!isset($arr[$a])) {
|
||||
$arr[$a] = 5;
|
||||
} else {
|
||||
$arr[(int) $a] += 4;
|
||||
$arr[$a] += 4;
|
||||
}
|
||||
}',
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user