1
0
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:
orklah 2021-12-10 22:40:16 +01:00 committed by GitHub
commit 76bb8bc655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 299 additions and 84 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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) . '()';

View File

@ -69,6 +69,10 @@ class NamedFunctionCallHandler
return;
}
if ($stmt->isFirstClassCallable()) {
return;
}
$first_arg = $stmt->getArgs()[0] ?? null;
if ($function_id === 'method_exists') {

View File

@ -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(),

View File

@ -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,

View File

@ -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

View File

@ -38,7 +38,8 @@ class ClosureFromCallableReturnTypeProvider implements MethodReturnTypeProviderI
$codebase,
$atomic_type,
null,
$source
$source,
true
);
if ($candidate_callable) {

View File

@ -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'
],
];
}