mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Merge pull request #7113 from trowski/first-class-callables
Added support for first-class callables
This commit is contained in:
commit
76bb8bc655
@ -227,7 +227,7 @@ class AssertionFinder
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\FuncCall) {
|
||||
if ($conditional instanceof PhpParser\Node\Expr\FuncCall && !$conditional->isFirstClassCallable()) {
|
||||
return self::processFunctionCall(
|
||||
$conditional,
|
||||
$this_class_name,
|
||||
@ -237,8 +237,9 @@ class AssertionFinder
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $conditional instanceof PhpParser\Node\Expr\StaticCall
|
||||
if (($conditional instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $conditional instanceof PhpParser\Node\Expr\StaticCall)
|
||||
&& !$conditional->isFirstClassCallable()
|
||||
) {
|
||||
$custom_assertions = self::processCustomAssertion($conditional, $this_class_name, $source);
|
||||
|
||||
|
@ -20,6 +20,7 @@ use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TypeCombiner;
|
||||
use Psalm\Issue\DeprecatedFunction;
|
||||
use Psalm\Issue\ImpureFunctionCall;
|
||||
use Psalm\Issue\InvalidFunctionCall;
|
||||
@ -85,6 +86,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$real_stmt = $stmt;
|
||||
|
||||
if ($function_name instanceof PhpParser\Node\Name
|
||||
&& !$stmt->isFirstClassCallable()
|
||||
&& isset($stmt->getArgs()[0])
|
||||
&& !$stmt->getArgs()[0]->unpack
|
||||
) {
|
||||
@ -143,6 +145,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$is_first_class_callable = $stmt->isFirstClassCallable();
|
||||
$set_inside_conditional = false;
|
||||
|
||||
if ($function_name instanceof PhpParser\Node\Name
|
||||
@ -153,14 +156,16 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$set_inside_conditional = true;
|
||||
}
|
||||
|
||||
ArgumentsAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
$function_call_info->function_params,
|
||||
$function_call_info->function_id,
|
||||
$function_call_info->allow_named_args,
|
||||
$context
|
||||
);
|
||||
if (!$is_first_class_callable) {
|
||||
ArgumentsAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
$function_call_info->function_params,
|
||||
$function_call_info->function_id,
|
||||
$function_call_info->allow_named_args,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
if ($set_inside_conditional) {
|
||||
$context->inside_conditional = false;
|
||||
@ -168,7 +173,10 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$function_callable = null;
|
||||
|
||||
if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) {
|
||||
if (!$is_first_class_callable
|
||||
&& $function_name instanceof PhpParser\Node\Name
|
||||
&& $function_call_info->function_id
|
||||
) {
|
||||
if (!$function_call_info->is_stubbed && $function_call_info->in_call_map) {
|
||||
$function_callable = InternalCallMapHandler::getCallableFromCallMapById(
|
||||
$codebase,
|
||||
@ -184,7 +192,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$template_result = new TemplateResult([], []);
|
||||
|
||||
// do this here to allow closure param checks
|
||||
if ($function_call_info->function_params !== null) {
|
||||
if (!$is_first_class_callable && $function_call_info->function_params !== null) {
|
||||
ArgumentsAnalyzer::checkArgumentsMatch(
|
||||
$statements_analyzer,
|
||||
$stmt->getArgs(),
|
||||
@ -235,6 +243,45 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
|
||||
$config->eventDispatcher->dispatchAfterEveryFunctionCallAnalysis($event);
|
||||
|
||||
if ($is_first_class_callable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_first_class_callable) {
|
||||
$type_provider = $statements_analyzer->getNodeTypeProvider();
|
||||
$closure_types = [];
|
||||
|
||||
if ($input_type = $type_provider->getType($function_name)) {
|
||||
foreach ($input_type->getAtomicTypes() as $atomic_type) {
|
||||
$candidate_callable = CallableTypeComparator::getCallableFromAtomic(
|
||||
$codebase,
|
||||
$atomic_type,
|
||||
null,
|
||||
$statements_analyzer
|
||||
);
|
||||
|
||||
if ($candidate_callable) {
|
||||
$closure_types[] = new Type\Atomic\TClosure(
|
||||
'Closure',
|
||||
$candidate_callable->params,
|
||||
$candidate_callable->return_type,
|
||||
$candidate_callable->is_pure
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($closure_types) {
|
||||
$stmt_type = TypeCombiner::combine($closure_types, $codebase);
|
||||
} else {
|
||||
$stmt_type = Type::getClosure();
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($real_stmt, $stmt_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($function_call_info->defined_constants as $const_name => $const_type) {
|
||||
@ -457,13 +504,14 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$function_call_info->function_params = null;
|
||||
$function_call_info->defined_constants = [];
|
||||
$function_call_info->global_variables = [];
|
||||
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
|
||||
|
||||
if ($function_call_info->function_exists) {
|
||||
if ($codebase->functions->params_provider->has($function_call_info->function_id)) {
|
||||
$function_call_info->function_params = $codebase->functions->params_provider->getFunctionParams(
|
||||
$statements_analyzer,
|
||||
$function_call_info->function_id,
|
||||
$stmt->getArgs(),
|
||||
$args,
|
||||
null,
|
||||
$code_location
|
||||
);
|
||||
@ -496,7 +544,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$function_callable = InternalCallMapHandler::getCallableFromCallMapById(
|
||||
$codebase,
|
||||
$function_call_info->function_id,
|
||||
$stmt->getArgs(),
|
||||
$args,
|
||||
$statements_analyzer->node_data
|
||||
);
|
||||
|
||||
@ -789,7 +837,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$fake_method_call = new VirtualMethodCall(
|
||||
$function_name,
|
||||
new VirtualIdentifier('__invoke', $function_name->getAttributes()),
|
||||
$stmt->getArgs()
|
||||
$stmt->args
|
||||
);
|
||||
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
@ -948,7 +996,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
$codebase,
|
||||
$statements_analyzer->node_data,
|
||||
$function_call_info->function_id,
|
||||
$stmt->getArgs(),
|
||||
$stmt->isFirstClassCallable() ? [] : $stmt->getArgs(),
|
||||
$must_use
|
||||
)
|
||||
: null;
|
||||
|
@ -13,6 +13,7 @@ use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\DataFlow\TaintSource;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateBound;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
@ -58,7 +59,26 @@ class FunctionCallReturnTypeFetcher
|
||||
$stmt_type = null;
|
||||
$config = $codebase->config;
|
||||
|
||||
if ($codebase->functions->return_type_provider->has($function_id)) {
|
||||
if ($stmt->isFirstClassCallable()) {
|
||||
$candidate_callable = CallableTypeComparator::getCallableFromAtomic(
|
||||
$codebase,
|
||||
new Type\Atomic\TLiteralString($function_id),
|
||||
null,
|
||||
$statements_analyzer,
|
||||
true
|
||||
);
|
||||
|
||||
if ($candidate_callable) {
|
||||
$stmt_type = new Type\Union([new Type\Atomic\TClosure(
|
||||
'Closure',
|
||||
$candidate_callable->params,
|
||||
$candidate_callable->return_type,
|
||||
$candidate_callable->is_pure
|
||||
)]);
|
||||
} else {
|
||||
$stmt_type = Type::getClosure();
|
||||
}
|
||||
} elseif ($codebase->functions->return_type_provider->has($function_id)) {
|
||||
$stmt_type = $codebase->functions->return_type_provider->getReturnType(
|
||||
$statements_analyzer,
|
||||
$function_id,
|
||||
|
@ -193,7 +193,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$method_id = new MethodIdentifier($fq_class_name, $method_name_lc);
|
||||
|
||||
$args = $stmt->getArgs();
|
||||
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
|
||||
|
||||
$naive_method_id = $method_id;
|
||||
|
||||
|
@ -196,7 +196,9 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
if (self::checkMethodArgs(
|
||||
$is_first_class_callable = $stmt->isFirstClassCallable();
|
||||
|
||||
if (!$is_first_class_callable && self::checkMethodArgs(
|
||||
$method_id,
|
||||
$args,
|
||||
$template_result,
|
||||
@ -225,6 +227,10 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$template_result
|
||||
);
|
||||
|
||||
if ($is_first_class_callable) {
|
||||
return $return_type_candidate;
|
||||
}
|
||||
|
||||
$in_call_map = InternalCallMapHandler::inCallMap((string) ($declaring_method_id ?? $method_id));
|
||||
|
||||
if (!$in_call_map) {
|
||||
|
@ -137,80 +137,95 @@ class MethodCallReturnTypeFetcher
|
||||
} else {
|
||||
$self_fq_class_name = $fq_class_name;
|
||||
|
||||
$return_type_candidate = $codebase->methods->getMethodReturnType(
|
||||
$method_id,
|
||||
$self_fq_class_name,
|
||||
$statements_analyzer,
|
||||
$args
|
||||
);
|
||||
if ($stmt->isFirstClassCallable()) {
|
||||
$method_storage = ($class_storage->methods[$method_id->method_name] ?? null);
|
||||
|
||||
if ($return_type_candidate) {
|
||||
$return_type_candidate = clone $return_type_candidate;
|
||||
if ($method_storage) {
|
||||
$return_type_candidate = new Type\Union([new Type\Atomic\TClosure(
|
||||
'Closure',
|
||||
$method_storage->params,
|
||||
$method_storage->return_type,
|
||||
$method_storage->pure
|
||||
)]);
|
||||
} else {
|
||||
$return_type_candidate = Type::getClosure();
|
||||
}
|
||||
} else {
|
||||
$return_type_candidate = $codebase->methods->getMethodReturnType(
|
||||
$method_id,
|
||||
$self_fq_class_name,
|
||||
$statements_analyzer,
|
||||
$args
|
||||
);
|
||||
|
||||
if ($return_type_candidate) {
|
||||
$return_type_candidate = clone $return_type_candidate;
|
||||
|
||||
if ($template_result->lower_bounds) {
|
||||
$return_type_candidate = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$return_type_candidate,
|
||||
$fq_class_name,
|
||||
null,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$static_type instanceof Type\Atomic\TNamedObject
|
||||
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$return_type_candidate = self::replaceTemplateTypes(
|
||||
$return_type_candidate,
|
||||
$template_result,
|
||||
$method_id,
|
||||
count($stmt->getArgs()),
|
||||
$codebase
|
||||
);
|
||||
|
||||
if ($template_result->lower_bounds) {
|
||||
$return_type_candidate = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$return_type_candidate,
|
||||
$fq_class_name,
|
||||
null,
|
||||
$self_fq_class_name,
|
||||
$static_type,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$static_type instanceof Type\Atomic\TNamedObject
|
||||
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
|
||||
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$return_type_candidate = self::replaceTemplateTypes(
|
||||
$return_type_candidate,
|
||||
$template_result,
|
||||
$method_id,
|
||||
count($stmt->getArgs()),
|
||||
$codebase
|
||||
);
|
||||
|
||||
$return_type_candidate = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$return_type_candidate,
|
||||
$self_fq_class_name,
|
||||
$static_type,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$static_type instanceof Type\Atomic\TNamedObject
|
||||
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
|
||||
true
|
||||
);
|
||||
|
||||
$return_type_location = $codebase->methods->getMethodReturnTypeLocation(
|
||||
$method_id,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
|
||||
if ($secondary_return_type_location) {
|
||||
$return_type_location = $secondary_return_type_location;
|
||||
}
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
// only check the type locally if it's defined externally
|
||||
if ($return_type_location && !$config->isInProjectDirs($return_type_location->file_path)) {
|
||||
$return_type_candidate->check(
|
||||
$statements_analyzer,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
$context->phantom_classes,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
$context->calling_method_id
|
||||
$return_type_location = $codebase->methods->getMethodReturnTypeLocation(
|
||||
$method_id,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$result->returns_by_ref =
|
||||
$result->returns_by_ref
|
||||
|
||||
if ($secondary_return_type_location) {
|
||||
$return_type_location = $secondary_return_type_location;
|
||||
}
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
// only check the type locally if it's defined externally
|
||||
if ($return_type_location && !$config->isInProjectDirs($return_type_location->file_path)) {
|
||||
$return_type_candidate->check(
|
||||
$statements_analyzer,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
$context->phantom_classes,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
$context->calling_method_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$result->returns_by_ref =
|
||||
$result->returns_by_ref
|
||||
|| $codebase->methods->getMethodReturnsByRef($method_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +198,10 @@ class MethodCallAnalyzer extends CallAnalyzer
|
||||
$possible_new_class_types[] = $context->vars_in_scope[$lhs_var_id];
|
||||
}
|
||||
}
|
||||
if (!$stmt->getArgs() && $lhs_var_id && $stmt->name instanceof PhpParser\Node\Identifier) {
|
||||
if (!$stmt->isFirstClassCallable()
|
||||
&& !$stmt->getArgs()
|
||||
&& $lhs_var_id && $stmt->name instanceof PhpParser\Node\Identifier
|
||||
) {
|
||||
if ($codebase->config->memoize_method_calls || $result->can_memoize) {
|
||||
$method_var_id = $lhs_var_id . '->' . strtolower($stmt->name->name) . '()';
|
||||
|
||||
|
@ -69,6 +69,10 @@ class NamedFunctionCallHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if ($stmt->isFirstClassCallable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$first_arg = $stmt->getArgs()[0] ?? null;
|
||||
|
||||
if ($function_id === 'method_exists') {
|
||||
|
@ -220,7 +220,7 @@ class StaticCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
if (!$has_existing_method) {
|
||||
if (!$stmt->isFirstClassCallable() && !$has_existing_method) {
|
||||
return self::checkMethodArgs(
|
||||
$method_id,
|
||||
$stmt->getArgs(),
|
||||
|
@ -274,7 +274,7 @@ class AtomicStaticCallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$args = $stmt->getArgs();
|
||||
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
|
||||
|
||||
if ($intersection_types
|
||||
&& !$codebase->methods->methodExists($method_id)
|
||||
@ -776,6 +776,25 @@ class AtomicStaticCallAnalyzer
|
||||
|
||||
$has_existing_method = true;
|
||||
|
||||
if ($stmt->isFirstClassCallable()) {
|
||||
$method_storage = ($class_storage->methods[$method_id->method_name] ?? null);
|
||||
|
||||
if ($method_storage) {
|
||||
$return_type_candidate = new Type\Union([new Type\Atomic\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;
|
||||
}
|
||||
|
||||
ExistingAtomicStaticCallAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
|
@ -26,6 +26,7 @@ class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface
|
||||
|
||||
if (!$statements_analyzer instanceof StatementsAnalyzer
|
||||
|| !$item instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $item->isFirstClassCallable()
|
||||
|| !$item->name instanceof PhpParser\Node\Name
|
||||
|| count($item->name->parts) !== 1
|
||||
|| count($item->getArgs()) === 0
|
||||
@ -74,6 +75,7 @@ class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface
|
||||
|
||||
if (!$statements_analyzer instanceof StatementsAnalyzer
|
||||
|| !$item instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $item->isFirstClassCallable()
|
||||
|| !$item->name instanceof PhpParser\Node\Name
|
||||
|| count($item->name->parts) !== 1
|
||||
|| count($item->getArgs()) === 0
|
||||
|
@ -38,7 +38,8 @@ class ClosureFromCallableReturnTypeProvider implements MethodReturnTypeProviderI
|
||||
$codebase,
|
||||
$atomic_type,
|
||||
null,
|
||||
$source
|
||||
$source,
|
||||
true
|
||||
);
|
||||
|
||||
if ($candidate_callable) {
|
||||
|
@ -412,6 +412,14 @@ class ClosureTest extends TestCase
|
||||
}
|
||||
}',
|
||||
],
|
||||
'PHP71-closureFromCallableNamedFunction' => [
|
||||
'<?php
|
||||
$closure = Closure::fromCallable("strlen");
|
||||
',
|
||||
'assertions' => [
|
||||
'$closure' => 'pure-Closure(string):(0|positive-int)',
|
||||
]
|
||||
],
|
||||
'allowClosureWithNarrowerReturn' => [
|
||||
'<?php
|
||||
class A {}
|
||||
@ -556,6 +564,94 @@ class ClosureTest extends TestCase
|
||||
'$result' => 'array{stdClass}'
|
||||
],
|
||||
],
|
||||
'FirstClassCallable:NamedFunction:is_int' => [
|
||||
'<?php
|
||||
$closure = is_int(...);
|
||||
$result = $closure(1);
|
||||
',
|
||||
'assertions' => [
|
||||
'$closure' => 'pure-Closure(mixed):bool',
|
||||
'$result' => 'bool',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
'FirstClassCallable:NamedFunction:strlen' => [
|
||||
'<?php
|
||||
$closure = strlen(...);
|
||||
$result = $closure("test");
|
||||
',
|
||||
'assertions' => [
|
||||
'$closure' => 'pure-Closure(string):(0|positive-int)',
|
||||
'$result' => 'int|positive-int',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
'FirstClassCallable:InstanceMethod' => [
|
||||
'<?php
|
||||
class Test {
|
||||
public function __construct(private readonly string $string) {
|
||||
}
|
||||
|
||||
public function length(): int {
|
||||
return strlen($this->string);
|
||||
}
|
||||
}
|
||||
$test = new Test("test");
|
||||
$closure = $test->length(...);
|
||||
$length = $closure();
|
||||
',
|
||||
'assertions' => [
|
||||
'$length' => 'int',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
'FirstClassCallable:StaticMethod' => [
|
||||
'<?php
|
||||
class Test {
|
||||
public static function length(string $param): int {
|
||||
return strlen($param);
|
||||
}
|
||||
}
|
||||
$closure = Test::length(...);
|
||||
$length = $closure("test");
|
||||
',
|
||||
'assertions' => [
|
||||
'$length' => 'int',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
'FirstClassCallable:InvokableObject' => [
|
||||
'<?php
|
||||
class Test {
|
||||
public function __invoke(string $param): int {
|
||||
return strlen($param);
|
||||
}
|
||||
}
|
||||
$test = new Test();
|
||||
$closure = $test(...);
|
||||
$length = $closure("test");
|
||||
',
|
||||
'assertions' => [
|
||||
'$length' => 'int',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
'FirstClassCallable:FromClosure' => [
|
||||
'<?php
|
||||
$closure = fn (string $string): int => strlen($string);
|
||||
$closure = $closure(...);
|
||||
',
|
||||
'assertions' => [
|
||||
'$closure' => 'pure-Closure(string):(0|positive-int)',
|
||||
],
|
||||
[],
|
||||
'8.1'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user