diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 05a1ddaef..0d269bf8a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -36,6 +36,8 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer { /** * @param Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam $static_type + * + * @psalm-suppress ComplexMethod it's really complex, but unavoidably so */ public static function analyze( StatementsAnalyzer $statements_analyzer, @@ -183,6 +185,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $naive_method_id = $method_id; + // this tells us whether or not we can stay on the happy path $naive_method_exists = $codebase->methods->methodExists( $method_id, $context->calling_method_id, @@ -199,41 +202,66 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $fake_method_exists = false; - if (!$naive_method_exists - && $codebase->methods->existence_provider->has($fq_class_name) - ) { - $method_exists = $codebase->methods->existence_provider->doesMethodExist( - $fq_class_name, - $method_id->method_name, - $source, - null - ); - - if ($method_exists) { - $fake_method_exists = true; - } - } - if (!$naive_method_exists) { - [$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name] - = self::handleMixins( - $class_storage, - $lhs_type_part, - $method_name_lc, - $codebase, - $context, - $method_id, - $source, - $stmt, - $statements_analyzer, + // if the method doesn't exist we check for any method existence providers + if ($codebase->methods->existence_provider->has($fq_class_name)) { + $method_exists = $codebase->methods->existence_provider->doesMethodExist( $fq_class_name, - $lhs_var_id + $method_id->method_name, + $source, + null ); + + if ($method_exists) { + $fake_method_exists = true; + } + } + + $naive_method_exists = false; + + // @mixin attributes are an absolute pain! Lots of complexity here, + // as they can redefine the called class, method id etc. + if ($class_storage->templatedMixins + && $lhs_type_part instanceof Type\Atomic\TGenericObject + && $class_storage->template_types + ) { + [$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name] + = self::handleTemplatedMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $method_id, + $source, + $stmt, + $statements_analyzer, + $fq_class_name + ); + } elseif ($class_storage->mixin_declaring_fqcln + && $class_storage->namedMixins + ) { + [$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name] + = self::handleRegularMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $method_id, + $source, + $stmt, + $statements_analyzer, + $fq_class_name, + $lhs_var_id + ); + } } $all_intersection_return_type = null; $all_intersection_existent_method_ids = []; + // insersection types are also fun, they also complicate matters if ($intersection_types) { [$all_intersection_return_type, $all_intersection_existent_method_ids] = self::getIntersectionReturnType( @@ -314,14 +342,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer } } - $source_source = $statements_analyzer->getSource(); - - /** - * @var \Psalm\Internal\Analyzer\ClassLikeAnalyzer|null - */ - $classlike_source = $source_source->getSource(); - $classlike_source_fqcln = $classlike_source ? $classlike_source->getFQCLN() : null; - $intersection_method_id = $intersection_types ? '(' . $lhs_type_part . ')' . '::' . $stmt->name->name : null; @@ -329,7 +349,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer if ($lhs_var_id === '$this' && $context->self - && $classlike_source_fqcln && $fq_class_name !== $context->self && $codebase->methods->methodExists( new MethodIdentifier($context->self, $method_name_lc) @@ -340,12 +359,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $fq_class_name = $context->self; } - $is_interface = false; - - if ($codebase->interfaceExists($fq_class_name)) { - $is_interface = true; - } - $source_method_id = $source instanceof FunctionLikeAnalyzer ? $source->getId() : null; @@ -369,7 +382,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer $codebase, $stmt, $method_id, - $is_interface, + $codebase->interfaceExists($fq_class_name), $context, $codebase->config, $all_intersection_return_type, @@ -623,7 +636,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer * @param lowercase-string $method_name_lc * @return array{Type\Atomic\TNamedObject, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string} */ - private static function handleMixins( + private static function handleTemplatedMixins( \Psalm\Storage\ClassLikeStorage $class_storage, Type\Atomic\TNamedObject $lhs_type_part, string $method_name_lc, @@ -633,8 +646,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer \Psalm\StatementsSource $source, PhpParser\Node\Expr\MethodCall $stmt, StatementsAnalyzer $statements_analyzer, - string $fq_class_name, - ?string $lhs_var_id + string $fq_class_name ) { $naive_method_exists = false; @@ -695,76 +707,102 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer } } } - } elseif ($class_storage->mixin_declaring_fqcln - && $class_storage->namedMixins - ) { - foreach ($class_storage->namedMixins as $mixin) { - if (!$class_storage->mixin_declaring_fqcln) { - continue; - } + } - $new_method_id = new MethodIdentifier( - $mixin->value, - $method_name_lc + return [ + $lhs_type_part, + $class_storage, + $naive_method_exists, + $method_id, + $fq_class_name + ]; + } + + /** + * @param lowercase-string $method_name_lc + * @return array{Type\Atomic\TNamedObject, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string} + */ + private static function handleRegularMixins( + \Psalm\Storage\ClassLikeStorage $class_storage, + Type\Atomic\TNamedObject $lhs_type_part, + string $method_name_lc, + Codebase $codebase, + Context $context, + MethodIdentifier $method_id, + \Psalm\StatementsSource $source, + PhpParser\Node\Expr\MethodCall $stmt, + StatementsAnalyzer $statements_analyzer, + string $fq_class_name, + ?string $lhs_var_id + ) { + $naive_method_exists = false; + + foreach ($class_storage->namedMixins as $mixin) { + if (!$class_storage->mixin_declaring_fqcln) { + continue; + } + + $new_method_id = new MethodIdentifier( + $mixin->value, + $method_name_lc + ); + + if ($codebase->methods->methodExists( + $new_method_id, + $context->calling_method_id, + $codebase->collect_locations + ? new CodeLocation($source, $stmt->name) + : null, + !$context->collect_initializations + && !$context->collect_mutations + ? $statements_analyzer + : null, + $statements_analyzer->getFilePath() + )) { + $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( + $class_storage->mixin_declaring_fqcln ); - if ($codebase->methods->methodExists( - $new_method_id, - $context->calling_method_id, - $codebase->collect_locations - ? new CodeLocation($source, $stmt->name) - : null, - !$context->collect_initializations - && !$context->collect_mutations - ? $statements_analyzer - : null, - $statements_analyzer->getFilePath() - )) { - $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( - $class_storage->mixin_declaring_fqcln - ); + $mixin_class_template_params = ClassTemplateParamCollector::collect( + $codebase, + $mixin_declaring_class_storage, + $codebase->classlike_storage_provider->get($fq_class_name), + null, + $lhs_type_part, + $lhs_var_id === '$this' + ); - $mixin_class_template_params = ClassTemplateParamCollector::collect( - $codebase, - $mixin_declaring_class_storage, - $codebase->classlike_storage_provider->get($fq_class_name), - null, - $lhs_type_part, - $lhs_var_id === '$this' - ); + $lhs_type_part = clone $mixin; - $lhs_type_part = clone $mixin; + $lhs_type_part->replaceTemplateTypesWithArgTypes( + new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []), + $codebase + ); - $lhs_type_part->replaceTemplateTypesWithArgTypes( - new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []), - $codebase - ); + $lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion( + $codebase, + new Type\Union([$lhs_type_part]), + $mixin_declaring_class_storage->name, + $fq_class_name, + $class_storage->parent_class, + true, + false, + $class_storage->final + ); - $lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion( - $codebase, - new Type\Union([$lhs_type_part]), - $mixin_declaring_class_storage->name, - $fq_class_name, - $class_storage->parent_class, - true, - false, - $class_storage->final - ); + $new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0]; - $new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0]; - - if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) { - $lhs_type_part = $new_lhs_type_part; - } - - $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); - - $fq_class_name = $mixin_class_storage->name; - $mixin_class_storage->mixin_declaring_fqcln = $class_storage->mixin_declaring_fqcln; - $class_storage = $mixin_class_storage; - $naive_method_exists = true; - $method_id = $new_method_id; + if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) { + $lhs_type_part = $new_lhs_type_part; } + + $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); + + $fq_class_name = $mixin_class_storage->name; + $mixin_class_storage->mixin_declaring_fqcln = $class_storage->mixin_declaring_fqcln; + $class_storage = $mixin_class_storage; + $naive_method_exists = true; + $method_id = $new_method_id; } }