From 95dbb937320f93e1288e828c4941022af292b406 Mon Sep 17 00:00:00 2001 From: Brown Date: Mon, 27 Apr 2020 09:10:24 -0400 Subject: [PATCH] Fix #3237 - allow mixin to reference generic params --- .../Call/AtomicMethodCallAnalyzer.php | 33 ++++++++++++-- .../Internal/PhpVisitor/ReflectorVisitor.php | 43 +++++++++++++------ src/Psalm/Storage/ClassLikeStorage.php | 4 +- tests/MixinAnnotationTest.php | 22 ++++++++++ 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php index e9ad8b8d2..c6672059c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php @@ -254,12 +254,12 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer ); if (!$naive_method_exists - && $class_storage->mixin_param + && $class_storage->mixin instanceof Type\Atomic\TTemplateParam && $lhs_type_part instanceof Type\Atomic\TGenericObject && $class_storage->template_types ) { $param_position = \array_search( - $class_storage->mixin_param, + $class_storage->mixin->param_name, \array_keys($class_storage->template_types) ); @@ -289,7 +289,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer : null, $statements_analyzer->getFilePath() )) { - $lhs_type_part = $lhs_type_part_new; + $lhs_type_part = clone $lhs_type_part_new; $class_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value); $naive_method_exists = true; @@ -298,6 +298,33 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer } } } + } elseif (!$naive_method_exists + && $class_storage->mixin instanceof Type\Atomic\TNamedObject + ) { + $new_method_id = new MethodIdentifier( + $class_storage->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() + )) { + $fq_class_name = $class_storage->mixin->value; + $lhs_type_part = clone $class_storage->mixin; + $class_storage = $codebase->classlike_storage_provider->get($class_storage->mixin->value); + + $naive_method_exists = true; + $method_id = $new_method_id; + } } if (!$naive_method_exists diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 184862e8a..d66ac8f8d 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -1260,22 +1260,41 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $storage->psalm_internal = $docblock_info->psalm_internal; if ($docblock_info->mixin) { - if (isset($this->class_template_types[$docblock_info->mixin])) { - $storage->mixin_param = $docblock_info->mixin; - } else { - $mixin_fqcln = Type::getFQCLNFromString( + $mixin_type = Type::parseTokens( + Type::fixUpLocalType( $docblock_info->mixin, - $this->aliases - ); + $this->aliases, + $this->class_template_types + ), + null, + $this->class_template_types + ); - $storage->mixin_fqcln = $mixin_fqcln; + $mixin_type->queueClassLikesForScanning( + $this->codebase, + $this->file_storage, + $storage->template_types ?: [] + ); - $this->codebase->scanner->queueClassLikeForScanning($mixin_fqcln); - $this->file_storage->referenced_classlikes[strtolower($mixin_fqcln)] = $mixin_fqcln; + if ($mixin_type->isSingle()) { + $mixin_type = array_values($mixin_type->getAtomicTypes())[0]; - // if there's a mixin, assume it's the reason for the __call - $storage->sealed_properties = true; - $storage->sealed_methods = true; + if ($mixin_type instanceof Type\Atomic\TNamedObject) { + if ($mixin_type instanceof Type\Atomic\TGenericObject) { + $storage->mixin = $mixin_type; + } else { + $storage->mixin_fqcln = $mixin_type->value; + + $this->file_storage->referenced_classlikes[strtolower($storage->mixin_fqcln)] + = $storage->mixin_fqcln; + + // if there's a mixin, assume it's the reason for the __call + $storage->sealed_properties = true; + $storage->sealed_methods = true; + } + } elseif ($mixin_type instanceof Type\Atomic\TTemplateParam) { + $storage->mixin = $mixin_type; + } } } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 952d99a60..ceac014bb 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -103,9 +103,9 @@ class ClassLikeStorage public $mixin_fqcln = null; /** - * @var null|string + * @var null|Type\Atomic\TTemplateParam|Type\ATomic\TNamedObject */ - public $mixin_param = null; + public $mixin = null; /** * @var array diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php index 98c395de5..8e145582b 100644 --- a/tests/MixinAnnotationTest.php +++ b/tests/MixinAnnotationTest.php @@ -145,6 +145,28 @@ class MixinAnnotationTest extends TestCase '$b' => 'bool', ] ], + 'templatedMixin' => [ + ' + */ + class Bar {} + + $bar = new Bar(); + $b = $bar->hi();', + [ + '$b' => 'string', + ] + ], ]; } }