From 00ce1107d3ffd2def619a94832c1370b646bcd51 Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 03:02:21 +0300 Subject: [PATCH 01/13] Infer partially applied closure arg by previous function arg --- .../Expression/Call/ArgumentsAnalyzer.php | 95 ++++++++++++++++++- .../Expression/Call/FunctionCallAnalyzer.php | 17 +++- .../Statements/ExpressionAnalyzer.php | 16 ++-- tests/CallableTest.php | 84 ++++++++++++++++ 4 files changed, 200 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a05c8a18d57..2d9c127a3a6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -54,6 +54,7 @@ use function array_map; use function array_reverse; use function array_slice; +use function assert; use function count; use function in_array; use function is_string; @@ -196,7 +197,19 @@ public static function analyze( $toggled_class_exists = true; } - if (($arg->value instanceof PhpParser\Node\Expr\Closure + $high_order_template_result = null; + + if ($arg->value instanceof PhpParser\Node\Expr\FuncCall + && $param + && !$arg->value->getDocComment() + ) { + $high_order_template_result = self::handlePartiallyAppliedClosureArg( + $statements_analyzer, + $arg->value, + $template_result ?? new TemplateResult([], []), + $param + ); + } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction) && $param && !$arg->value->getDocComment() @@ -217,7 +230,15 @@ public static function analyze( $context->inside_call = true; - if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) { + if (ExpressionAnalyzer::analyze( + $statements_analyzer, + $arg->value, + $context, + false, + null, + false, + $high_order_template_result + ) === false) { $context->inside_call = $was_inside_call; return false; @@ -315,6 +336,76 @@ private static function handleArrayMapFilterArrayArg( } } + private static function handlePartiallyAppliedClosureArg( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Expr\FuncCall $func_call, + TemplateResult $inferred_template_result, + FunctionLikeParameter $param + ): ?TemplateResult { + $codebase = $statements_analyzer->getCodebase(); + $function_id = strtolower((string) $func_call->name->getAttribute('resolvedName')); + + if (empty($function_id)) { + return null; + } + + try { + $storage = $codebase->functions->getStorage($statements_analyzer, $function_id); + } catch (UnexpectedValueException $e) { + return null; + } + + if (!$storage->return_type || !$storage->return_type->isSingle()) { + return null; + } + + $return_type = $storage->return_type->getSingleAtomic(); + + if (!$return_type instanceof TClosure && !$return_type instanceof TCallable) { + return null; + } + + if (!$param->type || !$param->type->isSingle()) { + return null; + } + + $param_type = $param->type->getSingleAtomic(); + + if (!$param_type instanceof TClosure && !$param_type instanceof TCallable) { + return null; + } + + $replaced_type = clone $param->type; + + TemplateInferredTypeReplacer::replace( + $replaced_type, + $inferred_template_result, + $codebase + ); + + $param_type = $replaced_type->getSingleAtomic(); + assert($param_type instanceof TClosure || $param_type instanceof TCallable); + + $high_order_template_result = new TemplateResult($storage->template_types ?: [], []); + + foreach ($return_type->params ?? [] as $offset => $param) { + if ($param->type && + $param->type->getTemplateTypes() && + isset($param_type->params[$offset]) + ) { + TemplateStandinTypeReplacer::replace( + clone $param->type, + $high_order_template_result, + $codebase, + null, + $param_type->params[$offset]->type + ); + } + } + + return $high_order_template_result; + } + /** * @param array $args */ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 9b6bf31b1ea..264c10fb2e4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -82,7 +82,8 @@ class FunctionCallAnalyzer extends CallAnalyzer public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\FuncCall $stmt, - Context $context + Context $context, + ?TemplateResult $template_result = null ): bool { $function_name = $stmt->name; @@ -166,10 +167,12 @@ public static function analyze( } if (!$is_first_class_callable) { - $template_result = null; - if (isset($function_call_info->function_storage->template_types)) { - $template_result = new TemplateResult($function_call_info->function_storage->template_types ?: [], []); + if (!$template_result) { + $template_result = new TemplateResult([], []); + } + + $template_result->template_types += $function_call_info->function_storage->template_types ?: []; } ArgumentsAnalyzer::analyze( @@ -205,6 +208,10 @@ public static function analyze( } } + $already_inferred_lower_bounds = $template_result + ? $template_result->lower_bounds + : []; + $template_result = new TemplateResult([], []); // do this here to allow closure param checks @@ -229,6 +236,8 @@ public static function analyze( $function_call_info->function_id ); + $template_result->lower_bounds += $already_inferred_lower_bounds; + if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { $stmt_type = FunctionCallReturnTypeFetcher::fetch( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 0ce2af836e8..5ffa52a5aea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -47,6 +47,7 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\FileManipulation\FileManipulationBuffer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\UnrecognizedExpression; use Psalm\IssueBuffer; @@ -70,7 +71,8 @@ public static function analyze( Context $context, bool $array_assignment = false, ?Context $global_context = null, - bool $from_stmt = false + bool $from_stmt = false, + ?TemplateResult $template_result = null ): bool { $codebase = $statements_analyzer->getCodebase(); @@ -80,9 +82,9 @@ public static function analyze( $context, $array_assignment, $global_context, - $from_stmt - ) === false - ) { + $from_stmt, + $template_result + ) === false) { return false; } @@ -144,7 +146,8 @@ private static function handleExpression( Context $context, bool $array_assignment, ?Context $global_context, - bool $from_stmt + bool $from_stmt, + ?TemplateResult $template_result = null ): bool { if ($stmt instanceof PhpParser\Node\Expr\Variable) { return VariableFetchAnalyzer::analyze( @@ -295,7 +298,8 @@ private static function handleExpression( return FunctionCallAnalyzer::analyze( $statements_analyzer, $stmt, - $context + $context, + $template_result ); } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 65fa422056c..917099a0f17 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -233,6 +233,90 @@ function ($i) { '$inferred' => 'list', ], ], + 'inferPartiallyAppliedClosureArgByPreviousFunctionArg' => [ + '): list + */ + function filter(callable $_predicate): Closure { throw new RuntimeException("???"); } + /** + * @template A + * @template B + * + * @param callable(A): B $_ab + * @return Closure(list): list + */ + function map(callable $_ab): Closure { throw new RuntimeException("???"); } + /** + * @template T + * @return (Closure(list): (non-empty-list | null)) + */ + function asNonEmptyList(): Closure { throw new RuntimeException("???"); } + /** + * @template T + * @return Closure(T): T + */ + function id(): Closure { throw new RuntimeException("???"); } + + /** + * @template A + * @template B + * @template C + * @template D + * @template E + * @template F + * + * @param A $arg + * @param callable(A): B $ab + * @param callable(B): C $bc + * @param callable(C): D $cd + * @param callable(D): E $de + * @param callable(E): F $ef + * @return F + */ + function pipe4(mixed $arg, callable $ab, callable $bc, callable $cd, callable $de, callable $ef): mixed + { + return $ef($de($cd($bc($ab($arg))))); + } + + /** + * @template TFoo of string + * @template TBar of bool + */ + final class Item + { + /** + * @param TFoo $foo + * @param TBar $bar + */ + public function __construct( + public string $foo, + public bool $bar, + ) { } + } + + /** + * @return list + */ + function getList(): array { return []; } + + $result = pipe4( + getList(), + filter(fn($i) => $i->bar), + filter(fn(Item $i) => $i->foo !== "bar"), + map(fn($i) => new Item("test: " . $i->foo, $i->bar)), + asNonEmptyList(), + id(), + );', + 'assertions' => [ + '$result' => 'non-empty-list>|null', + ], + 'error_levels' => [], + '8.0', + ], 'varReturnType' => [ ' Date: Tue, 18 Jan 2022 12:44:36 +0300 Subject: [PATCH 02/13] Some refactoring and doc for high order function argument handling --- .../Expression/Call/ArgumentsAnalyzer.php | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 2d9c127a3a6..1db75db95bc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -205,8 +205,8 @@ public static function analyze( ) { $high_order_template_result = self::handlePartiallyAppliedClosureArg( $statements_analyzer, - $arg->value, $template_result ?? new TemplateResult([], []), + $arg->value, $param ); } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure @@ -336,14 +336,37 @@ private static function handleArrayMapFilterArrayArg( } } + /** + * Compiles TemplateResult for high-order functions ($func_call) + * by previous template args ($inferred_template_result). + * + * It's need for proper template replacement: + * + * ``` + * * template T + * * return Closure(T): T + * function id(): Closure { ... } + * + * * template A + * * template B + * * + * * param list $_items + * * param callable(A): B $_ab + * * return list + * function map(array $items, callable $ab): array { ... } + * + * $result = map([1, 2, 3], id()); + * // $result is list<1|2|3> because template T of id() was inferred by previous arg. + * ``` + */ private static function handlePartiallyAppliedClosureArg( StatementsAnalyzer $statements_analyzer, - PhpParser\Node\Expr\FuncCall $func_call, TemplateResult $inferred_template_result, - FunctionLikeParameter $param + PhpParser\Node\Expr\FuncCall $high_order_func_call, + FunctionLikeParameter $actual_func_param ): ?TemplateResult { $codebase = $statements_analyzer->getCodebase(); - $function_id = strtolower((string) $func_call->name->getAttribute('resolvedName')); + $function_id = strtolower((string) $high_order_func_call->name->getAttribute('resolvedName')); if (empty($function_id)) { return null; @@ -355,50 +378,45 @@ private static function handlePartiallyAppliedClosureArg( return null; } - if (!$storage->return_type || !$storage->return_type->isSingle()) { - return null; - } - - $return_type = $storage->return_type->getSingleAtomic(); - - if (!$return_type instanceof TClosure && !$return_type instanceof TCallable) { - return null; - } + $expected_hof_atomic = $storage->return_type && $storage->return_type->isSingle() + ? $storage->return_type->getSingleAtomic() + : null; - if (!$param->type || !$param->type->isSingle()) { + if (!$expected_hof_atomic instanceof TClosure && !$expected_hof_atomic instanceof TCallable) { return null; } - $param_type = $param->type->getSingleAtomic(); + $actual_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle() + ? $actual_func_param->type->getSingleAtomic() + : null; - if (!$param_type instanceof TClosure && !$param_type instanceof TCallable) { + if (!$actual_hof_atomic instanceof TClosure && !$actual_hof_atomic instanceof TCallable) { return null; } - $replaced_type = clone $param->type; + $replaced_actual_hof_atomic = clone $actual_func_param->type; TemplateInferredTypeReplacer::replace( - $replaced_type, + $replaced_actual_hof_atomic, $inferred_template_result, $codebase ); - $param_type = $replaced_type->getSingleAtomic(); - assert($param_type instanceof TClosure || $param_type instanceof TCallable); - + /** @var TClosure|TCallable $actual_hof_atomic */ + $actual_hof_atomic = $replaced_actual_hof_atomic->getSingleAtomic(); $high_order_template_result = new TemplateResult($storage->template_types ?: [], []); - foreach ($return_type->params ?? [] as $offset => $param) { - if ($param->type && - $param->type->getTemplateTypes() && - isset($param_type->params[$offset]) + foreach ($expected_hof_atomic->params ?? [] as $offset => $actual_func_param) { + if ($actual_func_param->type && + $actual_func_param->type->getTemplateTypes() && + isset($actual_hof_atomic->params[$offset]) ) { TemplateStandinTypeReplacer::replace( - clone $param->type, + clone $actual_func_param->type, $high_order_template_result, $codebase, null, - $param_type->params[$offset]->type + $actual_hof_atomic->params[$offset]->type ); } } From fac7115eaed89cd87ac5ddbf0e3f1aaef5f4b8ce Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 12:48:53 +0300 Subject: [PATCH 03/13] Add simple test for high order function arg inference --- tests/CallableTest.php | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 917099a0f17..05fc088bea6 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -233,7 +233,36 @@ function ($i) { '$inferred' => 'list', ], ], - 'inferPartiallyAppliedClosureArgByPreviousFunctionArg' => [ + 'inferTemplateOfHighOrderFunctionArgByPreviousArg' => [ + ' + */ + function getList() { throw new RuntimeException("???"); } + + /** + * @template T + * @return Closure(T): T + */ + function id() { throw new RuntimeException("???"); } + + /** + * @template A + * @template B + * + * @param list $_items + * @param callable(A): B $_ab + * @return list + */ + function map(array $_items, callable $_ab) { throw new RuntimeException("???"); } + + $result = map(getList(), id()); + ', + 'assertions' => [ + '$result' => 'list', + ], + ], + 'inferPipelineWithPartiallyAppliedFunctions' => [ ' Date: Tue, 18 Jan 2022 12:53:39 +0300 Subject: [PATCH 04/13] Fix psalm issues --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 1db75db95bc..4804bb90465 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -394,7 +394,7 @@ private static function handlePartiallyAppliedClosureArg( return null; } - $replaced_actual_hof_atomic = clone $actual_func_param->type; + $replaced_actual_hof_atomic = new Union([clone $actual_hof_atomic]); TemplateInferredTypeReplacer::replace( $replaced_actual_hof_atomic, From 3886d5b4c6642342addf4190e1a2076b073ceb33 Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 13:12:01 +0300 Subject: [PATCH 05/13] Add test for high order function arg inference in class context --- tests/CallableTest.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 05fc088bea6..e23e207f32b 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -262,6 +262,39 @@ function map(array $_items, callable $_ab) { throw new RuntimeException("???"); '$result' => 'list', ], ], + 'inferTemplateOfHighOrderFunctionArgByPreviousArgInClassContext' => [ + ' + */ + public function map(callable $ab) { throw new RuntimeException("???"); } + } + + /** + * @return ArrayList + */ + function getList() { throw new RuntimeException("???"); } + + /** + * @template T + * @return Closure(T): T + */ + function id() { throw new RuntimeException("???"); } + + $result = getList()->map(id()); + ', + 'assertions' => [ + '$result' => 'ArrayList', + ], + ], 'inferPipelineWithPartiallyAppliedFunctions' => [ ' Date: Tue, 18 Jan 2022 13:13:17 +0300 Subject: [PATCH 06/13] Rename method --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 4804bb90465..48be3301de5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -203,7 +203,7 @@ public static function analyze( && $param && !$arg->value->getDocComment() ) { - $high_order_template_result = self::handlePartiallyAppliedClosureArg( + $high_order_template_result = self::handleHighOrderFuncCallArg( $statements_analyzer, $template_result ?? new TemplateResult([], []), $arg->value, @@ -359,7 +359,7 @@ private static function handleArrayMapFilterArrayArg( * // $result is list<1|2|3> because template T of id() was inferred by previous arg. * ``` */ - private static function handlePartiallyAppliedClosureArg( + private static function handleHighOrderFuncCallArg( StatementsAnalyzer $statements_analyzer, TemplateResult $inferred_template_result, PhpParser\Node\Expr\FuncCall $high_order_func_call, From ffdf97c44dd6a0631dcd369fe59e1912a2462d8a Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 13:17:50 +0300 Subject: [PATCH 07/13] Remove unused function --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 48be3301de5..0c5c54b6869 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -54,7 +54,6 @@ use function array_map; use function array_reverse; use function array_slice; -use function assert; use function count; use function in_array; use function is_string; From 12648f4c9e2bb0abe3c18ea6b68b9f81ca66a4f6 Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 13:36:16 +0300 Subject: [PATCH 08/13] Comments for describe how works higher order func arg inference --- .../Expression/Call/ArgumentsAnalyzer.php | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 0c5c54b6869..d4ec7a78c15 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -354,8 +354,11 @@ private static function handleArrayMapFilterArrayArg( * * return list * function map(array $items, callable $ab): array { ... } * - * $result = map([1, 2, 3], id()); - * // $result is list<1|2|3> because template T of id() was inferred by previous arg. + * // list + * $numbers = [1, 2, 3]; + * + * $result = map($numbers, id()); + * // $result is list because template T of id() was inferred by previous arg. * ``` */ private static function handleHighOrderFuncCallArg( @@ -377,45 +380,57 @@ private static function handleHighOrderFuncCallArg( return null; } - $expected_hof_atomic = $storage->return_type && $storage->return_type->isSingle() + $input_hof_atomic = $storage->return_type && $storage->return_type->isSingle() ? $storage->return_type->getSingleAtomic() : null; - if (!$expected_hof_atomic instanceof TClosure && !$expected_hof_atomic instanceof TCallable) { + if (!$input_hof_atomic instanceof TClosure && !$input_hof_atomic instanceof TCallable) { return null; } - $actual_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle() + $container_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle() ? $actual_func_param->type->getSingleAtomic() : null; - if (!$actual_hof_atomic instanceof TClosure && !$actual_hof_atomic instanceof TCallable) { + if (!$container_hof_atomic instanceof TClosure && !$container_hof_atomic instanceof TCallable) { return null; } - $replaced_actual_hof_atomic = new Union([clone $actual_hof_atomic]); + $replaced_container_hof_atomic = new Union([clone $container_hof_atomic]); + // Replaces all input args in container function. + // + // For example: + // The map function expects callable(A):B as second param + // We know that previous arg type is list where the int is the A template. + // Then we can replace callable(A): B to callable(int):B using $inferred_template_result. TemplateInferredTypeReplacer::replace( - $replaced_actual_hof_atomic, + $replaced_container_hof_atomic, $inferred_template_result, $codebase ); - /** @var TClosure|TCallable $actual_hof_atomic */ - $actual_hof_atomic = $replaced_actual_hof_atomic->getSingleAtomic(); + /** @var TClosure|TCallable $container_hof_atomic */ + $container_hof_atomic = $replaced_container_hof_atomic->getSingleAtomic(); $high_order_template_result = new TemplateResult($storage->template_types ?: [], []); - foreach ($expected_hof_atomic->params ?? [] as $offset => $actual_func_param) { + // We can replace each templated param for the input function. + // Example: + // map($numbers, id()); + // We know that map expects callable(int):B because the $numbers is list. + // We know that id() returns callable(T):T. + // Then we can replace templated params sequentially using the expected callable(int):B. + foreach ($input_hof_atomic->params ?? [] as $offset => $actual_func_param) { if ($actual_func_param->type && $actual_func_param->type->getTemplateTypes() && - isset($actual_hof_atomic->params[$offset]) + isset($container_hof_atomic->params[$offset]) ) { TemplateStandinTypeReplacer::replace( clone $actual_func_param->type, $high_order_template_result, $codebase, null, - $actual_hof_atomic->params[$offset]->type + $container_hof_atomic->params[$offset]->type ); } } From 6e1957e40a04806d52ed349bc6247ad4ca60114a Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 18:45:28 +0300 Subject: [PATCH 09/13] Support higher order func arg inference for static and instance calls --- .../Expression/Call/ArgumentsAnalyzer.php | 77 +++++++++++++++---- .../Call/Method/AtomicMethodCallAnalyzer.php | 6 +- .../ExistingAtomicMethodCallAnalyzer.php | 7 +- .../Expression/Call/MethodCallAnalyzer.php | 7 +- .../Expression/Call/StaticCallAnalyzer.php | 6 +- .../StaticMethod/AtomicStaticCallAnalyzer.php | 13 +++- .../ExistingAtomicStaticCallAnalyzer.php | 7 +- .../Statements/ExpressionAnalyzer.php | 4 +- 8 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index d4ec7a78c15..40b0b0b5935 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -198,14 +198,16 @@ public static function analyze( $high_order_template_result = null; - if ($arg->value instanceof PhpParser\Node\Expr\FuncCall + if (($arg->value instanceof PhpParser\Node\Expr\FuncCall + || $arg->value instanceof PhpParser\Node\Expr\MethodCall + || $arg->value instanceof PhpParser\Node\Expr\StaticCall) && $param - && !$arg->value->getDocComment() + && $function_storage = self::getHighOrderFuncStorage($context, $statements_analyzer, $arg->value) ) { $high_order_template_result = self::handleHighOrderFuncCallArg( $statements_analyzer, $template_result ?? new TemplateResult([], []), - $arg->value, + $function_storage, $param ); } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure @@ -335,6 +337,59 @@ private static function handleArrayMapFilterArrayArg( } } + private static function getHighOrderFuncStorage( + Context $context, + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Expr\CallLike $function_like_call + ): ?FunctionLikeStorage { + $codebase = $statements_analyzer->getCodebase(); + + try { + if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall) { + $function_id = strtolower((string) $function_like_call->name->getAttribute('resolvedName')); + + if (empty($function_id)) { + return null; + } + + return $codebase->functions->getStorage($statements_analyzer, $function_id); + } + + if ($function_like_call instanceof PhpParser\Node\Expr\MethodCall && + $function_like_call->var instanceof PhpParser\Node\Expr\Variable && + isset($context->vars_in_scope['$' . $function_like_call->var->name]) + ) { + $lhs_type = $context->vars_in_scope['$' . $function_like_call->var->name]->getSingleAtomic(); + + if (!$lhs_type instanceof Type\Atomic\TNamedObject) { + return null; + } + + $method_id = new MethodIdentifier( + $lhs_type->value, + (string)$function_like_call->name + ); + + return $codebase->methods->getStorage($method_id); + } + + if ($function_like_call instanceof PhpParser\Node\Expr\StaticCall && + $function_like_call->name instanceof PhpParser\Node\Identifier + ) { + $method_id = new MethodIdentifier( + (string)$function_like_call->class->getAttribute('resolvedName'), + $function_like_call->name->name + ); + + return $codebase->methods->getStorage($method_id); + } + } catch (UnexpectedValueException $e) { + return null; + } + + return null; + } + /** * Compiles TemplateResult for high-order functions ($func_call) * by previous template args ($inferred_template_result). @@ -364,22 +419,9 @@ private static function handleArrayMapFilterArrayArg( private static function handleHighOrderFuncCallArg( StatementsAnalyzer $statements_analyzer, TemplateResult $inferred_template_result, - PhpParser\Node\Expr\FuncCall $high_order_func_call, + FunctionLikeStorage $storage, FunctionLikeParameter $actual_func_param ): ?TemplateResult { - $codebase = $statements_analyzer->getCodebase(); - $function_id = strtolower((string) $high_order_func_call->name->getAttribute('resolvedName')); - - if (empty($function_id)) { - return null; - } - - try { - $storage = $codebase->functions->getStorage($statements_analyzer, $function_id); - } catch (UnexpectedValueException $e) { - return null; - } - $input_hof_atomic = $storage->return_type && $storage->return_type->isSingle() ? $storage->return_type->getSingleAtomic() : null; @@ -397,6 +439,7 @@ private static function handleHighOrderFuncCallArg( } $replaced_container_hof_atomic = new Union([clone $container_hof_atomic]); + $codebase = $statements_analyzer->getCodebase(); // Replaces all input args in container function. // 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 7236562df6d..3bc8665ae2f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -74,7 +74,8 @@ public static function analyze( ?Atomic $static_type, bool $is_intersection, ?string $lhs_var_id, - AtomicMethodCallAnalysisResult $result + AtomicMethodCallAnalysisResult $result, + ?TemplateResult $inferred_template_result = null ): void { if ($lhs_type_part instanceof TTemplateParam && !$lhs_type_part->as->isMixed() @@ -438,7 +439,8 @@ public static function analyze( $static_type, $lhs_var_id, $method_id, - $result + $result, + $inferred_template_result ); $statements_analyzer->node_data = $old_node_data; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 26faf4e593e..d75792134aa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -65,7 +65,8 @@ public static function analyze( ?Atomic $static_type, ?string $lhs_var_id, MethodIdentifier $method_id, - AtomicMethodCallAnalysisResult $result + AtomicMethodCallAnalysisResult $result, + ?TemplateResult $inferred_template_result = null ): Union { $config = $codebase->config; @@ -217,6 +218,10 @@ public static function analyze( $template_result = new TemplateResult([], $class_template_params ?: []); $template_result->lower_bounds += $method_template_params; + if ($inferred_template_result) { + $template_result->lower_bounds += $inferred_template_result->lower_bounds; + } + if ($codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 04f0b96cd6a..7fdd1549ed8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Issue\InvalidMethodCall; use Psalm\Issue\InvalidScope; use Psalm\Issue\NullReference; @@ -43,7 +44,8 @@ public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\MethodCall $stmt, Context $context, - bool $real_method_call = true + bool $real_method_call = true, + ?TemplateResult $template_result = null ): bool { $was_inside_call = $context->inside_call; @@ -194,7 +196,8 @@ public static function analyze( : null, false, $lhs_var_id, - $result + $result, + $template_result ); if (isset($context->vars_in_scope[$lhs_var_id]) && ($possible_new_class_type = $context->vars_in_scope[$lhs_var_id]) instanceof Union diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index da43d41f250..9579e8055c2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -41,7 +41,8 @@ class StaticCallAnalyzer extends CallAnalyzer public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, - Context $context + Context $context, + ?TemplateResult $template_result = null ): bool { $method_id = null; @@ -219,7 +220,8 @@ public static function analyze( $lhs_type->ignore_nullable_issues, $moved_call, $has_mock, - $has_existing_method + $has_existing_method, + $template_result ); } 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 0130f030696..3e91aee6ef8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -22,6 +22,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\MethodIdentifier; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\ImpureMethodCall; @@ -71,7 +72,8 @@ public static function analyze( bool $ignore_nullable_issues, bool &$moved_call, bool &$has_mock, - bool &$has_existing_method + bool &$has_existing_method, + ?TemplateResult $inferred_template_result = null ): void { $intersection_types = []; @@ -206,7 +208,8 @@ public static function analyze( $intersection_types ?: [], $fq_class_name, $moved_call, - $has_existing_method + $has_existing_method, + $inferred_template_result ); } else { if ($stmt->name instanceof PhpParser\Node\Expr) { @@ -268,7 +271,8 @@ private static function handleNamedCall( array $intersection_types, string $fq_class_name, bool &$moved_call, - bool &$has_existing_method + bool &$has_existing_method, + ?TemplateResult $inferred_template_result = null ): bool { $codebase = $statements_analyzer->getCodebase(); @@ -827,7 +831,8 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { $method_id, $cased_method_id, $class_storage, - $moved_call + $moved_call, + $inferred_template_result ); return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 6951396f72a..013a44f61dd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -59,7 +59,8 @@ public static function analyze( MethodIdentifier $method_id, string $cased_method_id, ClassLikeStorage $class_storage, - bool &$moved_call + bool &$moved_call, + ?TemplateResult $inferred_template_result = null ): void { $fq_class_name = $method_id->fq_class_name; $method_name_lc = $method_id->method_name; @@ -182,6 +183,10 @@ public static function analyze( $template_result = new TemplateResult([], $found_generic_params ?: []); + if ($inferred_template_result) { + $template_result->lower_bounds += $inferred_template_result->lower_bounds; + } + if (CallAnalyzer::checkMethodArgs( $method_id, $args, diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 5ffa52a5aea..f5a8ba2649e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -186,11 +186,11 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\MethodCall) { - return MethodCallAnalyzer::analyze($statements_analyzer, $stmt, $context); + return MethodCallAnalyzer::analyze($statements_analyzer, $stmt, $context, true, $template_result); } if ($stmt instanceof PhpParser\Node\Expr\StaticCall) { - return StaticCallAnalyzer::analyze($statements_analyzer, $stmt, $context); + return StaticCallAnalyzer::analyze($statements_analyzer, $stmt, $context, $template_result); } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { From 379d0d6c494e0e18f9e812c718ae692c2465a8a2 Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 18 Jan 2022 18:54:47 +0300 Subject: [PATCH 10/13] Add tests --- tests/CallableTest.php | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/CallableTest.php b/tests/CallableTest.php index e23e207f32b..227cc62edc8 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -295,6 +295,77 @@ function id() { throw new RuntimeException("???"); } '$result' => 'ArrayList', ], ], + 'inferTemplateOfHighOrderFunctionFromMethodArgByPreviousArg' => [ + '): T + */ + public function flatten() { throw new RuntimeException("???"); } + } + /** + * @return list> + */ + function getList() { throw new RuntimeException("???"); } + /** + * @template T + * @return Closure(list): T + */ + function flatten() { throw new RuntimeException("???"); } + /** + * @template A + * @template B + * + * @param list $_a + * @param callable(A): B $_ab + * @return list + */ + function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } + + $ops = new Ops; + $result = map(getList(), $ops->flatten()); + ', + 'assertions' => [ + '$result' => 'list', + ], + ], + 'inferTemplateOfHighOrderFunctionFromStaticMethodArgByPreviousArg' => [ + '): T + */ + public static function flatten() { throw new RuntimeException("???"); } + } + /** + * @return list> + */ + function getList() { throw new RuntimeException("???"); } + /** + * @template T + * @return Closure(list): T + */ + function flatten() { throw new RuntimeException("???"); } + /** + * @template A + * @template B + * + * @param list $_a + * @param callable(A): B $_ab + * @return list + */ + function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } + + $result = map(getList(), StaticOps::flatten()); + ', + 'assertions' => [ + '$result' => 'list', + ], + ], 'inferPipelineWithPartiallyAppliedFunctions' => [ ' Date: Tue, 18 Jan 2022 22:05:32 +0300 Subject: [PATCH 11/13] Fix psalm issues --- .../Statements/Expression/Call/ArgumentsAnalyzer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 40b0b0b5935..bcab7a0b4a9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -357,6 +357,8 @@ private static function getHighOrderFuncStorage( if ($function_like_call instanceof PhpParser\Node\Expr\MethodCall && $function_like_call->var instanceof PhpParser\Node\Expr\Variable && + $function_like_call->name instanceof PhpParser\Node\Identifier && + is_string($function_like_call->var->name) && isset($context->vars_in_scope['$' . $function_like_call->var->name]) ) { $lhs_type = $context->vars_in_scope['$' . $function_like_call->var->name]->getSingleAtomic(); @@ -367,7 +369,7 @@ private static function getHighOrderFuncStorage( $method_id = new MethodIdentifier( $lhs_type->value, - (string)$function_like_call->name + strtolower((string)$function_like_call->name) ); return $codebase->methods->getStorage($method_id); @@ -378,7 +380,7 @@ private static function getHighOrderFuncStorage( ) { $method_id = new MethodIdentifier( (string)$function_like_call->class->getAttribute('resolvedName'), - $function_like_call->name->name + strtolower($function_like_call->name->name) ); return $codebase->methods->getStorage($method_id); From 7e623ed48e63b86e07e0ffb9419f08742cf967df Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 19 Jan 2022 00:37:05 +0300 Subject: [PATCH 12/13] Upcast invokable to callable during hof arg analysis --- .../Expression/Call/ArgumentsAnalyzer.php | 23 ++++++- .../Comparator/CallableTypeComparator.php | 44 +++++++++++- tests/CallableTest.php | 67 +++++++++++++++++++ 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index bcab7a0b4a9..089b958144d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -19,6 +19,7 @@ use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Stubs\Generator\StubsGenerator; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; @@ -424,10 +425,31 @@ private static function handleHighOrderFuncCallArg( FunctionLikeStorage $storage, FunctionLikeParameter $actual_func_param ): ?TemplateResult { + $codebase = $statements_analyzer->getCodebase(); + $input_hof_atomic = $storage->return_type && $storage->return_type->isSingle() ? $storage->return_type->getSingleAtomic() : null; + // Try upcast invokable to callable type. + if ($input_hof_atomic instanceof Type\Atomic\TNamedObject && + $input_hof_atomic->value !== 'Closure' && + $codebase->classExists($input_hof_atomic->value) + ) { + $callable_from_invokable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $input_hof_atomic + ); + + if ($callable_from_invokable) { + $invoke_id = new MethodIdentifier($input_hof_atomic->value, '__invoke'); + $declaring_invoke_id = $codebase->methods->getDeclaringMethodId($invoke_id); + + $storage = $codebase->methods->getStorage($declaring_invoke_id ?? $invoke_id); + $input_hof_atomic = $callable_from_invokable; + } + } + if (!$input_hof_atomic instanceof TClosure && !$input_hof_atomic instanceof TCallable) { return null; } @@ -441,7 +463,6 @@ private static function handleHighOrderFuncCallArg( } $replaced_container_hof_atomic = new Union([clone $container_hof_atomic]); - $codebase = $statements_analyzer->getCodebase(); // Replaces all input args in container function. // diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 107e3dd5785..4dca3e0e45c 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -10,6 +10,9 @@ use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\NodeDataProvider; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Type; use Psalm\Type\Atomic; @@ -385,6 +388,31 @@ public static function getCallableFromAtomic( if ($codebase->methods->methodExists($invoke_id)) { $declaring_method_id = $codebase->methods->getDeclaringMethodId($invoke_id); + $template_result = null; + + if ($input_type_part instanceof Atomic\TGenericObject) { + $invokable_storage = $codebase->methods->getClassLikeStorageForMethod($declaring_method_id); + $type_params = []; + + foreach ($invokable_storage->template_types ?? [] as $template => $for_class) { + foreach ($for_class as $type) { + $type_params[] = new Type\Union([ + new TTemplateParam($template, $type, $input_type_part->value) + ]); + } + } + + $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); + $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); + + TemplateStandinTypeReplacer::replace( + new Type\Union([$input_with_templates]), + $template_result, + $codebase, + null, + new Type\Union([$input_type_part]) + ); + } if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); @@ -400,12 +428,26 @@ public static function getCallableFromAtomic( ); } - return new TCallable( + $callable = new TCallable( 'callable', $method_storage->params, $converted_return_type, $method_storage->pure ); + + if ($template_result) { + $replaced_callable = clone $callable; + + TemplateInferredTypeReplacer::replace( + new Type\Union([$replaced_callable]), + $template_result, + $codebase + ); + + $callable = $replaced_callable; + } + + return $callable; } } } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 227cc62edc8..a23f79bc7b8 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -366,6 +366,73 @@ function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } '$result' => 'list', ], ], + '' => [ + ' $a + * @return list + */ + public function __invoke($a): array + { + $b = []; + + foreach ($a as $item) { + $b[] = ($this->ab)($item); + } + + return $b; + } + } + /** + * @template A + * @template B + * + * @param Closure(A): B $ab + * @return MapOperator + */ + function map(Closure $ab): MapOperator + { + return new MapOperator($ab); + } + /** + * @template A + * @template B + * + * @param A $_a + * @param callable(A): B $_ab + * @return B + */ + function pipe(array $_a, callable $_ab): array + { + throw new RuntimeException("???"); + } + $result1 = pipe( + ["1", "2", "3"], + map(fn ($i) => (int) $i) + ); + $result2 = pipe( + ["1", "2", "3"], + new MapOperator(fn ($i) => (int) $i) + ); + ', + 'assertions' => [ + '$result1' => 'list', + '$result2' => 'list', + ], + 'error_levels' => [], + '8.0', + ], 'inferPipelineWithPartiallyAppliedFunctions' => [ ' Date: Wed, 19 Jan 2022 00:48:50 +0300 Subject: [PATCH 13/13] Fix psalm issues in CallableTypeComparator --- .../Comparator/CallableTypeComparator.php | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 4dca3e0e45c..e89269c3e0d 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -391,7 +391,9 @@ public static function getCallableFromAtomic( $template_result = null; if ($input_type_part instanceof Atomic\TGenericObject) { - $invokable_storage = $codebase->methods->getClassLikeStorageForMethod($declaring_method_id); + $invokable_storage = $codebase->methods->getClassLikeStorageForMethod( + $declaring_method_id ?? $invoke_id + ); $type_params = []; foreach ($invokable_storage->template_types ?? [] as $template => $for_class) { @@ -402,16 +404,18 @@ public static function getCallableFromAtomic( } } - $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); - $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); + if (!empty($type_params)) { + $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); + $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - TemplateStandinTypeReplacer::replace( - new Type\Union([$input_with_templates]), - $template_result, - $codebase, - null, - new Type\Union([$input_type_part]) - ); + TemplateStandinTypeReplacer::replace( + new Type\Union([$input_with_templates]), + $template_result, + $codebase, + null, + new Type\Union([$input_type_part]) + ); + } } if ($declaring_method_id) {