diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a05c8a18d57..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; @@ -196,7 +197,21 @@ 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 + || $arg->value instanceof PhpParser\Node\Expr\MethodCall + || $arg->value instanceof PhpParser\Node\Expr\StaticCall) + && $param + && $function_storage = self::getHighOrderFuncStorage($context, $statements_analyzer, $arg->value) + ) { + $high_order_template_result = self::handleHighOrderFuncCallArg( + $statements_analyzer, + $template_result ?? new TemplateResult([], []), + $function_storage, + $param + ); + } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction) && $param && !$arg->value->getDocComment() @@ -217,7 +232,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 +338,172 @@ 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 && + $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(); + + if (!$lhs_type instanceof Type\Atomic\TNamedObject) { + return null; + } + + $method_id = new MethodIdentifier( + $lhs_type->value, + strtolower((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'), + strtolower($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). + * + * 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 { ... } + * + * // 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( + StatementsAnalyzer $statements_analyzer, + TemplateResult $inferred_template_result, + 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; + } + + $container_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle() + ? $actual_func_param->type->getSingleAtomic() + : null; + + if (!$container_hof_atomic instanceof TClosure && !$container_hof_atomic instanceof TCallable) { + return null; + } + + $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_container_hof_atomic, + $inferred_template_result, + $codebase + ); + + /** @var TClosure|TCallable $container_hof_atomic */ + $container_hof_atomic = $replaced_container_hof_atomic->getSingleAtomic(); + $high_order_template_result = new TemplateResult($storage->template_types ?: [], []); + + // 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($container_hof_atomic->params[$offset]) + ) { + TemplateStandinTypeReplacer::replace( + clone $actual_func_param->type, + $high_order_template_result, + $codebase, + null, + $container_hof_atomic->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/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 0ce2af836e8..f5a8ba2649e 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( @@ -183,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) { @@ -295,7 +298,8 @@ private static function handleExpression( return FunctionCallAnalyzer::analyze( $statements_analyzer, $stmt, - $context + $context, + $template_result ); } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 107e3dd5785..e89269c3e0d 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,35 @@ 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 ?? $invoke_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) + ]); + } + } + + 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]) + ); + } + } if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); @@ -400,12 +432,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 65fa422056c..a23f79bc7b8 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -233,6 +233,290 @@ function ($i) { '$inferred' => 'list', ], ], + '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', + ], + ], + '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', + ], + ], + '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', + ], + ], + '' => [ + ' $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' => [ + '): 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' => [ '