diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 84bbae63a53..71cc2ef6324 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -216,13 +216,15 @@ + + $token_list[$iter] + $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[0] - $token_list[1] @@ -230,6 +232,11 @@ expr->getArgs()[0]]]> + + + + + $identifier_name diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index ad95cce30d5..2a2be74606f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -34,14 +34,11 @@ 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 @@ public static function analyze( 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, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index f00d81a48dd..576fe45a011 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -349,29 +349,23 @@ private static function updateTypeWithKeyValues( } 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, diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 07d5e6f93e8..1dc16fbe5bf 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -315,11 +315,9 @@ static function (string $arg) use ($valid_long_options): void { $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( diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1ae92b40571..f5e288639cf 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -20,6 +20,7 @@ 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\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 @@ public static function reconcileKeyedTypes( $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 @@ private static function adjustTKeyedArrayType( 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(); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); - $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; + $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, - ); - } + 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; + $existing_types[$base_key] = $new_base_type; + break; + } } } } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 38f98d70d60..364b6def325 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -34,6 +34,27 @@ public function testConditionalAssignment(): void public function providerValidCodeParse(): iterable { return [ + 'assignUnionOfLiterals' => [ + 'code' => ' [ + '$result===' => 'array{a: true, b: true}', + '$resultOpt===' => 'array{a?: true, b?: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', - '$bat' => 'non-empty-array', + '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ @@ -979,6 +1000,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index fbbb6e445f9..d7b56e5fa48 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,9 +1027,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 3ee780251c1..e44bfa55464 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -46,6 +46,28 @@ function three(array $a): void { echo $a["a"]; echo $a["b"]; }', + ], + 'arrayKeyExistsNegation' => [ + 'code' => ' */ + } + ', + ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ + 'code'=> ' [ + 'code'=> ' [ 'code' => '