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

Handle inherited docblock method #7391

Merged
merged 1 commit into from Jan 15, 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 @@ -94,11 +94,17 @@ public static function handleMagicMethod(
}
}

if (isset($class_storage->pseudo_methods[$method_name_lc])) {
$found_method_and_class_storage = self::findPseudoMethodAndClassStorages(
$codebase,
$class_storage,
$method_name_lc
);

if ($found_method_and_class_storage) {
$result->has_valid_method_call_type = true;
$result->existent_method_ids[] = $method_id->__toString();

$pseudo_method_storage = $class_storage->pseudo_methods[$method_name_lc];
[$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage;

ArgumentsAnalyzer::analyze(
$statements_analyzer,
Expand Down Expand Up @@ -127,9 +133,9 @@ public static function handleMagicMethod(
$return_type_candidate = TypeExpander::expandUnion(
$codebase,
$return_type_candidate,
$defining_class_storage->name,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class
$defining_class_storage->parent_class
);

if ($all_intersection_return_type) {
Expand Down Expand Up @@ -229,13 +235,19 @@ public static function handleMissingOrMagicMethod(

$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);

$found_method_and_class_storage = self::findPseudoMethodAndClassStorages(
$codebase,
$class_storage,
$method_name_lc
);

if (($is_interface || $config->use_phpdoc_method_without_magic_or_parent)
&& isset($class_storage->pseudo_methods[$method_name_lc])
&& $found_method_and_class_storage
) {
$result->has_valid_method_call_type = true;
$result->existent_method_ids[] = $method_id->__toString();

$pseudo_method_storage = $class_storage->pseudo_methods[$method_name_lc];
[$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage;

if ($stmt->isFirstClassCallable()) {
$result->return_type = self::createFirstClassCallableReturnType($pseudo_method_storage);
Expand Down Expand Up @@ -281,9 +293,9 @@ public static function handleMissingOrMagicMethod(
$return_type_candidate = TypeExpander::expandUnion(
$codebase,
$return_type_candidate,
$defining_class_storage->name,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class,
$defining_class_storage->parent_class,
true,
false,
$class_storage->final
Expand Down Expand Up @@ -351,4 +363,41 @@ private static function createFirstClassCallableReturnType(?MethodStorage $metho

return Type::getClosure();
}

/**
* Try to find matching pseudo method over ancestors (including interfaces).
*
* Returns the pseudo method if exists, with its defining class storage.
* If the method is not declared, null is returned.
*
* @param Codebase $codebase
* @param ClassLikeStorage $static_class_storage The called class
* @param lowercase-string $method_name_lc
*
* @return array{MethodStorage, ClassLikeStorage}
*/
private static function findPseudoMethodAndClassStorages(
Codebase $codebase,
ClassLikeStorage $static_class_storage,
string $method_name_lc
): ?array {
if ($pseudo_method_storage = $static_class_storage->pseudo_methods[$method_name_lc] ?? null) {
return [$pseudo_method_storage, $static_class_storage];
}

$ancestors = $static_class_storage->class_implements + $static_class_storage->parent_classes;

foreach ($ancestors as $fq_class_name => $_) {
$class_storage = $codebase->classlikes->getStorageFor($fq_class_name);

if ($class_storage && isset($class_storage->pseudo_methods[$method_name_lc])) {
return [
$class_storage->pseudo_methods[$method_name_lc],
$class_storage
];
}
}

return null;
}
}
Expand Up @@ -5,6 +5,7 @@
use Exception;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
Expand Down Expand Up @@ -481,14 +482,20 @@ private static function handleNamedCall(

$config = $codebase->config;

$found_method_and_class_storage = self::findPseudoMethodAndClassStorages(
$codebase,
$class_storage,
$method_name_lc
);

if (!$naive_method_exists
|| !MethodAnalyzer::isMethodVisible(
$method_id,
$context,
$statements_analyzer->getSource()
)
|| $fake_method_exists
|| (isset($class_storage->pseudo_static_methods[$method_name_lc])
|| ($found_method_and_class_storage
&& ($config->use_phpdoc_method_without_magic_or_parent || $class_storage->parent_class))
) {
$callstatic_id = new MethodIdentifier(
Expand Down Expand Up @@ -548,16 +555,16 @@ private static function handleNamedCall(
}
}

if (isset($class_storage->pseudo_static_methods[$method_name_lc])) {
$pseudo_method_storage = $class_storage->pseudo_static_methods[$method_name_lc];
if ($found_method_and_class_storage) {
[$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage;

if (self::checkPseudoMethod(
$statements_analyzer,
$stmt,
$method_id,
$fq_class_name,
$args,
$class_storage,
$defining_class_storage,
$pseudo_method_storage,
$context
) === false
Expand Down Expand Up @@ -642,18 +649,18 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem {
$fq_class_name,
'__callstatic'
);
} elseif (isset($class_storage->pseudo_static_methods[$method_name_lc])
} elseif ($found_method_and_class_storage
&& ($config->use_phpdoc_method_without_magic_or_parent || $class_storage->parent_class)
) {
$pseudo_method_storage = $class_storage->pseudo_static_methods[$method_name_lc];
[$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage;

if (self::checkPseudoMethod(
$statements_analyzer,
$stmt,
$method_id,
$fq_class_name,
$args,
$class_storage,
$defining_class_storage,
$pseudo_method_storage,
$context
) === false
Expand Down Expand Up @@ -834,7 +841,7 @@ private static function checkPseudoMethod(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\StaticCall $stmt,
MethodIdentifier $method_id,
string $fq_class_name,
string $static_fq_class_name,
array $args,
ClassLikeStorage $class_storage,
MethodStorage $pseudo_method_storage,
Expand Down Expand Up @@ -904,8 +911,8 @@ private static function checkPseudoMethod(
$return_type_candidate = TypeExpander::expandUnion(
$statements_analyzer->getCodebase(),
$return_type_candidate,
$fq_class_name,
$fq_class_name,
$class_storage->name,
$static_fq_class_name,
$class_storage->parent_class
);

Expand Down Expand Up @@ -1002,4 +1009,41 @@ public static function handleNonObjectCall(
$statements_analyzer->getSuppressedIssues()
);
}

/**
* Try to find matching pseudo method over ancestors (including interfaces).
*
* Returns the pseudo method if exists, with its defining class storage.
* If the method is not declared, null is returned.
*
* @param Codebase $codebase
* @param ClassLikeStorage $static_class_storage The called class
* @param lowercase-string $method_name_lc
*
* @return array{MethodStorage, ClassLikeStorage}|null
*/
private static function findPseudoMethodAndClassStorages(
Codebase $codebase,
ClassLikeStorage $static_class_storage,
string $method_name_lc
): ?array {
if ($pseudo_method_storage = $static_class_storage->pseudo_static_methods[$method_name_lc] ?? null) {
return [$pseudo_method_storage, $static_class_storage];
}

$ancestors = $static_class_storage->class_implements + $static_class_storage->parent_classes;
orklah marked this conversation as resolved.
Show resolved Hide resolved

foreach ($ancestors as $fq_class_name => $_) {
$class_storage = $codebase->classlikes->getStorageFor($fq_class_name);

if ($class_storage && isset($class_storage->pseudo_static_methods[$method_name_lc])) {
return [
$class_storage->pseudo_static_methods[$method_name_lc],
$class_storage
];
}
}

return null;
}
}
69 changes: 69 additions & 0 deletions tests/MagicMethodAnnotationTest.php
Expand Up @@ -750,6 +750,75 @@ public function __call(string $method, array $args) {

(new Cache)->bar(new \DateTime(), new Cache());'
],
'magicMethodInheritance' => [
'<?php
/**
* @method string foo()
*/
interface I {}

/**
* @method int bar()
*/
class A implements I {}

class B extends A {
public function __call(string $method, array $args) {}
}

$b = new B();

function consumeString(string $s): void {}
function consumeInt(int $i): void {}

consumeString($b->foo());
consumeInt($b->bar());'
],
'magicMethodInheritanceOnInterface' => [
'<?php
/**
* @method string foo()
*/
interface I {}
interface I2 extends I {}
function consumeString(string $s): void {}

/** @var I2 $i */
consumeString($i->foo());'
],
'magicStaticMethodInheritance' => [
'<?php
/**
* @method static string foo()
*/
interface I {}

/**
* @method static int bar()
*/
class A implements I {}

class B extends A {
public static function __callStatic(string $name, array $arguments) {}
}

function consumeString(string $s): void {}
function consumeInt(int $i): void {}

consumeString(B::foo());
consumeInt(B::bar());'
],
'magicStaticMethodInheritanceWithoutCallStatic' => [
'<?php
/**
* @method static int bar()
*/
class A {}
class B extends A {}
function consumeInt(int $i): void {}

consumeInt(B::bar());'
],
];
}

Expand Down