diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 6b85661be..0861db4b2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -10,6 +10,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator; use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -258,6 +259,33 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer false ); + if ($naive_method_exists && $fq_class_name === 'Closure' && $method_name_lc === '__invoke') { + $old_node_data = $statements_analyzer->node_data; + $statements_analyzer->node_data = clone $statements_analyzer->node_data; + + $fake_function_call = new PhpParser\Node\Expr\FuncCall( + $stmt->var, + $stmt->args, + $stmt->getAttributes() + ); + $ret_value = FunctionCallAnalyzer::analyze( + $statements_analyzer, + $fake_function_call, + $context + ); + + $function_return = $statements_analyzer->node_data->getType($fake_function_call); + $statements_analyzer->node_data = $old_node_data; + + if (!$result->return_type) { + $result->return_type = $function_return; + } else { + $result->return_type = Type::combineUnionTypes($function_return, $result->return_type); + } + + return; + } + $fake_method_exists = false; if (!$naive_method_exists diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php index 2713b455c..283e353ce 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php @@ -35,40 +35,42 @@ class ClosureFromCallableReturnTypeProvider implements \Psalm\Plugin\Hook\Method if (!$source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) { return; } + $type_provider = $source->getNodeTypeProvider(); $codebase = $source->getCodebase(); - $closure_types = []; + if ($method_name_lowercase === 'fromcallable') { + $closure_types = []; - if ($method_name_lowercase === 'fromcallable' - && isset($call_args[0]) - && ($input_type = $type_provider->getType($call_args[0]->value)) - ) { - foreach ($input_type->getAtomicTypes() as $atomic_type) { - $candidate_callable = CallableTypeComparator::getCallableFromAtomic( - $codebase, - $atomic_type, - null, - $source - ); - - if ($candidate_callable) { - $closure_types[] = new Type\Atomic\TFn( - 'Closure', - $candidate_callable->params, - $candidate_callable->return_type, - $candidate_callable->is_pure + if (isset($call_args[0]) + && ($input_type = $type_provider->getType($call_args[0]->value)) + ) { + foreach ($input_type->getAtomicTypes() as $atomic_type) { + $candidate_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $atomic_type, + null, + $source ); - } else { - return Type::getClosure(); + + if ($candidate_callable) { + $closure_types[] = new Type\Atomic\TFn( + 'Closure', + $candidate_callable->params, + $candidate_callable->return_type, + $candidate_callable->is_pure + ); + } else { + return Type::getClosure(); + } } } - } - if ($closure_types) { - return TypeCombination::combineTypes($closure_types, $codebase); - } + if ($closure_types) { + return TypeCombination::combineTypes($closure_types, $codebase); + } - return Type::getClosure(); + return Type::getClosure(); + } } } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index fbd444ed1..e749a00a2 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -492,6 +492,24 @@ class ClosureTest extends TestCase })() );' ], + 'callingInvokeOnClosureIsSameAsCallingDirectly' => [ + 'a = fn(int $a) : int => $a + 5; + } + + public function invoker(int $b) : int { + return $this->a->__invoke($b); + } + }', + 'assertions' => [], + 'error_levels' => [], + '7.4' + ], ]; }