Skip to content

Commit

Permalink
Merge pull request #7113 from trowski/first-class-callables
Browse files Browse the repository at this point in the history
Added support for first-class callables
  • Loading branch information
orklah committed Dec 10, 2021
2 parents 4ac0b64 + 3c5e99e commit 76bb8bc
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 83 deletions.
Expand Up @@ -227,7 +227,7 @@ public static function scrapeAssertions(
);
}

if ($conditional instanceof PhpParser\Node\Expr\FuncCall) {
if ($conditional instanceof PhpParser\Node\Expr\FuncCall && !$conditional->isFirstClassCallable()) {
return self::processFunctionCall(
$conditional,
$this_class_name,
Expand All @@ -237,8 +237,9 @@ public static function scrapeAssertions(
);
}

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

Expand Down
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -85,6 +86,7 @@ public static function analyze(
$real_stmt = $stmt;

if ($function_name instanceof PhpParser\Node\Name
&& !$stmt->isFirstClassCallable()
&& isset($stmt->getArgs()[0])
&& !$stmt->getArgs()[0]->unpack
) {
Expand Down Expand Up @@ -143,6 +145,7 @@ public static function analyze(
}
}

$is_first_class_callable = $stmt->isFirstClassCallable();
$set_inside_conditional = false;

if ($function_name instanceof PhpParser\Node\Name
Expand All @@ -153,22 +156,27 @@ public static function analyze(
$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;
}

$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,
Expand All @@ -184,7 +192,7 @@ public static function analyze(
$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(),
Expand Down Expand Up @@ -235,6 +243,45 @@ public static function analyze(
);

$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) {
Expand Down Expand Up @@ -457,13 +504,14 @@ private static function handleNamedFunction(
$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
);
Expand Down Expand Up @@ -496,7 +544,7 @@ private static function handleNamedFunction(
$function_callable = InternalCallMapHandler::getCallableFromCallMapById(
$codebase,
$function_call_info->function_id,
$stmt->getArgs(),
$args,
$statements_analyzer->node_data
);

Expand Down Expand Up @@ -789,7 +837,7 @@ private static function analyzeInvokeCall(
$fake_method_call = new VirtualMethodCall(
$function_name,
new VirtualIdentifier('__invoke', $function_name->getAttributes()),
$stmt->getArgs()
$stmt->args
);

$suppressed_issues = $statements_analyzer->getSuppressedIssues();
Expand Down Expand Up @@ -948,7 +996,7 @@ private static function checkFunctionCallPurity(
$codebase,
$statements_analyzer->node_data,
$function_call_info->function_id,
$stmt->getArgs(),
$stmt->isFirstClassCallable() ? [] : $stmt->getArgs(),
$must_use
)
: null;
Expand Down
Expand Up @@ -13,6 +13,7 @@
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;
Expand Down Expand Up @@ -58,7 +59,26 @@ public static function fetch(
$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,
Expand Down
Expand Up @@ -193,7 +193,7 @@ public static function analyze(

$method_id = new MethodIdentifier($fq_class_name, $method_name_lc);

$args = $stmt->getArgs();
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();

$naive_method_id = $method_id;

Expand Down
Expand Up @@ -196,7 +196,9 @@ public static function analyze(
);
}

if (self::checkMethodArgs(
$is_first_class_callable = $stmt->isFirstClassCallable();

if (!$is_first_class_callable && self::checkMethodArgs(
$method_id,
$args,
$template_result,
Expand Down Expand Up @@ -225,6 +227,10 @@ public static function analyze(
$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) {
Expand Down

0 comments on commit 76bb8bc

Please sign in to comment.