diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index f0cfe0b6878..cc4483cd1f5 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; @@ -688,12 +689,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/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 05fcc2931c2..42af2598ecc 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -128,6 +128,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/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 002e63548f6..31b63573c94 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -429,7 +429,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 92f47dfa3a2..22442e8e1c5 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -1944,6 +1944,33 @@ 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 + * @param mixed $actual + * @psalm-assert =T $actual + */ + public function assertSame($expected, $actual): void + { + return; + } + } + ', + ], ]; } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index b0a610b368b..add6331882a 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -316,6 +316,15 @@ function foo(callable $c): void { $c2 = new C(); $c2();', ], + 'invokeMethodExists' => [ + ' [ ' [ + ' [], + [], + '8.1', + ], ]; } @@ -1171,9 +1177,8 @@ public static function __callStatic(string $name, array $args): mixed { 'error_message' => 'MixedAssignment', [], false, - '8.1' + '8.1', ], - ]; } }