From f3d67845c5aea0057b3838df1172f8d8a1ec7fc8 Mon Sep 17 00:00:00 2001 From: someniatko Date: Thu, 4 Aug 2022 16:50:38 +0300 Subject: [PATCH 1/2] #8363 - ensure we recognize inherited static methods for the first-class callables --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 67 +++++++++++-------- tests/ClosureTest.php | 21 ++++++ 2 files changed, 61 insertions(+), 27 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 673dc56c505..a147c4aa37a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -56,6 +56,7 @@ use function array_filter; use function array_map; +use function array_values; use function assert; use function count; use function in_array; @@ -369,39 +370,15 @@ private static function handleNamedCall( if (!$naive_method_exists && $codebase->methods->existence_provider->has($fq_class_name) ) { - $method_exists = $codebase->methods->existence_provider->doesMethodExist( + $fake_method_exists = $codebase->methods->existence_provider->doesMethodExist( $fq_class_name, $method_id->method_name, $statements_analyzer, null - ); - - if ($method_exists) { - $fake_method_exists = true; - } - } - - if ($stmt->isFirstClassCallable()) { - $method_storage = ($class_storage->methods[$method_name_lc] ?? - ($class_storage->pseudo_static_methods[$method_name_lc] ?? null)); - - if ($method_storage) { - $return_type_candidate = new Union([new TClosure( - 'Closure', - $method_storage->params, - $method_storage->return_type, - $method_storage->pure - )]); - } else { - $return_type_candidate = Type::getClosure(); - } - - $statements_analyzer->node_data->setType($stmt, $return_type_candidate); - - return true; + ) ?? false; } - $args = $stmt->getArgs(); + $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); if (!$naive_method_exists && $class_storage->mixin_declaring_fqcln @@ -504,6 +481,42 @@ private static function handleNamedCall( $method_name_lc ); + if ($stmt->isFirstClassCallable()) { + if ($found_method_and_class_storage) { + [ $method_storage ] = $found_method_and_class_storage; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + $method_storage->params, + $method_storage->return_type, + $method_storage->pure + )]); + } else { + $method_exists = $naive_method_exists + || $fake_method_exists + || isset($class_storage->methods[$method_name_lc]) + || isset($class_storage->pseudo_static_methods[$method_name_lc]); + + if ($method_exists) { + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id) ?? $method_id; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + array_values($codebase->getMethodParams($method_id)), + $codebase->getMethodReturnType($method_id, $fq_class_name), + $codebase->methods->getStorage($declaring_method_id)->pure + )]); + } else { + // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + $return_type_candidate = Type::getClosure(); + } + } + + $statements_analyzer->node_data->setType($stmt, $return_type_candidate); + + return true; + } + if (!$naive_method_exists || !MethodAnalyzer::isMethodVisible( $method_id, diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index cea843a3725..e6e5699600e 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -751,6 +751,27 @@ public static function __callStatic(string $name, array $args): mixed { [], '8.1' ], + 'FirstClassCallable:OverriddenStaticMethod' => [ + ' [], + [], + '8.1', + ], 'FirstClassCallable:WithArrayMap' => [ ' Date: Thu, 4 Aug 2022 17:16:06 +0300 Subject: [PATCH 2/2] #8363 - support `static` as a type parameter in return types of the first-class callables --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 13 +++++++- tests/ClosureTest.php | 30 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 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 a147c4aa37a..a4c33c24c08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -512,7 +512,18 @@ private static function handleNamedCall( } } - $statements_analyzer->node_data->setType($stmt, $return_type_candidate); + $expanded_return_type = TypeExpander::expandUnion( + $codebase, + $return_type_candidate, + $context->self, + $class_storage->name, + $context->parent, + true, + false, + true + ); + + $statements_analyzer->node_data->setType($stmt, $expanded_return_type); return true; } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index e6e5699600e..53e5cc3f9b2 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -751,7 +751,7 @@ public static function __callStatic(string $name, array $args): mixed { [], '8.1' ], - 'FirstClassCallable:OverriddenStaticMethod' => [ + 'FirstClassCallable:InheritedStaticMethod' => [ ' [ + ' */ + public static function create(int $i): Holder + { + return new Holder(new static($i)); + } + } + + class C extends A {} + + /** @param \Closure(int):Holder $_ */ + function takesIntToHolder(\Closure $_): void {} + + takesIntToHolder(C::create(...));' + ], 'FirstClassCallable:WithArrayMap' => [ '