Skip to content

Commit

Permalink
Fix first-class callables when used with magic method, built-in, or u…
Browse files Browse the repository at this point in the history
…ndefined methods

Closes #7196.
  • Loading branch information
trowski committed Dec 27, 2021
1 parent 33466d8 commit e23a915
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ public static function analyze(
$code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
$config = $codebase->config;

$is_first_class_callable = $stmt->isFirstClassCallable();

$real_stmt = $stmt;

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

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

if ($function_name instanceof PhpParser\Node\Name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ public static function fetch(
$fq_class_name = $method_id->fq_class_name;
$method_name = $method_id->method_name;

$class_storage = $codebase->methods->getClassLikeStorageForMethod($method_id);
$method_storage = ($class_storage->methods[$method_id->method_name] ?? null);

if ($stmt->isFirstClassCallable()) {
if ($method_storage) {
return new Union([new TClosure(
'Closure',
$method_storage->params,
$method_storage->return_type,
$method_storage->pure
)]);
}

return Type::getClosure();
}

if ($codebase->methods->return_type_provider->has($premixin_method_id->fq_class_name)) {
$return_type_candidate = $codebase->methods->return_type_provider->getReturnType(
$statements_analyzer,
Expand Down Expand Up @@ -99,8 +115,6 @@ public static function fetch(
}
}

$class_storage = $codebase->methods->getClassLikeStorageForMethod($method_id);

if (InternalCallMapHandler::inCallMap((string) $call_map_id)) {
if (($template_result->lower_bounds || $class_storage->stubbed)
&& ($method_storage = ($class_storage->methods[$method_id->method_name] ?? null))
Expand Down Expand Up @@ -143,95 +157,80 @@ public static function fetch(
} else {
$self_fq_class_name = $fq_class_name;

if ($stmt->isFirstClassCallable()) {
$method_storage = ($class_storage->methods[$method_id->method_name] ?? null);

if ($method_storage) {
$return_type_candidate = new Union([new 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 TNamedObject
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
true
);
}
$return_type_candidate = $codebase->methods->getMethodReturnType(
$method_id,
$self_fq_class_name,
$statements_analyzer,
$args
);

$return_type_candidate = self::replaceTemplateTypes(
$return_type_candidate,
$template_result,
$method_id,
count($stmt->getArgs()),
$codebase
);
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,
$self_fq_class_name,
$static_type,
$fq_class_name,
null,
$class_storage->parent_class,
true,
false,
$static_type instanceof TNamedObject
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
true
);
}

$return_type_location = $codebase->methods->getMethodReturnTypeLocation(
$method_id,
$secondary_return_type_location
);
$return_type_candidate = self::replaceTemplateTypes(
$return_type_candidate,
$template_result,
$method_id,
count($stmt->getArgs()),
$codebase
);

if ($secondary_return_type_location) {
$return_type_location = $secondary_return_type_location;
}
$return_type_candidate = TypeExpander::expandUnion(
$codebase,
$return_type_candidate,
$self_fq_class_name,
$static_type,
$class_storage->parent_class,
true,
false,
$static_type instanceof TNamedObject
&& $codebase->classlike_storage_provider->get($static_type->value)->final,
true
);

$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);
$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
);
}
} else {
$result->returns_by_ref =
$result->returns_by_ref
|| $codebase->methods->getMethodReturnsByRef($method_id);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
use Psalm\Node\Scalar\VirtualString;
use Psalm\Node\VirtualArg;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Union;

use function array_map;
Expand All @@ -39,6 +41,21 @@ public static function handleMagicMethod(
$fq_class_name = $method_id->fq_class_name;
$method_name_lc = $method_id->method_name;

if ($stmt->isFirstClassCallable()) {
if (isset($class_storage->pseudo_methods[$method_name_lc])) {
$result->has_valid_method_call_type = true;
$result->existent_method_ids[] = $method_id->__toString();
$result->return_type = self::createFirstClassCallableReturnType(
$class_storage->pseudo_methods[$method_name_lc]
);
} else {
$result->non_existent_magic_method_ids[] = $method_id->__toString();
$result->return_type = self::createFirstClassCallableReturnType();
}

return null;
}

if ($codebase->methods->return_type_provider->has($fq_class_name)) {
$return_type_candidate = $codebase->methods->return_type_provider->getReturnType(
$statements_analyzer,
Expand Down Expand Up @@ -220,6 +237,11 @@ public static function handleMissingOrMagicMethod(

$pseudo_method_storage = $class_storage->pseudo_methods[$method_name_lc];

if ($stmt->isFirstClassCallable()) {
$result->return_type = self::createFirstClassCallableReturnType($pseudo_method_storage);
return;
}

if (ArgumentsAnalyzer::analyze(
$statements_analyzer,
$stmt->getArgs(),
Expand Down Expand Up @@ -277,6 +299,12 @@ public static function handleMissingOrMagicMethod(
return;
}

if ($stmt->isFirstClassCallable()) {
$result->non_existent_class_method_ids[] = $method_id->__toString();
$result->return_type = self::createFirstClassCallableReturnType();
return;
}

if (ArgumentsAnalyzer::analyze(
$statements_analyzer,
$stmt->getArgs(),
Expand Down Expand Up @@ -309,4 +337,18 @@ public static function handleMissingOrMagicMethod(
}
}
}

private static function createFirstClassCallableReturnType(?MethodStorage $method_storage = null): Union
{
if ($method_storage) {
return new Union([new TClosure(
'Closure',
$method_storage->params,
$method_storage->return_type,
$method_storage->pure
)]);
}

return Type::getClosure();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ function (?Union $type_1, Union $type_2) use ($codebase): Union {
}

if (!$result->existent_method_ids) {
return self::checkMethodArgs(
return $stmt->isFirstClassCallable() || self::checkMethodArgs(
null,
$stmt->getArgs(),
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,6 @@ private static function handleNamedCall(
);
}

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

if ($intersection_types
&& !$codebase->methods->methodExists($method_id)
) {
Expand Down Expand Up @@ -346,6 +344,28 @@ private static function handleNamedCall(
}
}

if ($stmt->isFirstClassCallable()) {
$method_storage = ($class_storage->methods[$method_name_lc] ??
($class_storage->pseudo_static_methods[$method_name_lc] ?? null));

if ($method_storage) {
$return_type_candidate = new Union([new 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;
}

$args = $stmt->getArgs();

if (!$naive_method_exists
&& $class_storage->mixin_declaring_fqcln
&& $class_storage->namedMixins
Expand Down Expand Up @@ -790,25 +810,6 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem {

$has_existing_method = true;

if ($stmt->isFirstClassCallable()) {
$method_storage = ($class_storage->methods[$method_id->method_name] ?? null);

if ($method_storage) {
$return_type_candidate = new Union([new 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public static function getArrayVarId(

if ($stmt instanceof PhpParser\Node\Expr\MethodCall
&& $stmt->name instanceof PhpParser\Node\Identifier
&& !$stmt->isFirstClassCallable()
&& !$stmt->getArgs()
) {
$config = Config::getInstance();
Expand Down
Loading

0 comments on commit e23a915

Please sign in to comment.