1
0
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:
orklah 2023-12-03 21:21:41 +01:00 committed by GitHub
commit c620f6e80d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 207 additions and 116 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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(

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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;',

View File

@ -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;
}
}',
],