From 7e2b9d0fab3f2bae0ec1c6409ab165da501f0b6d Mon Sep 17 00:00:00 2001 From: orklah Date: Wed, 5 Jan 2022 22:26:35 +0100 Subject: [PATCH 1/4] fix empty string in keyed array offset --- src/Psalm/Type/Atomic/TKeyedArray.php | 2 +- tests/AssertAnnotationTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index aea960c8346..c37c07d5fd0 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -430,7 +430,7 @@ public function getList(): TNonEmptyList */ private function escapeAndQuote($name) { - if (is_string($name) && preg_match('/[^a-zA-Z0-9_]/', $name)) { + if (is_string($name) && ($name === '' || preg_match('/[^a-zA-Z0-9_]/', $name))) { $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; } diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index ade990073f5..6c6d6334aa1 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -1921,6 +1921,32 @@ function assertString(A $arg): bool {return $arg->b !== null;} function requiresString(string $_str): void {} ', ], + 'assertWithEmptyStringOnKeyedArray' => [ + ' ""]; + + /** @var array $b */ + $b = []; + + $this->assertSame($a, $b); + } + + /** + * @template T + * @param T $expected + * @psalm-assert =T $actual + */ + public function assertSame(mixed $expected, mixed $actual): void + { + return; + } + } + ', + ], ]; } From 72216f9354012eb0fe9315ee6cf53dc2cdf1bfc6 Mon Sep 17 00:00:00 2001 From: orklah Date: Wed, 5 Jan 2022 22:36:22 +0100 Subject: [PATCH 2/4] fix test --- tests/AssertAnnotationTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 6c6d6334aa1..09fed59b365 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -1938,9 +1938,10 @@ function test(): void /** * @template T * @param T $expected + * @param mixed $actual * @psalm-assert =T $actual */ - public function assertSame(mixed $expected, mixed $actual): void + public function assertSame($expected, $actual): void { return; } From fe036d4db0f5d3ae9af7a83d6fe38195779a8d26 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Thu, 6 Jan 2022 10:38:58 +0200 Subject: [PATCH 3/4] Prevent crashes on `array_map(...)` Fixes vimeo/psalm#7305 --- .../Internal/PhpVisitor/Reflector/ExpressionScanner.php | 4 ++++ tests/ClosureTest.php | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 6e88891a20a..2a6dbf71eb9 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -125,6 +125,10 @@ private static function registerClassMapFunctionCall( } } + if ($node->isFirstClassCallable()) { + return; + } + if ($function_id === 'define') { $first_arg_value = isset($node->getArgs()[0]) ? $node->getArgs()[0]->value : null; $second_arg_value = isset($node->getArgs()[1]) ? $node->getArgs()[1]->value : null; diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 64a17a53d28..f0ca9bb1fe5 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -729,6 +729,12 @@ public static function __callStatic(string $name, array $args): mixed { [], '8.1' ], + 'FirstClassCallable:array_map' => [ + ' [], + [], + '8.1', + ], ]; } @@ -1171,9 +1177,8 @@ public static function __callStatic(string $name, array $args): mixed { 'error_message' => 'MixedAssignment', [], false, - '8.1' + '8.1', ], - ]; } } From b9d8dd9d3f01ba145582f77b7eecb65d07b280c2 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 6 Jan 2022 21:22:18 +0100 Subject: [PATCH 4/4] Fix analysis when __invoke() exists --- .../Statements/Expression/Call/FunctionCallAnalyzer.php | 6 +++--- tests/CallableTest.php | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index becd4b5ea11..9b6bf31b1ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -52,6 +52,7 @@ use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Reconciler; @@ -690,12 +691,11 @@ private static function getAnalyzeNamedExpression( ); } elseif ($var_type_part instanceof TCallableObject || $var_type_part instanceof TCallableString + || ($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure') + || ($var_type_part instanceof TObjectWithProperties && isset($var_type_part->methods['__invoke'])) ) { // this is fine $has_valid_function_call_type = true; - } elseif (($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure')) { - // this is fine - $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TString || $var_type_part instanceof TArray || $var_type_part instanceof TList diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 3864ec7a00c..65fa422056c 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -316,6 +316,15 @@ function foo(callable $c): void { $c2 = new C(); $c2();', ], + 'invokeMethodExists' => [ + ' [ '