From d10e384338abc1513efb7ce70050f02b0bb0fa89 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 02:24:27 +0100 Subject: [PATCH 1/2] Report first class callables generated for unknown static methods Fixes vimeo/psalm#10170 --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 14 +++++++++++++- tests/MethodCallTest.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) 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 ad9fd724f4e..0b1b187b8d7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -30,6 +30,7 @@ use Psalm\Issue\InvalidStringClass; use Psalm\Issue\MixedMethodCall; use Psalm\Issue\UndefinedClass; +use Psalm\Issue\UndefinedMethod; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualArray; use Psalm\Node\Expr\VirtualArrayItem; @@ -490,6 +491,7 @@ private static function handleNamedCall( $method_name_lc, ); + if ($stmt->isFirstClassCallable()) { if ($found_method_and_class_storage) { [ $method_storage ] = $found_method_and_class_storage; @@ -516,7 +518,17 @@ private static function handleNamedCall( $codebase->methods->getStorage($declaring_method_id)->pure, )]); } else { - // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + if (IssueBuffer::accepts( + new UndefinedMethod( + 'Method ' . $method_id . ' does not exist', + new CodeLocation($statements_analyzer, $stmt), + (string) $method_id, + ), + $statements_analyzer->getSuppressedIssues(), + )) { + return false; + } + $return_type_candidate = Type::getClosure(); } } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 49eebbf7546..def613cc537 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1805,6 +1805,22 @@ public function foo(callable $_a = "strlen"): void {} PHP, 'error_message' => 'TooManyArguments', ], + 'firstClassCallableWithUnknownStaticMethod' => [ + 'code' => <<<'PHP' + 'UndefinedMethod', + ], + 'firstClassCallableWithUnknownInstanceMethod' => [ + 'code' => <<<'PHP' + foo(...); + PHP, + 'error_message' => 'UndefinedMethod', + ], ]; } } From a8c093aea29552d4f34f720efaaab6876b9120ee Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 02:45:24 +0100 Subject: [PATCH 2/2] Handle taking references to unspecified magic methods --- .../Call/StaticMethod/AtomicStaticCallAnalyzer.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 0b1b187b8d7..8e187ab391f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -517,6 +517,19 @@ private static function handleNamedCall( $codebase->getMethodReturnType($method_id, $fq_class_name), $codebase->methods->getStorage($declaring_method_id)->pure, )]); + } elseif ($codebase->methodExists( + $call_static_method_id = new MethodIdentifier($method_id->fq_class_name, '__callstatic'), + new CodeLocation($statements_analyzer, $stmt), + null, + null, + false, + )) { + $return_type_candidate = new Union([new TClosure( + 'Closure', + null, + $codebase->getMethodReturnType($call_static_method_id, $fq_class_name), + $codebase->methods->getStorage($call_static_method_id)->pure, + )]); } else { if (IssueBuffer::accepts( new UndefinedMethod(