From d91aab15be353b1edabec92e3447c70b78fedf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:06:20 +0100 Subject: [PATCH 01/58] Restore support for null coalesce on match expressions https://github.com/vimeo/psalm/pull/10068 added isset restrictions that didn't consider null coalesces on match expressions. This restores that support by converting the match expression to a virtual variable for the isset analysis, similar to other incompatible expressions. --- .../Expression/BinaryOp/CoalesceAnalyzer.php | 1 + tests/MatchTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php index 0a52166699a..72194f55224 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php @@ -39,6 +39,7 @@ public static function analyze( || $root_expr instanceof PhpParser\Node\Expr\MethodCall || $root_expr instanceof PhpParser\Node\Expr\StaticCall || $root_expr instanceof PhpParser\Node\Expr\Cast + || $root_expr instanceof PhpParser\Node\Expr\Match_ || $root_expr instanceof PhpParser\Node\Expr\NullsafePropertyFetch || $root_expr instanceof PhpParser\Node\Expr\NullsafeMethodCall || $root_expr instanceof PhpParser\Node\Expr\Ternary diff --git a/tests/MatchTest.php b/tests/MatchTest.php index e281a200e97..64f2c4f5e0d 100644 --- a/tests/MatchTest.php +++ b/tests/MatchTest.php @@ -167,6 +167,21 @@ function process(Obj1|Obj2 $obj): int|string 'ignored_issues' => [], 'php_version' => '8.0', ], + 'nullCoalesce' => [ + 'code' => <<<'PHP' + null, + true => 1, + } ?? 2; + PHP, + 'assertions' => [ + '$match' => 'int', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } From 4f25ccee400c3e2241d393eb60332e5edabcc67c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:41:54 +0100 Subject: [PATCH 02/58] update define types to be correct --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_73_delta.php | 4 +++ dictionaries/CallMap_historical.php | 2 +- .../Call/NamedFunctionCallHandler.php | 33 ++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 4e7166ec105..5f3dba9c880 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1552,7 +1552,7 @@ 'decbin' => ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], -'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], +'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'DeflateContext', 'data'=>'string', 'flush_mode='=>'int'], diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php index d79026c3805..6352fa06292 100644 --- a/dictionaries/CallMap_73_delta.php +++ b/dictionaries/CallMap_73_delta.php @@ -64,6 +64,10 @@ 'old' => ['int', 'scale'=>'int'], 'new' => ['int', 'scale='=>'int'], ], + 'define' => [ + 'old' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], + 'new' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], + ], 'ldap_compare' => [ 'old' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], 'new' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index afb21ba72df..864b668bd0b 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -9937,7 +9937,7 @@ 'decbin' => ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], - 'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], + 'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'resource', 'data'=>'string', 'flush_mode='=>'int'], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 961662f7044..514df1a55a5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -225,7 +225,38 @@ public static function handle( } if ($function_id === 'defined') { - $context->check_consts = false; + if ($first_arg && !$context->inside_negation) { + $fq_const_name = ConstFetchAnalyzer::getConstName( + $first_arg->value, + $statements_analyzer->node_data, + $codebase, + $statements_analyzer->getAliases(), + ); + + if ($fq_const_name !== null) { + $const_type = ConstFetchAnalyzer::getConstType( + $statements_analyzer, + $fq_const_name, + true, + $context, + ); + + if (!$const_type) { + ConstFetchAnalyzer::setConstType( + $statements_analyzer, + $fq_const_name, + Type::getMixed(), + $context, + ); + + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } return; } From 2c5645c4669d6a6fd65a2d0e477f920eec74ed8b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:56:31 +0100 Subject: [PATCH 03/58] use branch name to randomize tests to ensure test dependencies will error --- .github/workflows/ci.yml | 1 + bin/tests-github-actions.sh | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8baa8961369..e01a894b61b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: CHUNK_COUNT: "${{ matrix.count }}" CHUNK_NUMBER: "${{ matrix.chunk }}" PARALLEL_PROCESSES: 5 + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} steps: - name: Set up PHP diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh index 4296f70591a..2c0ef4b787b 100755 --- a/bin/tests-github-actions.sh +++ b/bin/tests-github-actions.sh @@ -3,13 +3,15 @@ set -eu function get_seeded_random() { - openssl enc -aes-256-ctr -pass pass:"vimeo/psalm" -nosalt /dev/null + local -r branch_name="$1" + openssl enc -aes-256-ctr -pass pass:"$branch_name" -nosalt /dev/null } function run { local -r chunk_count="$1" local -r chunk_number="$2" local -r parallel_processes="$3" + local -r branch_name="$4" local -r phpunit_cmd=' echo "::group::{}"; @@ -23,7 +25,7 @@ exit "$exit_code"' mkdir -p build/parallel/ build/phpunit/logs/ - find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all + find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random "$branch_name") > build/tests_all # split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible #split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split local -r lines=$(wc -l Date: Wed, 29 Nov 2023 23:08:44 +0100 Subject: [PATCH 04/58] Fix static magic method pureness not being inherited from traits https://github.com/vimeo/psalm/pull/10385 "broke" this by propagating pseudo static methods from traits to using classes. `AtomicStaticCallAnalyzer` was then not capable of dealing with this, because now these static pseudo methods actually exist. As long as the methods from traits aren't actually transferred to the using class, it seems right that the logic in `AtomicStaticCallAnalyzer` uses `::getDeclaringMethodId()` instead of `::getAppearingMethodId()` for this purpose. --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 8 +- tests/PureAnnotationTest.php | 75 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index e42024d08f6..ad9fd724f4e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -566,12 +566,12 @@ private static function handleNamedCall( true, $context->insideUse(), )) { - $callstatic_appearing_id = $codebase->methods->getAppearingMethodId($callstatic_id); - assert($callstatic_appearing_id !== null); + $callstatic_declaring_id = $codebase->methods->getDeclaringMethodId($callstatic_id); + assert($callstatic_declaring_id !== null); $callstatic_pure = false; $callstatic_mutation_free = false; - if ($codebase->methods->hasStorage($callstatic_appearing_id)) { - $callstatic_storage = $codebase->methods->getStorage($callstatic_appearing_id); + if ($codebase->methods->hasStorage($callstatic_declaring_id)) { + $callstatic_storage = $codebase->methods->getStorage($callstatic_declaring_id); $callstatic_pure = $callstatic_storage->pure; $callstatic_mutation_free = $callstatic_storage->mutation_free; } diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index f27632baed6..57c27e7b113 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -446,6 +446,81 @@ function gimmeFoo(): MyEnum return MyEnum::FOO(); }', ], + 'pureThroughCallStaticInTrait' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' Date: Fri, 1 Dec 2023 12:03:24 +0100 Subject: [PATCH 05/58] Use correct file path while adding unused suppressions for virtual __constructs --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 4e93704e2c3..7b7a9972c98 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -186,7 +186,7 @@ public function analyze( || !in_array("UnusedPsalmSuppress", $storage->suppressed_issues) ) { foreach ($storage->suppressed_issues as $offset => $issue_name) { - IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name); + IssueBuffer::addUnusedSuppression($storage->location->file_path, $offset, $issue_name); } } } From 8111319fc3eea807789f0c6f1d8211a823ef3fc9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 12:25:04 +0100 Subject: [PATCH 06/58] Fix --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 7b7a9972c98..920ff547287 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -186,7 +186,13 @@ public function analyze( || !in_array("UnusedPsalmSuppress", $storage->suppressed_issues) ) { foreach ($storage->suppressed_issues as $offset => $issue_name) { - IssueBuffer::addUnusedSuppression($storage->location->file_path, $offset, $issue_name); + IssueBuffer::addUnusedSuppression( + $storage->location !== null + ? $storage->location->file_path + : $this->getFilePath(), + $offset, + $issue_name + ); } } } From 461cd184e5fc571bf82777a9efacfd1308b110d2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 12:25:30 +0100 Subject: [PATCH 07/58] cs-fix --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 920ff547287..3df1a6a1b0b 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -191,7 +191,7 @@ public function analyze( ? $storage->location->file_path : $this->getFilePath(), $offset, - $issue_name + $issue_name, ); } } From 7e948419cddac6b146ceaaeeac9d42f61b3a7627 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 13:37:28 +0100 Subject: [PATCH 08/58] Fix array_key_exists negation --- src/Psalm/Type/Reconciler.php | 5 ++++- tests/TypeReconciliation/ArrayKeyExistsTest.php | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1ae92b40571..7863d05c265 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; @@ -197,7 +198,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; diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 3ee780251c1..c8b8dafbaf8 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -46,6 +46,15 @@ function three(array $a): void { echo $a["a"]; echo $a["b"]; }', + ], + 'arrayKeyExistsNegation' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:17 +0100 Subject: [PATCH 09/58] Fix --- .../Statements/Block/IfElse/IfAnalyzer.php | 14 -------------- tests/TypeReconciliation/ArrayKeyExistsTest.php | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index ad95cce30d5..c74da5d3efe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -272,20 +272,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/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index c8b8dafbaf8..19174d347cd 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -56,6 +56,19 @@ function getMethodName(array $data = []): void { } ', ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ' + ] 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:24 +0100 Subject: [PATCH 10/58] Fix --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 19174d347cd..1390caeee13 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -68,7 +68,7 @@ function getMethodName(array $ddata = []): void { /** @psalm-check-type-exact $ddata = array */ } ' - ] + ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:07:55 +0100 Subject: [PATCH 11/58] cs-fix --- .../Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php | 3 --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index c74da5d3efe..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 diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 1390caeee13..e44bfa55464 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -67,7 +67,7 @@ function getMethodName(array $ddata = []): void { } /** @psalm-check-type-exact $ddata = array */ } - ' + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 16:03:08 +0100 Subject: [PATCH 12/58] Create keyed arrays when assigning literal union keys --- .../Assignment/ArrayAssignmentAnalyzer.php | 38 ++++++++----------- tests/ArrayAssignmentTest.php | 21 ++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) 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/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 38f98d70d60..4377c1e72cf 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' => ' Date: Fri, 1 Dec 2023 16:11:05 +0100 Subject: [PATCH 13/58] Fix --- tests/ArrayAssignmentTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 4377c1e72cf..364b6def325 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -213,7 +213,7 @@ class B {} 'assertions' => [ '$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' => [ @@ -1000,6 +1000,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } From 1a4656564a5b3c41d5444473d31725936db86b6a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 16:31:01 +0100 Subject: [PATCH 14/58] Cleanup --- tests/TypeReconciliation/TypeAlgebraTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TypeReconciliation/TypeAlgebraTest.php b/tests/TypeReconciliation/TypeAlgebraTest.php index aea50a778e6..cfc8374a94d 100644 --- a/tests/TypeReconciliation/TypeAlgebraTest.php +++ b/tests/TypeReconciliation/TypeAlgebraTest.php @@ -287,12 +287,12 @@ function foo(array $arr): void { $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; } }', ], From ead29084646d0c1bbb842e981b32ca400bf1f78b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:05:23 +0100 Subject: [PATCH 15/58] Fixup tests --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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; } From 147129345ea848d0f2559206a106ab13e2c364c2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:23:35 +0100 Subject: [PATCH 16/58] Add failing test --- tests/TypeReconciliation/IssetTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index c15612692e5..19e7dbfcd0e 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -41,6 +41,21 @@ public function providerValidCodeParse(): iterable 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:46:24 +0100 Subject: [PATCH 17/58] Fixup --- src/Psalm/Type/Reconciler.php | 133 ++++++++++++++----------- tests/Loop/ForeachTest.php | 4 +- tests/TypeReconciliation/IssetTest.php | 31 +++++- 3 files changed, 108 insertions(+), 60 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 7863d05c265..61e731862ff 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -46,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; @@ -1108,88 +1110,103 @@ 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/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index d7b56e5fa48..fbbb6e445f9 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,7 +1027,9 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - $arr[$i]["a"] ??= 0; + if (!isset($arr[$i]["a"])) { + $arr[$i]["a"] = 0; + } $arr[$i]["a"] += 5; } diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index 19e7dbfcd0e..f9d24846e10 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -54,7 +54,36 @@ function t2(array $arr, int $i): array { $arr[$i] = 1; } return $arr; - }' + }', + ], + 'issetWithArrayAssignment2' => [ + 'code'=> ' [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:48:47 +0100 Subject: [PATCH 18/58] Fixup --- src/Psalm/Type/Reconciler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 61e731862ff..f5e288639cf 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1124,7 +1124,10 @@ private static function adjustTKeyedArrayType( return; } } else { - $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' + ? substr($array_key, 1, -1) + : $array_key + ; } $base_key = implode($key_parts); From 0aeb87c21cd730a5bf7796724653671a5df89b6f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:57:50 +0100 Subject: [PATCH 19/58] Simplify --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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; } From 59fd539ab98b3401c6f25b57aaa8f00d163129e5 Mon Sep 17 00:00:00 2001 From: rarila Date: Fri, 1 Dec 2023 18:01:57 +0100 Subject: [PATCH 20/58] Fix POSIX only detection of absolute paths --- src/Psalm/Config/FileFilter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 4ef5f993c4c..dd8fb31186a 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -7,6 +7,7 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SimpleXMLElement; +use Symfony\Component\Filesystem\Path; use function array_filter; use function array_map; @@ -127,7 +128,7 @@ public static function loadFromArray( $resolve_symlinks = (bool) ($directory['resolveSymlinks'] ?? false); $declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false); - if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($directory_path)) { /** @var non-empty-string */ $prospective_directory_path = $directory_path; } else { From c6bf949c712b1565170d7ae1203d776ae18bfb6d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 2 Dec 2023 09:04:37 +0100 Subject: [PATCH 21/58] Fix CLI -r error Fix https://github.com/vimeo/psalm/issues/10418 --- src/Psalm/Internal/CliUtils.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index cdd4e281311..e90f73e4e5d 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -231,7 +231,7 @@ public static function getArguments(): array } if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -271,7 +271,7 @@ public static function getPathsToCheck($f_paths): ?array $input_path = $input_paths[$i]; if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -287,6 +287,7 @@ public static function getPathsToCheck($f_paths): ?array $ignored_arguments = array( 'config', 'printer', + 'root', ); if (in_array(substr($input_path, 2), $ignored_arguments, true)) { From 6eba2f564c499262bad320d6f697a3bdfc09e0fd Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 2 Dec 2023 12:02:55 +0100 Subject: [PATCH 22/58] Fix return type of DOMXPath::query This can also return namespace nodes, which are not a child class of DOMNode. --- stubs/extensions/dom.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index f52153787d6..2520a479902 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -975,7 +975,7 @@ class DOMXPath public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} /** - * @return DOMNodeList|false + * @return DOMNodeList|false */ public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} From b03b846682e2ee6b90c2c2742faa7b93821a7c81 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 30 Nov 2023 13:48:32 +0100 Subject: [PATCH 23/58] Emit UnusedPsalmSuppress issues for suppressed issues already removed by plugins --- psalm-baseline.xml | 28 ++++++++++++++++++++- src/Psalm/IssueBuffer.php | 13 ++++++---- tests/CodebaseTest.php | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..12ccf4812d9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -350,6 +350,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 9ca29ef701b..33fc16bc800 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -133,6 +133,14 @@ final class IssueBuffer */ public static function accepts(CodeIssue $e, array $suppressed_issues = [], bool $is_fixable = false): bool { + $config = Config::getInstance(); + $project_analyzer = ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); + if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { + return false; + } + if (self::isSuppressed($e, $suppressed_issues)) { return false; } @@ -258,11 +266,6 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); - $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); - if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { - return false; - } - $fqcn_parts = explode('\\', get_class($e)); $issue_type = array_pop($fqcn_parts); diff --git a/tests/CodebaseTest.php b/tests/CodebaseTest.php index f5291649826..254922c2060 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt\Class_; use Psalm\Codebase; use Psalm\Context; +use Psalm\Exception\CodeException; use Psalm\Exception\UnpopulatedClasslikeException; use Psalm\Issue\InvalidReturnStatement; use Psalm\Issue\InvalidReturnType; @@ -21,6 +22,9 @@ use function array_map; use function array_values; use function get_class; +use function getcwd; + +use const DIRECTORY_SEPARATOR; class CodebaseTest extends TestCase { @@ -246,4 +250,51 @@ public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool $this->analyzeFile('somefile.php', new Context); self::assertSame(0, IssueBuffer::getErrorCount()); } + /** + * @test + */ + public function addingCodeIssueIsMarkedAsRedundant(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('UnusedPsalmSuppress'); + + $this->addFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + 'getIssue(); + if ($issue->code_location->file_path !== (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php') { + return null; + } + if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) { + return false; + } elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) { + return false; + } + return null; + } + }; + + (new PluginRegistrationSocket($this->codebase->config, $this->codebase)) + ->registerHooksFromClass(get_class($eventHandler)); + + $this->analyzeFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + new Context, + ); + } } From ee5e4b800f01e183c7366badde4ef39be7663623 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 12:36:14 +0100 Subject: [PATCH 24/58] Update --- psalm.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > From de53638295356ba5b7a20f2d003be1e927f1e804 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 13:06:36 +0100 Subject: [PATCH 25/58] Fixes --- psalm-baseline.xml | 37 +++++++++++++++++++++-- psalm.xml.dist | 2 +- src/Psalm/Internal/Cli/LanguageServer.php | 8 ++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..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 @@ -350,6 +357,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > 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( From 18a6c0b6e9aade82a2f3cc36e3a644ba70eaf539 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 15:28:51 +0100 Subject: [PATCH 26/58] Implement by-ref closure use analysis --- .../Internal/Analyzer/ClosureAnalyzer.php | 41 ++------- .../Analyzer/FunctionLikeAnalyzer.php | 36 +++++++- .../Expression/Call/FunctionCallAnalyzer.php | 7 -- .../Internal/Diff/ClassStatementsDiffer.php | 2 - .../LanguageServer/LanguageServer.php | 7 -- tests/CallableTest.php | 12 +-- tests/ClosureTest.php | 84 ++++++++++++------- tests/ReferenceConstraintTest.php | 13 --- tests/UnusedVariableTest.php | 7 -- 9 files changed, 95 insertions(+), 114 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 9ed72b26914..8a69cf2fd10 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -13,7 +13,6 @@ use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; use Psalm\Type; -use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -133,12 +132,6 @@ public static function analyzeExpression( $use_var_id = '$' . $use->var->name; - // insert the ref into the current context if passed by ref, as whatever we're passing - // the closure to could execute it straight away. - if ($use->byRef && !$context->hasVariable($use_var_id)) { - $context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]); - } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph && $context->hasVariable($use_var_id) ) { @@ -154,7 +147,7 @@ public static function analyzeExpression( } $use_context->vars_in_scope[$use_var_id] = - $context->hasVariable($use_var_id) && !$use->byRef + $context->hasVariable($use_var_id) ? $context->vars_in_scope[$use_var_id] : Type::getMixed(); @@ -205,7 +198,12 @@ public static function analyzeExpression( $use_context->calling_method_id = $context->calling_method_id; $use_context->phantom_classes = $context->phantom_classes; - $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false); + $byref_vars = []; + $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false, $byref_vars); + + foreach ($byref_vars as $key => $value) { + $context->vars_in_scope[$key] = $value; + } if ($closure_analyzer->inferred_impure && $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer @@ -229,7 +227,7 @@ public static function analyzeExpression( /** * @return false|null */ - public static function analyzeClosureUses( + private static function analyzeClosureUses( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Closure $stmt, Context $context @@ -268,21 +266,6 @@ public static function analyzeClosureUses( continue; } - if ($use->byRef) { - $context->vars_in_scope[$use_var_id] = Type::getMixed(); - $context->vars_possibly_in_scope[$use_var_id] = true; - - if (!$statements_analyzer->hasVariable($use_var_id)) { - $statements_analyzer->registerVariable( - $use_var_id, - new CodeLocation($statements_analyzer, $use->var), - null, - ); - } - - return null; - } - if (!isset($context->vars_possibly_in_scope[$use_var_id])) { if ($context->check_variables) { if (IssueBuffer::accepts( @@ -329,14 +312,6 @@ public static function analyzeClosureUses( continue; } - } elseif ($use->byRef) { - $new_type = new Union([new TMixed()], [ - 'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes, - ]); - - $context->remove($use_var_id); - - $context->vars_in_scope[$use_var_id] = $new_type; } } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 3df1a6a1b0b..bf4378d9158 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -45,6 +45,8 @@ use Psalm\Issue\UnusedDocblockParam; use Psalm\Issue\UnusedParam; use Psalm\IssueBuffer; +use Psalm\Node\Expr\VirtualVariable; +use Psalm\Node\Stmt\VirtualWhile; use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; @@ -149,14 +151,18 @@ public function __construct($function, SourceAnalyzer $source, FunctionLikeStora /** * @param bool $add_mutations whether or not to add mutations to this method + * @param array $byref_vars + * @param-out array $byref_vars * @return false|null * @psalm-suppress PossiblyUnusedReturnValue unused but seems important + * @psalm-suppress ComplexMethod Unavoidably complex */ public function analyze( Context $context, NodeDataProvider $type_provider, ?Context $global_context = null, - bool $add_mutations = false + bool $add_mutations = false, + array &$byref_vars = [] ): ?bool { $storage = $this->storage; @@ -235,9 +241,8 @@ public function analyze( $statements_analyzer = new StatementsAnalyzer($this, $type_provider); + $byref_uses = []; if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) { - $byref_uses = []; - foreach ($this->function->uses as $use) { if (!is_string($use->var->name)) { continue; @@ -352,6 +357,31 @@ public function analyze( (bool) $template_types, ); + if ($byref_uses) { + $ref_context = clone $context; + $var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos'); + + $ref_context->vars_in_scope[$var] = Type::getBool(); + + $var = new VirtualVariable( + substr($var, 1), + ); + $virtual_while = new VirtualWhile( + $var, + $function_stmts, + ); + + $statements_analyzer->analyze( + [$virtual_while], + $ref_context, + ); + + foreach ($byref_uses as $var_id => $_) { + $byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id]; + $context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id]; + } + } + if ($storage->pure) { $context->pure = true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index c5b60af8c80..98e192c72f7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -404,13 +404,6 @@ public static function analyze( } } - if ($function_call_info->byref_uses) { - foreach ($function_call_info->byref_uses as $byref_use_var => $_) { - $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed(); - $context->vars_possibly_in_scope['$' . $byref_use_var] = true; - } - } - if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { NamedFunctionCallHandler::handle( $statements_analyzer, diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index 235cff8f400..c6d54c869c8 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -89,7 +89,6 @@ static function ( $start_diff = $b_start - $a_start; $line_diff = $b->getLine() - $a->getLine(); - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $start_diff, $line_diff]; return true; @@ -183,7 +182,6 @@ static function ( } if (!$signature_change && !$body_change) { - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $b_start - $a_start, $b->getLine() - $a->getLine()]; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3a0ff9a1e02..7062885e790 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -777,11 +777,8 @@ function (IssueData $issue_data): Diagnostic { //Process Baseline $file = $issue_data->file_name; $type = $issue_data->type; - /** @psalm-suppress MixedArrayAccess */ if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { - /** @psalm-suppress MixedArrayAccess, MixedArgument */ if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { - /** @psalm-suppress MixedArrayAccess, MixedAssignment */ $position = array_search( str_replace("\r\n", "\n", trim($issue_data->selected_text)), $issue_baseline[$file][$type]['s'], @@ -790,16 +787,12 @@ function (IssueData $issue_data): Diagnostic { if ($position !== false) { $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArgument */ array_splice($issue_baseline[$file][$type]['s'], $position, 1); - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } else { - /** @psalm-suppress MixedArrayAssignment */ $issue_baseline[$file][$type]['s'] = []; $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 200177e1d7b..23054bf9a6e 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -22,9 +22,9 @@ function run_function(\Closure $fnc) { /** * @return void - * @psalm-suppress MixedArgument */ function f() { + $data = 0; run_function( /** * @return void @@ -1786,16 +1786,6 @@ function takesCallable(callable $c) : void {} takesCallable(function() { return; });', ], - 'byRefUsesAlwaysMixed' => [ - 'code' => ' [ 'code' => ' [ 'code' => ' [ + '$testBefore===' => '123', + '$testInsideBefore===' => "'test'|123|null", + '$testInsideAfter===' => "'test'|null", + '$test===' => "'test'|123", + + '$doNotContaminate===' => '123', + ], + ], + 'byRefUseSelf' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], - 'closureByRefUseToMixed' => [ - 'code' => ' 'MixedReturnStatement', - ], 'noCrashWhenComparingIllegitimateCallable' => [ 'code' => 'getString());', ], - 'makeByRefUseMixed' => [ - 'code' => ' [], - 'ignored_issues' => ['MixedArgument'], - ], 'assignByRefToMixed' => [ 'code' => ' [ @@ -2234,10 +2232,6 @@ function string_to_float(string $a): float { ], 'allowUseByRef' => [ 'code' => ' Date: Sun, 3 Dec 2023 15:30:57 +0100 Subject: [PATCH 27/58] Fixup --- psalm-baseline.xml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..84bbae63a53 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -350,6 +350,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name From a2d89d09902c8b422ea4bbf444f5a66384d3e322 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 15:32:20 +0100 Subject: [PATCH 28/58] Fixup --- psalm.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > From 4ed0fe934f3902ab8d7a3e26bcdd41d9ea3cf6eb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:12:19 +0100 Subject: [PATCH 29/58] Fix shaped array class string key combination --- .../Assignment/ArrayAssignmentAnalyzer.php | 4 +++- src/Psalm/Internal/Type/TypeCombination.php | 3 +++ src/Psalm/Internal/Type/TypeCombiner.php | 12 +++++++++++- src/Psalm/Type/Reconciler.php | 4 +++- tests/ArrayAssignmentTest.php | 18 ++++++++++++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 576fe45a011..01150233a66 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -351,7 +351,9 @@ private static function updateTypeWithKeyValues( if (!$has_matching_objectlike_property && !$has_matching_string) { $properties = []; $classStrings = []; - $current_type = $current_type->setPossiblyUndefined(count($key_values) > 1); + $current_type = $current_type->setPossiblyUndefined( + $current_type->possibly_undefined || count($key_values) > 1, + ); foreach ($key_values as $key_value) { $properties[$key_value->value] = $current_type; if ($key_value instanceof TLiteralClassString) { diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0706315d1ac..0e4cc225383 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -55,6 +55,9 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; + /** @var array */ + public array $objectlike_class_string_keys = []; + public bool $objectlike_sealed = true; public ?Union $objectlike_key_type = null; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 3f841da4fa3..b48387acb8c 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -667,6 +667,7 @@ private static function scrapeTypeProperties( $has_defined_keys = false; + $class_strings = $type->class_strings ?? []; foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; @@ -705,6 +706,15 @@ private static function scrapeTypeProperties( ); } + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { + $combination->objectlike_class_string_keys[$candidate_property_name] = + $combination->objectlike_class_string_keys[$candidate_property_name] + && ($class_strings[$candidate_property_name] ?? false); + } else { + $combination->objectlike_class_string_keys[$candidate_property_name] = + ($class_strings[$candidate_property_name] ?? false); + } + unset($missing_entries[$candidate_property_name]); } @@ -1421,7 +1431,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - null, + $combination->objectlike_class_string_keys, $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index f5e288639cf..baae339644d 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1132,7 +1132,9 @@ private static function adjustTKeyedArrayType( $base_key = implode($key_parts); - $result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1); + $result_type = $result_type->setPossiblyUndefined( + $result_type->possibly_undefined || count($array_key_offsets) > 1, + ); foreach ($array_key_offsets as $array_key_offset) { if (isset($existing_types[$base_key]) && $array_key_offset !== false) { diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 364b6def325..790f901b398 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -55,6 +55,24 @@ public function providerValidCodeParse(): iterable '$resultOpt===' => 'array{a?: true, b?: true}', ], ], + 'assignUnionOfLiteralsClassKeys' => [ + 'code' => ' $v) { + $vv = new $k; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Wed, 6 Dec 2023 14:23:45 +0100 Subject: [PATCH 30/58] Fix --- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index b48387acb8c..2854b1cb25f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1431,7 +1431,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - $combination->objectlike_class_string_keys, + array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], From d3b7f3f0b4c27a3436dc3c5d25df56c1c28e5cc4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:47:24 +0100 Subject: [PATCH 31/58] Fix --- src/Psalm/Internal/Type/TypeCombination.php | 2 +- src/Psalm/Internal/Type/TypeCombiner.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0e4cc225383..94e64793e8b 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -55,7 +55,7 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; - /** @var array */ + /** @var array */ public array $objectlike_class_string_keys = []; public bool $objectlike_sealed = true; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 2854b1cb25f..773e3f71a47 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -706,6 +706,12 @@ private static function scrapeTypeProperties( ); } + unset($missing_entries[$candidate_property_name]); + + if (is_int($candidate_property_name)) { + continue; + } + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { $combination->objectlike_class_string_keys[$candidate_property_name] = $combination->objectlike_class_string_keys[$candidate_property_name] @@ -714,8 +720,6 @@ private static function scrapeTypeProperties( $combination->objectlike_class_string_keys[$candidate_property_name] = ($class_strings[$candidate_property_name] ?? false); } - - unset($missing_entries[$candidate_property_name]); } if ($type->fallback_params) { From 76458e0b50d8871366b0f72c257562dafc592859 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:52:54 +0100 Subject: [PATCH 32/58] Add test --- tests/ArrayAssignmentTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 790f901b398..713756ad2a9 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -73,6 +73,20 @@ class b {} '$result===' => 'array{a::class: true, b::class: true}', ], ], + 'assignUnionOfLiteralsClassKeys2' => [ + 'code' => ' true]; + + foreach ([a::class, b::class] as $k) { + $result[$k] = true; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true, c: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Wed, 6 Dec 2023 11:20:18 -0600 Subject: [PATCH 33/58] Replace remaining POSIX only absolute path detection These were missed in #10441. Fixes "Could not resolve config path" error on Windows (#10418). --- src/Psalm/Config.php | 2 +- src/Psalm/Config/FileFilter.php | 2 +- .../Statements/Expression/IncludeAnalyzer.php | 17 +++-------------- .../PhpVisitor/Reflector/ExpressionScanner.php | 10 ++-------- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d8ba8e8f416..0cd65f165f4 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1315,7 +1315,7 @@ private static function fromXmlAndPaths( } // we need an absolute path for checks - if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isRelative($path)) { $prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path; } else { $prospective_path = $path; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index dd8fb31186a..8626deca4ee 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -247,7 +247,7 @@ public static function loadFromArray( foreach ($config['file'] as $file) { $file_path = (string) ($file['name'] ?? ''); - if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($file_path)) { /** @var non-empty-string */ $prospective_file_path = $file_path; } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 24db2b6d92e..6c4a36ab2bf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -20,6 +20,7 @@ use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; +use Symfony\Component\Filesystem\Path; use function constant; use function defined; @@ -93,13 +94,7 @@ public static function analyze( $include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath())); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { @@ -285,13 +280,7 @@ public static function getPathTo( string $file_name, Config $config ): ?string { - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name); - } - - if ($is_path_relative) { + if (Path::isRelative($file_name)) { $file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 6afd142bfa5..6e7887ff158 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -20,13 +20,13 @@ use Psalm\Storage\FileStorage; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type; +use Symfony\Component\Filesystem\Path; use function assert; use function defined; use function dirname; use function explode; use function in_array; -use function preg_match; use function strpos; use function strtolower; use function substr; @@ -316,13 +316,7 @@ public static function visitInclude( $include_path = IncludeAnalyzer::resolveIncludePath($path_to_file, dirname($file_storage->file_path)); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { From dbded437ada472efbae3c6b6c018f2647ec6b086 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 11:29:33 +0100 Subject: [PATCH 34/58] Small assertion fix --- src/Psalm/Type/Reconciler.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index f5e288639cf..287e9f78816 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -171,6 +171,7 @@ public static function reconcileKeyedTypes( $has_negation = false; $has_isset = false; $has_inverted_isset = false; + $has_inverted_key_exists = false; $has_truthy_or_falsy_or_empty = false; $has_empty = false; $has_count_check = false; @@ -201,7 +202,9 @@ public static function reconcileKeyedTypes( && $new_type_part_part instanceof IsIdentical; $has_inverted_isset = $has_inverted_isset - || $new_type_part_part instanceof IsNotIsset + || $new_type_part_part instanceof IsNotIsset; + + $has_inverted_key_exists = $has_inverted_key_exists || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check @@ -221,6 +224,7 @@ public static function reconcileKeyedTypes( $code_location, $has_isset, $has_inverted_isset, + $has_inverted_key_exists, $has_empty, $inside_loop, $has_object_array_access, @@ -334,7 +338,7 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_empty && !$is_equality) { + if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_inverted_key_exists && !$has_empty && !$is_equality) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -648,6 +652,7 @@ private static function getValueForKey( ?CodeLocation $code_location, bool $has_isset, bool $has_inverted_isset, + bool $has_inverted_key_exists, bool $has_empty, bool $inside_loop, bool &$has_object_array_access @@ -723,11 +728,11 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset) { + if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset && !$has_inverted_key_exists) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); @@ -756,7 +761,7 @@ private static function getValueForKey( } elseif ($existing_key_type_part instanceof TString) { $new_base_type_candidate = Type::getString(); } elseif ($existing_key_type_part instanceof TNamedObject - && ($has_isset || $has_inverted_isset) + && ($has_isset || $has_inverted_isset || $has_inverted_key_exists) ) { $has_object_array_access = true; From bfd167515b47323cfe823adea6a696fe15c7372e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:25:03 +0100 Subject: [PATCH 35/58] the new version has no changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7e41dd27eeb..c4dda05c87e 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.1", "felixfbecker/language-server-protocol": "^1.5.2", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.16", "sebastian/diff": "^4.0 || ^5.0", From 6650bd8a572e886e677f7c323a4c3031a3ad37c9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 12:31:21 +0100 Subject: [PATCH 36/58] cs-fix --- src/Psalm/Type/Reconciler.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 287e9f78816..751d76f63cc 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -338,7 +338,12 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_inverted_key_exists && !$has_empty && !$is_equality) { + if (substr($key, -1) === ']' + && !$has_inverted_isset + && !$has_inverted_key_exists + && !$has_empty + && !$is_equality + ) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -728,11 +733,17 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset && !$has_inverted_key_exists) { + if ($new_base_type_candidate->isMixed() + && !$has_isset + && !$has_inverted_isset + && !$has_inverted_key_exists + ) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) + && isset($new_assertions[$new_base_key]) + ) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); From d5bac4d51d9dae580dc0846a219f84b42b1052f1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 12:46:34 +0100 Subject: [PATCH 37/58] Emit AfterCodebasePopulatedEvent even on partial scans --- src/Psalm/Internal/Analyzer/ProjectAnalyzer.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 3dd7a646038..ed22c279d97 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1065,6 +1065,10 @@ public function checkPaths(array $paths_to_check): void $this->config->visitStubFiles($this->codebase, $this->progress); + $event = new AfterCodebasePopulatedEvent($this->codebase); + + $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event); + $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( From 0d3485b588a572d4f5f13324ee96a623c0b03779 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 13:04:59 +0100 Subject: [PATCH 38/58] Commit just first part of fix for now --- tests/ArrayAssignmentTest.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 713756ad2a9..790f901b398 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -73,20 +73,6 @@ class b {} '$result===' => 'array{a::class: true, b::class: true}', ], ], - 'assignUnionOfLiteralsClassKeys2' => [ - 'code' => ' true]; - - foreach ([a::class, b::class] as $k) { - $result[$k] = true; - }', - 'assertions' => [ - '$result===' => 'array{a::class: true, b::class: true, c: true}', - ], - ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Thu, 7 Dec 2023 17:20:37 +0100 Subject: [PATCH 39/58] fix composer scripts running with inconsistent php versions --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 7e41dd27eeb..d1678884c54 100644 --- a/composer.json +++ b/composer.json @@ -110,16 +110,16 @@ "psalter" ], "scripts": { - "cs": "phpcs -ps", - "cs-fix": "phpcbf -ps", - "lint": "parallel-lint ./src ./tests", + "cs": "@php phpcs -ps", + "cs-fix": "@php phpcbf -ps", + "lint": "@php parallel-lint ./src ./tests", "phpunit": [ "Composer\\Config::disableProcessTimeout", - "paratest --runner=WrapperRunner" + "@php paratest --runner=WrapperRunner" ], "phpunit-std": [ "Composer\\Config::disableProcessTimeout", - "phpunit" + "@php phpunit" ], "verify-callmap": "@php phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php", "psalm": "@php ./psalm", From 576ecd66e6c909bb63a8e106e8fe573cf0d1c358 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 17:29:22 +0100 Subject: [PATCH 40/58] Fix #10460 --- .../Codebase/ConstantTypeResolver.php | 4 ++-- tests/ArrayAssignmentTest.php | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 866b5180e14..4bc718e3e49 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -216,8 +216,8 @@ public static function resolve( return new TArray([Type::getArrayKey(), Type::getMixed()]); } - foreach ($spread_array->properties as $spread_array_type) { - $properties[$auto_key++] = $spread_array_type; + foreach ($spread_array->properties as $k => $spread_array_type) { + $properties[is_string($k) ? $k : $auto_key++] = $spread_array_type; } continue; } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 364b6def325..d016298c44a 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1280,6 +1280,29 @@ function foo(array $arr) : string { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'constantArraySpreadWithString' => [ + 'code' => ' "a", + "b" => "b", + ]; + } + + class ChildClass extends BaseClass { + public const A = [ + ...parent::KEYS, + "c" => "c", + ]; + } + + $a = ChildClass::A;', + 'assertions' => [ + '$a===' => "array{a: 'a', b: 'b', c: 'c'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'listPropertyAssignmentAfterIsset' => [ 'code' => ' Date: Thu, 7 Dec 2023 16:12:03 +0100 Subject: [PATCH 41/58] dont combine empty string with numeric-string Fix https://github.com/vimeo/psalm/issues/6646 --- src/Psalm/Internal/Type/TypeCombiner.php | 3 +++ tests/TypeCombinationTest.php | 32 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 773e3f71a47..e21d41b0559 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1080,6 +1080,9 @@ private static function scrapeStringProperties( if ($has_only_numeric_strings) { $combination->value_types['string'] = $type; + } elseif (count($combination->strings) === 1 && !$has_only_non_empty_strings) { + $combination->value_types['string'] = $type; + return; } elseif ($has_only_non_empty_strings) { $combination->value_types['string'] = new TNonEmptyString(); } else { diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index c3371ef2a6b..b891e84dcc2 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -88,6 +88,38 @@ function expectsTraversableOrArray($_a): void } ', ], + 'emptyStringNumericStringDontCombine' => [ + 'code' => ' [ + 'code' => ' Date: Tue, 12 Dec 2023 07:51:21 +0100 Subject: [PATCH 42/58] fix psalm v4 hardcoded in tests --- tests/Config/ConfigTest.php | 5 +++-- tests/Config/PluginTest.php | 5 +++-- tests/ProjectCheckerTest.php | 5 +++-- tests/ReportOutputTest.php | 3 ++- tests/StubTest.php | 5 +++-- tests/TestCase.php | 5 +++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 88273122b36..e8fe9a94c40 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -17,6 +17,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\VersionUtils; use Psalm\Issue\TooManyArguments; use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; @@ -58,11 +59,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 130fd2d3304..bfd98cffc0b 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -12,6 +12,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; @@ -46,11 +47,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index cce7da9d697..3e7bf7af31c 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -8,6 +8,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; @@ -43,11 +44,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index 8864247abe3..67aeedaca4b 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -10,6 +10,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Report; use Psalm\Report\JsonReport; @@ -109,7 +110,7 @@ public function testSarifReport(): void 'driver' => [ 'name' => 'Psalm', 'informationUri' => 'https://psalm.dev', - 'version' => '4.0.0', + 'version' => VersionUtils::getPsalmVersion(), 'rules' => [ [ 'id' => '246', diff --git a/tests/StubTest.php b/tests/StubTest.php index 6c835a65d8d..f12fb943ed8 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -13,6 +13,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use function define; @@ -37,11 +38,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2da89b3558c..39f02d46703 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,6 +12,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Type\TypeParser; use Psalm\Internal\Type\TypeTokenizer; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Type\Union; @@ -56,11 +57,11 @@ public static function setUpBeforeClass(): void ini_set('memory_limit', '-1'); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } parent::setUpBeforeClass(); From 0fd789cdcc2afd0784d8dd87eccd08482e2cbac9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:44:17 +0100 Subject: [PATCH 43/58] Fix type not equal when parent parent nodes are only populated if taint/unused variable analysis is enabled --- src/Psalm/Type/Atomic/TArray.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 06477607592..54dfef34117 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -84,7 +84,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } From 679a492609100e586fce6e356d1b378f98d9da06 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:54:35 +0100 Subject: [PATCH 44/58] other atomics --- src/Psalm/Type/Atomic/TClassStringMap.php | 2 +- src/Psalm/Type/Atomic/TGenericObject.php | 2 +- src/Psalm/Type/Atomic/TIterable.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 6 +++--- src/Psalm/Type/Atomic/TList.php | 2 +- src/Psalm/Type/Atomic/TObjectWithProperties.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index d15d297f10e..56b43ccf597 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -216,7 +216,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality)) { + if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index c362085f565..415458de135 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -111,7 +111,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index 6b6c9ea32ab..1f67bfb5602 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -115,7 +115,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index dfddbe81334..3bd7e2a65e4 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -658,11 +658,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } if ($this->fallback_params !== null && $other_type->fallback_params !== null) { - if (!$this->fallback_params[0]->equals($other_type->fallback_params[0])) { + if (!$this->fallback_params[0]->equals($other_type->fallback_params[0], false, false)) { return false; } - if (!$this->fallback_params[1]->equals($other_type->fallback_params[1])) { + if (!$this->fallback_params[1]->equals($other_type->fallback_params[1], false, false)) { return false; } } @@ -672,7 +672,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 13c44e5b453..7d102709919 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -205,7 +205,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality)) { + if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index cae5be7e7f7..b681459bfaa 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -207,7 +207,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } From 3c045b30a7a2aad2f2e9395ba179a0d136351391 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:05:15 +0100 Subject: [PATCH 45/58] fix false positive ArgumentTypeCoercion for callback param when unsealed and all optional --- .../Type/Comparator/ArrayTypeComparator.php | 13 +++++++++++++ tests/CallableTest.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index c440526fea5..122bc65d70e 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -49,6 +49,19 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TKeyedArray + && $input_type_part instanceof TArray + && !$container_type_part->is_list + && !$container_type_part->isNonEmpty() + && !$container_type_part->isSealed() + && $input_type_part->equals( + $container_type_part->getGenericArrayType($container_type_part->isNonEmpty()), + false, + ) + ) { + return true; + } + if ($container_type_part instanceof TKeyedArray && $input_type_part instanceof TArray ) { diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 23054bf9a6e..fc8c36d212f 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1892,6 +1892,22 @@ function addHandler(string $_message, callable $_handler): void {} return [1, 2, 3]; });', ], + 'unsealedAllOptionalCbParam' => [ + 'code' => ') $arg + * @return void + */ + function foo($arg) {} + + /** + * @param array{a?: string}&array $cb_arg + * @return void + */ + function bar($cb_arg) {} + + foo("bar");', + ], ]; } From 761f390d9bb4803d8cf35fb2934db389838b66d4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 12 Dec 2023 18:51:31 +0100 Subject: [PATCH 46/58] Use same parameter names in stubs --- stubs/CoreGenericClasses.phpstub | 18 +++++------ stubs/CoreGenericIterators.phpstub | 18 +++++------ stubs/SPL.phpstub | 52 +++++++++++++++--------------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 2b7b76da7e4..62e0ff33750 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -158,46 +158,46 @@ class ArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Count * Returns whether the requested index exists * @link http://php.net/manual/en/arrayobject.offsetexists.php * - * @param TKey $index The index being checked. + * @param TKey $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.0.0 */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** * Returns the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetget.php * - * @param TKey $index The index with the value. + * @param TKey $offset The index with the value. * @return TValue The value at the specified index or false. * * @since 5.0.0 */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** * Sets the value at the specified index to newval * @link http://php.net/manual/en/arrayobject.offsetset.php * - * @param TKey $index The index being set. - * @param TValue $newval The new value for the index. + * @param TKey $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.0.0 */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** * Unsets the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetunset.php * - * @param TKey $index The index being unset. + * @param TKey $offset The index being unset. * @return void * * @since 5.0.0 */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * Appends the value diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..48abad51dea 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -185,30 +185,30 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Serializable, Coun public function __construct($array = array(), $flags = 0) { } /** - * @param TKey $index The offset being checked. + * @param TKey $offset The offset being checked. * @return bool true if the offset exists, otherwise false */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** - * @param TKey $index The offset to get the value from. + * @param TKey $offset The offset to get the value from. * @return TValue|null The value at offset index, null when accessing invalid indexes * @psalm-ignore-nullable-return */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** - * @param TKey $index The index to set for. - * @param TValue $newval The new value to store at the index. + * @param TKey $offset The index to set for. + * @param TValue $value The new value to store at the index. * @return void */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** - * @param TKey $index The offset to unset. + * @param TKey $offset The offset to unset. * @return void */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * @param TValue $value The value to append. diff --git a/stubs/SPL.phpstub b/stubs/SPL.phpstub index 288ceba770a..7a623c933a0 100644 --- a/stubs/SPL.phpstub +++ b/stubs/SPL.phpstub @@ -15,14 +15,14 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa /** * Add/insert a new value at the specified index * - * @param int $index The index where the new value is to be inserted. - * @param TValue $newval The new value for the index. + * @param int $offset The index where the new value is to be inserted. + * @param TValue $value The new value for the index. * @return void * * @link https://php.net/spldoublylinkedlist.add * @since 5.5.0 */ - public function add($index, $newval) {} + public function add($offset, $value) {} /** * Pops a node from the end of the doubly linked list @@ -107,49 +107,49 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa public function isEmpty() {} /** - * Returns whether the requested $index exists + * Returns whether the requested $offset exists * @link https://php.net/manual/en/spldoublylinkedlist.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.3.0 */ - public function offsetExists($index) {} + public function offsetExists($offset) {} /** - * Returns the value at the specified $index + * Returns the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetget.php * - * @param int $index The index with the value. + * @param int $offset The index with the value. * @return TValue The value at the specified index. * * @since 5.3.0 */ - public function offsetGet($index) {} + public function offsetGet($offset) {} /** - * Sets the value at the specified $index to $newval + * Sets the value at the specified $offset to $value * @link https://php.net/manual/en/spldoublylinkedlist.offsetset.php * - * @param int $index The index being set. - * @param TValue $newval The new value for the index. + * @param int $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.3.0 */ - public function offsetSet($index, $newval) {} + public function offsetSet($offset, $value) {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetunset.php * - * @param int $index The index being unset. + * @param int $offset The index being unset. * @return void * * @since 5.3.0 */ - public function offsetUnset($index) {} + public function offsetUnset($offset) {} /** * Return current array entry @@ -297,46 +297,46 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, and false otherwise. * * @since 5.3.0 */ - public function offsetExists(int $index): bool {} + public function offsetExists(int $offset): bool {} /** * Sets a new value at a specified index * @link https://php.net/manual/en/splfixedarray.offsetset.php * - * @param int $index The index being sent. - * @param TValue $newval The new value for the index + * @param int $offset The index being sent. + * @param TValue $value The new value for the index * @return void * * @since 5.3.0 */ - public function offsetSet(int $index, $newval): void {} + public function offsetSet(int $offset, $value): void {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/splfixedarray.offsetunset.php * - * @param int $index The index being unset + * @param int $offset The index being unset * @return void * * @since 5.3.0 */ - public function offsetUnset(int $index): void {} + public function offsetUnset(int $offset): void {} /** * Returns the value at the specified index * @link https://php.net/manual/en/splfixedarray.offsetget.php * - * @param int $index The index with the value + * @param int $offset The index with the value * @return TValue The value at the specified index * * @since 5.3.0 */ - public function offsetGet(int $index) {} + public function offsetGet(int $offset) {} } From 82ff58228092cb15f6bd17adfba5aa607179a4a9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:29:54 +0100 Subject: [PATCH 47/58] add error for invalid array key type in docblock --- src/Psalm/Internal/Type/TypeParser.php | 58 ++++++++++++++++++++++++++ tests/AnnotationTest.php | 10 ++++- tests/ArrayAssignmentTest.php | 9 ++-- tests/KeyOfArrayTest.php | 18 ++++---- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 770e7efc958..b498c9944e5 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -50,11 +50,13 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPropertiesOf; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; @@ -643,6 +645,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TArray($generic_params, $from_docblock); } @@ -671,6 +701,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index b06bd217e7f..e0855c3bdc8 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1366,7 +1366,15 @@ public function barBar() { }', 'error_message' => 'MissingDocblockType', ], - + 'invalidArrayKeyType' => [ + 'code' => ' $arg + * @return void + */ + function foo($arg) {}', + 'error_message' => 'InvalidDocblock', + ], 'invalidClassMethodReturnBrackets' => [ 'code' => ' [ 'code' => ' 'InvalidArrayOffset', + 'error_message' => 'MixedArrayAccess', + 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' [ 'code' => '|array> + * @return key-of|array> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asString) { + if ($asString) { + return "42"; } return 42; } @@ -194,14 +194,14 @@ public function getKey() { ', 'error_message' => 'InvalidReturnStatement', ], - 'noStringAllowedInKeyOfIntFloatArray' => [ + 'noStringAllowedInKeyOfIntFloatStringArray' => [ 'code' => '|array> + * @return key-of|array<"42.0", string>> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asInt) { + if ($asInt) { + return 42; } return "42"; } From 9be7fceb594eb1fdfe9886c39c54db48a815afca Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:17:43 +0100 Subject: [PATCH 48/58] Fix literal string keys int not handled as int as PHP does Fix https://github.com/vimeo/psalm/issues/8680 See also https://github.com/vimeo/psalm/issues/9295 --- src/Psalm/Internal/Type/TypeParser.php | 29 ++++++++++++++ tests/ArrayAssignmentTest.php | 23 +++++++++++ tests/ArrayKeysTest.php | 55 ++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index b498c9944e5..58718eae2dd 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -48,6 +48,7 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; 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; @@ -85,6 +86,7 @@ use function defined; use function end; use function explode; +use function filter_var; use function get_class; use function in_array; use function is_int; @@ -98,6 +100,9 @@ use function strtolower; use function strtr; use function substr; +use function trim; + +use const FILTER_VALIDATE_INT; /** * @psalm-suppress InaccessibleProperty Allowed during construction @@ -646,6 +651,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey @@ -702,6 +719,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 769ebadc290..fa65da8223c 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -2115,6 +2115,29 @@ function getQueryParams(): array return $queryParams; }', ], + 'stringIntKeys' => [ + 'code' => ' $arg + * @return bool + */ + function foo($arg) { + foreach ($arg as $k => $v) { + if ( $k === 15 ) { + return true; + } + + if ( $k === 17 ) { + return false; + } + } + + return true; + } + + $x = ["15" => "a", 17 => "b"]; + foo($x);', + ], ]; } diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index 070ea52873b..e3f9cc897a4 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -97,6 +97,33 @@ function getKey() { } ', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + ], ]; } @@ -126,6 +153,34 @@ function getKeys() { ', 'error_message' => 'InvalidReturnStatement', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + 'error_message' => 'InvalidReturnStatement', + ], ]; } } From 108f6267124377263ac1c99e5f22e105367b0842 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:59:26 +0100 Subject: [PATCH 49/58] fix literal int/string comparisons only using one literal Fix https://github.com/vimeo/psalm/issues/9552 --- .../Statements/Expression/AssertionFinder.php | 9 ++++++-- tests/TypeReconciliation/ConditionalTest.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index de4d2022aaa..740994506ed 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -555,8 +555,13 @@ private static function scrapeEqualityAssertions( $var_assertion_different = $var_type->getId() !== $intersection_type->getId(); + $all_assertions = []; + foreach ($intersection_type->getAtomicTypes() as $atomic_type) { + $all_assertions[] = new IsIdentical($atomic_type); + } + if ($var_name_left && $var_assertion_different) { - $if_types[$var_name_left] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_left] = [$all_assertions]; } $var_name_right = ExpressionIdentifier::getExtendedVarId( @@ -568,7 +573,7 @@ private static function scrapeEqualityAssertions( $other_assertion_different = $other_type->getId() !== $intersection_type->getId(); if ($var_name_right && $other_assertion_different) { - $if_types[$var_name_right] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_right] = [$all_assertions]; } return $if_types ? [$if_types] : []; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index ca69a70a358..819d871eebb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -580,6 +580,29 @@ function foo($a, string $b) : void { } }', ], + 'reconcileMultipleLiteralStrings' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:10:22 +0100 Subject: [PATCH 50/58] Fix https://psalm.dev/r/aada187f50 where 2 union types are not intersected and the condition contains both types --- .../Statements/Expression/AssertionFinder.php | 2 +- tests/TypeReconciliation/ConditionalTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 740994506ed..4b81cb4adb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -544,7 +544,7 @@ private static function scrapeEqualityAssertions( // both side of the Identical can be asserted to the intersection of both $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); - if ($intersection_type !== null && $intersection_type->isSingle()) { + if ($intersection_type !== null) { $if_types = []; $var_name_left = ExpressionIdentifier::getExtendedVarId( diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 819d871eebb..c8f3f9ab7bb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -603,6 +603,21 @@ function foo($param, $param2) { } }', ], + 'reconcileMultipleUnionIntersection' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:43:55 +0100 Subject: [PATCH 51/58] fix bug equality assertion with int and float setting wrong type - required so previous commit works --- .../Statements/Expression/AssertionFinder.php | 2 +- src/Psalm/Type.php | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 4b81cb4adb9..8c6aaa03ce3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -542,7 +542,7 @@ private static function scrapeEqualityAssertions( ); } else { // both side of the Identical can be asserted to the intersection of both - $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); + $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase, false, false); if ($intersection_type !== null) { $if_types = []; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 6903c94094a..1215799f785 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -712,7 +712,9 @@ public static function combineUnionTypes( public static function intersectUnionTypes( ?Union $type_1, ?Union $type_2, - Codebase $codebase + Codebase $codebase, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -766,6 +768,8 @@ public static function intersectUnionTypes( $type_2_atomic, $codebase, $intersection_performed, + $allow_interface_equality, + $allow_float_int_equality, ); if (null !== $intersection_atomic) { @@ -838,7 +842,9 @@ private static function intersectAtomicTypes( Atomic $type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, - bool &$intersection_performed + bool &$intersection_performed, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Atomic { $intersection_atomic = null; $wider_type = null; @@ -884,6 +890,8 @@ private static function intersectAtomicTypes( $codebase, $type_2_atomic, $type_1_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; @@ -892,6 +900,8 @@ private static function intersectAtomicTypes( $codebase, $type_1_atomic, $type_2_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; From af3978281edc7e4feec1205b72fb31cbf725d27c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:05:48 +0100 Subject: [PATCH 52/58] remove previously broken test https://github.com/vimeo/psalm/issues/10487 --- tests/TypeReconciliation/ValueTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 10df002174c..527b4f6f76f 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -272,17 +272,6 @@ function foo($f) : void { if ($s === "a") {} }', ], - 'moreValueReconciliation' => [ - 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 15:10:15 +0100 Subject: [PATCH 53/58] add missing phpdoc in new tests --- tests/TypeReconciliation/ConditionalTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index c8f3f9ab7bb..99650002db3 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -608,6 +608,7 @@ function foo($param, $param2) { /** * @param int|string $param * @param float|string $param2 + * @return void */ function foo($param, $param2) { if ($param === $param2) { @@ -616,7 +617,7 @@ function foo($param, $param2) { } } - function takesString(string $arg) {}', + function takesString(string $arg): void {}', ], 'reconcileNullableStringWithWeakEquality' => [ 'code' => ' Date: Wed, 13 Dec 2023 15:30:43 +0100 Subject: [PATCH 54/58] Fix https://github.com/vimeo/psalm/issues/9267 --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 2 +- tests/FunctionCallTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 706534fbbe5..a1df71add81 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -902,7 +902,7 @@ public static function verifyType( $input_type, $param_type, true, - true, + !isset($param_type->getAtomicTypes()['true']), $union_comparison_results, ); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index f712af9dacd..2b73d655018 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -2358,6 +2358,17 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidArgument', ], + 'invalidArgumentFalseTrueExpected' => [ + 'code' => ' 'InvalidArgument', + ], 'builtinFunctioninvalidArgumentWithWeakTypes' => [ 'code' => ' Date: Thu, 14 Dec 2023 09:44:28 +0300 Subject: [PATCH 55/58] Fix Uncaught RuntimeException: PHP Error: Uninitialized string offset 0 when $pattern is empty --- .../Call/FunctionCallReturnTypeFetcher.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index e15fd22a71d..8695053c6f8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -636,17 +636,19 @@ private static function taintReturnType( $first_arg_value = $first_stmt_type->getSingleStringLiteral()->value; $pattern = substr($first_arg_value, 1, -1); + if (strlen(trim($pattern)) > 0) { + $pattern = trim($pattern); + if ($pattern[0] === '[' + && $pattern[1] === '^' + && substr($pattern, -1) === ']' + ) { + $pattern = substr($pattern, 2, -1); - if ($pattern[0] === '[' - && $pattern[1] === '^' - && substr($pattern, -1) === ']' - ) { - $pattern = substr($pattern, 2, -1); - - if (self::simpleExclusion($pattern, $first_arg_value[0])) { - $removed_taints[] = 'html'; - $removed_taints[] = 'has_quotes'; - $removed_taints[] = 'sql'; + if (self::simpleExclusion($pattern, $first_arg_value[0])) { + $removed_taints[] = 'html'; + $removed_taints[] = 'has_quotes'; + $removed_taints[] = 'sql'; + } } } } From c8748dc5c976cc52940b93b6c3689869c1363978 Mon Sep 17 00:00:00 2001 From: mu3ic Date: Thu, 14 Dec 2023 09:54:32 +0300 Subject: [PATCH 56/58] Add trim() in global use --- .../Statements/Expression/Call/FunctionCallReturnTypeFetcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 8695053c6f8..55557576e6b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -51,6 +51,7 @@ use function strpos; use function strtolower; use function substr; +use function trim; /** * @internal From d6cf9faebbfc0c21710b99eae01e9d4188aae76e Mon Sep 17 00:00:00 2001 From: Antonio del Olmo Date: Fri, 15 Dec 2023 11:14:53 +0100 Subject: [PATCH 57/58] Add support for Override attribute --- stubs/CoreGenericAttributes.phpstub | 6 ++++++ tests/AttributeTest.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/stubs/CoreGenericAttributes.phpstub b/stubs/CoreGenericAttributes.phpstub index 7aa6400df06..92abe9542f8 100644 --- a/stubs/CoreGenericAttributes.phpstub +++ b/stubs/CoreGenericAttributes.phpstub @@ -6,6 +6,12 @@ final class AllowDynamicProperties public function __construct() {} } +#[Attribute(Attribute::TARGET_METHOD)] +final class Override +{ + public function __construct() {} +} + #[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index f1051773882..ddb5b1f5fd9 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -293,6 +293,22 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], + 'override' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'sensitiveParameter' => [ 'code' => ' Date: Sun, 17 Dec 2023 16:06:03 +0100 Subject: [PATCH 58/58] strtok always returns a non-empty-string when it does not return false --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 5f3dba9c880..853b78ed8a0 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12923,8 +12923,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'?int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'string'=>'string'], +'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'?int'], 'strtoupper' => ['string', 'string'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 864b668bd0b..60e798e09cb 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14333,8 +14333,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], - 'strtok\'1' => ['string|false', 'string'=>'string'], + 'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], + 'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'int'], 'strtoupper' => ['string', 'string'=>'string'],