mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Merge pull request #7391 from b2pweb/docblock-method-inheritance
Handle inherited docblock method
This commit is contained in:
commit
75947c97c6
@ -94,11 +94,17 @@ class MissingMethodCallHandler
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -127,9 +133,9 @@ class MissingMethodCallHandler
|
||||
$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) {
|
||||
@ -229,13 +235,19 @@ class MissingMethodCallHandler
|
||||
|
||||
$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);
|
||||
@ -281,9 +293,9 @@ class MissingMethodCallHandler
|
||||
$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
|
||||
@ -351,4 +363,41 @@ class MissingMethodCallHandler
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call\StaticMethod;
|
||||
use Exception;
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
|
||||
@ -481,6 +482,12 @@ class AtomicStaticCallAnalyzer
|
||||
|
||||
$config = $codebase->config;
|
||||
|
||||
$found_method_and_class_storage = self::findPseudoMethodAndClassStorages(
|
||||
$codebase,
|
||||
$class_storage,
|
||||
$method_name_lc
|
||||
);
|
||||
|
||||
if (!$naive_method_exists
|
||||
|| !MethodAnalyzer::isMethodVisible(
|
||||
$method_id,
|
||||
@ -488,7 +495,7 @@ class AtomicStaticCallAnalyzer
|
||||
$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(
|
||||
@ -548,8 +555,8 @@ class AtomicStaticCallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -557,7 +564,7 @@ class AtomicStaticCallAnalyzer
|
||||
$method_id,
|
||||
$fq_class_name,
|
||||
$args,
|
||||
$class_storage,
|
||||
$defining_class_storage,
|
||||
$pseudo_method_storage,
|
||||
$context
|
||||
) === false
|
||||
@ -642,10 +649,10 @@ class AtomicStaticCallAnalyzer
|
||||
$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,
|
||||
@ -653,7 +660,7 @@ class AtomicStaticCallAnalyzer
|
||||
$method_id,
|
||||
$fq_class_name,
|
||||
$args,
|
||||
$class_storage,
|
||||
$defining_class_storage,
|
||||
$pseudo_method_storage,
|
||||
$context
|
||||
) === false
|
||||
@ -834,7 +841,7 @@ class AtomicStaticCallAnalyzer
|
||||
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,
|
||||
@ -904,8 +911,8 @@ class AtomicStaticCallAnalyzer
|
||||
$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
|
||||
);
|
||||
|
||||
@ -1002,4 +1009,41 @@ class AtomicStaticCallAnalyzer
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -750,6 +750,75 @@ class MagicMethodAnnotationTest extends TestCase
|
||||
|
||||
(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());'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user