Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure we recognize inherited static methods for the first-class callables #8370

Merged
merged 2 commits into from Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -56,6 +56,7 @@

use function array_filter;
use function array_map;
use function array_values;
use function assert;
use function count;
use function in_array;
Expand Down Expand Up @@ -369,39 +370,15 @@ private static function handleNamedCall(
if (!$naive_method_exists
&& $codebase->methods->existence_provider->has($fq_class_name)
) {
$method_exists = $codebase->methods->existence_provider->doesMethodExist(
$fake_method_exists = $codebase->methods->existence_provider->doesMethodExist(
$fq_class_name,
$method_id->method_name,
$statements_analyzer,
null
);

if ($method_exists) {
$fake_method_exists = true;
}
}

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;
) ?? false;
}

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

if (!$naive_method_exists
&& $class_storage->mixin_declaring_fqcln
Expand Down Expand Up @@ -504,6 +481,53 @@ private static function handleNamedCall(
$method_name_lc
);

if ($stmt->isFirstClassCallable()) {
if ($found_method_and_class_storage) {
[ $method_storage ] = $found_method_and_class_storage;

$return_type_candidate = new Union([new TClosure(
'Closure',
$method_storage->params,
$method_storage->return_type,
$method_storage->pure
)]);
} else {
$method_exists = $naive_method_exists
|| $fake_method_exists
|| isset($class_storage->methods[$method_name_lc])
|| isset($class_storage->pseudo_static_methods[$method_name_lc]);

if ($method_exists) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id) ?? $method_id;

$return_type_candidate = new Union([new TClosure(
'Closure',
array_values($codebase->getMethodParams($method_id)),
$codebase->getMethodReturnType($method_id, $fq_class_name),
$codebase->methods->getStorage($declaring_method_id)->pure
)]);
} else {
// FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception?
$return_type_candidate = Type::getClosure();
}
}

$expanded_return_type = TypeExpander::expandUnion(
$codebase,
$return_type_candidate,
$context->self,
$class_storage->name,
$context->parent,
true,
false,
true
);

$statements_analyzer->node_data->setType($stmt, $expanded_return_type);

return true;
}

if (!$naive_method_exists
|| !MethodAnalyzer::isMethodVisible(
$method_id,
Expand Down
49 changes: 49 additions & 0 deletions tests/ClosureTest.php
Expand Up @@ -751,6 +751,55 @@ public static function __callStatic(string $name, array $args): mixed {
[],
'8.1'
],
'FirstClassCallable:InheritedStaticMethod' => [
'<?php

abstract class A
{
public function foo(int $i): string
{
return (string) $i;
}
}

class C extends A {}

/** @param \Closure(int):string $_ */
function takesIntToString(\Closure $_): void {}

takesIntToString(C::foo(...));',
'assertions' => [],
[],
'8.1',
],
'FirstClassCallable:InheritedStaticMethodWithStaticTypeParameter' => [
'<?php

/** @template T */
class Holder
{
/** @param T $value */
public function __construct(public $value) {}
}

abstract class A
{
final public function __construct(public int $i) {}

/** @return Holder<static> */
public static function create(int $i): Holder
{
return new Holder(new static($i));
}
}

class C extends A {}

/** @param \Closure(int):Holder<C> $_ */
function takesIntToHolder(\Closure $_): void {}

takesIntToHolder(C::create(...));'
],
'FirstClassCallable:WithArrayMap' => [
'<?php
$array = [1, 2, 3];
Expand Down