diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 3b050ce763f..df41eb020fe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -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, @@ -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) { @@ -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); @@ -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 @@ -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; + } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 04b86338104..0130f030696 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -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; @@ -481,6 +482,12 @@ 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, @@ -488,7 +495,7 @@ private static function handleNamedCall( $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 @@ 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, @@ -557,7 +564,7 @@ private static function handleNamedCall( $method_id, $fq_class_name, $args, - $class_storage, + $defining_class_storage, $pseudo_method_storage, $context ) === false @@ -642,10 +649,10 @@ 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, @@ -653,7 +660,7 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { $method_id, $fq_class_name, $args, - $class_storage, + $defining_class_storage, $pseudo_method_storage, $context ) === false @@ -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, @@ -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 ); @@ -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; + } } diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index f961664c170..e2c4afdfedd 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -750,6 +750,75 @@ public function __call(string $method, array $args) { (new Cache)->bar(new \DateTime(), new Cache());' ], + 'magicMethodInheritance' => [ + 'foo()); + consumeInt($b->bar());' + ], + 'magicMethodInheritanceOnInterface' => [ + 'foo());' + ], + 'magicStaticMethodInheritance' => [ + ' [ + '