Skip to content

Commit

Permalink
Merge pull request #7391 from b2pweb/docblock-method-inheritance
Browse files Browse the repository at this point in the history
Handle inherited docblock method
  • Loading branch information
orklah committed Jan 15, 2022
2 parents fb30d83 + a523624 commit 75947c9
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 18 deletions.
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;

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

0 comments on commit 75947c9

Please sign in to comment.