From d0538e55b499b9969f88a53d8a22beb78c0c750c Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Thu, 22 Feb 2024 20:22:40 +0100 Subject: [PATCH] Add the TemplateExtends, TemplateImplements and TemplateUse attributes --- composer.json | 2 +- src/AttributeNodeVisitor.php | 135 ++++++++++++++---- ...emplateExtendsAttributeNodeVisitorTest.php | 32 +++++ ...lateImplementsAttributeNodeVisitorTest.php | 52 +++++++ tests/TemplateUseAttributeNodeVisitorTest.php | 124 ++++++++++++++++ 5 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 tests/TemplateExtendsAttributeNodeVisitorTest.php create mode 100644 tests/TemplateImplementsAttributeNodeVisitorTest.php create mode 100644 tests/TemplateUseAttributeNodeVisitorTest.php diff --git a/composer.json b/composer.json index 40662c3..5053f71 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": ">=8.0", "nikic/php-parser": "^4 || ^5", - "php-static-analysis/attributes": "^0.1.10 || dev-main" + "php-static-analysis/attributes": "^0.1.11 || dev-main" }, "require-dev": { "php-static-analysis/phpstan-extension": "dev-main", diff --git a/src/AttributeNodeVisitor.php b/src/AttributeNodeVisitor.php index ae17d88..596060e 100644 --- a/src/AttributeNodeVisitor.php +++ b/src/AttributeNodeVisitor.php @@ -24,6 +24,9 @@ use PhpStaticAnalysis\Attributes\Template; use PhpStaticAnalysis\Attributes\TemplateContravariant; use PhpStaticAnalysis\Attributes\TemplateCovariant; +use PhpStaticAnalysis\Attributes\TemplateExtends; +use PhpStaticAnalysis\Attributes\TemplateImplements; +use PhpStaticAnalysis\Attributes\TemplateUse; use PhpStaticAnalysis\Attributes\Type; class AttributeNodeVisitor extends NodeVisitorAbstract @@ -32,6 +35,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract private const ARGS_ONE = 'one'; private const ARGS_ONE_OPTIONAL = 'one_optional'; private const ARGS_TWO_WITH_TYPE = 'two with type'; + private const ARGS_MANY_IN_USE = "many in use"; private const ARGS_MANY_WITH_NAME = "many with name"; private const ARGS_MANY_WITHOUT_NAME = "many without name"; @@ -57,6 +61,9 @@ class AttributeNodeVisitor extends NodeVisitorAbstract Template::class, TemplateContravariant::class, TemplateCovariant::class, + TemplateExtends::class, + TemplateImplements::class, + TemplateUse::class, ], Stmt\ClassConst::class => [ Deprecated::class, @@ -126,6 +133,9 @@ class AttributeNodeVisitor extends NodeVisitorAbstract 'Template' => Template::class, 'TemplateContravariant' => TemplateContravariant::class, 'TemplateCovariant' => TemplateCovariant::class, + 'TemplateExtends' => TemplateExtends::class, + 'TemplateImplements' => TemplateImplements::class, + 'TemplateUse' => TemplateUse::class, 'Type' => Type::class, ]; @@ -170,6 +180,15 @@ class AttributeNodeVisitor extends NodeVisitorAbstract TemplateCovariant::class => [ 'all' => 'template-covariant', ], + TemplateExtends::class => [ + 'all' => 'template-extends', + ], + TemplateImplements::class => [ + 'all' => 'template-implements', + ], + TemplateUse::class => [ + 'all' => 'template-use', + ], Type::class => [ Stmt\ClassConst::class => 'var', Stmt\ClassMethod::class => 'return', @@ -219,6 +238,15 @@ class AttributeNodeVisitor extends NodeVisitorAbstract TemplateCovariant::class => [ 'all' => self::ARGS_TWO_WITH_TYPE, ], + TemplateExtends::class => [ + 'all' => self::ARGS_ONE, + ], + TemplateImplements::class => [ + 'all' => self::ARGS_MANY_WITHOUT_NAME, + ], + TemplateUse::class => [ + 'all' => self::ARGS_MANY_IN_USE, + ], Type::class => [ 'all' => self::ARGS_ONE ], @@ -242,6 +270,7 @@ public function enterNode(Node $node) if (in_array($node::class, self::ALLOWED_NODE_TYPES)) { /** @var Stmt\Class_|Stmt\ClassConst|Stmt\ClassMethod|Stmt\Function_|Stmt\Interface_|Stmt\Property|Stmt\Trait_ $node */ $tagsToAdd = []; + $useTagsToAdd = []; $attributeGroups = $node->attrGroups; $nodeType = $node::class; @@ -306,10 +335,18 @@ public function enterNode(Node $node) break; case self::ARGS_MANY_WITHOUT_NAME: foreach ($args as $arg) { - $tagsToAdd[] = $this->createTag($nodeType, $attributeName, $arg, useName: false); + $tagsToAdd[] = $this->createTag($nodeType, $attributeName, $arg); $tagCreated = true; } break; + case self::ARGS_MANY_IN_USE: + foreach ($args as $arg) { + if ($arg->value instanceof String_) { + $useValue = $arg->value->value; + $useTagsToAdd[$useValue] = $this->createTag($nodeType, $attributeName, $arg); + } + } + break; } if ($tagCreated) { $this->updatePositions($attribute); @@ -318,37 +355,14 @@ public function enterNode(Node $node) } } if ($node instanceof Stmt\ClassMethod || $node instanceof Stmt\Function_) { - foreach ($node->getParams() as $param) { - $attributeGroups = $param->attrGroups; - foreach ($attributeGroups as $attributeGroup) { - $attributes = $attributeGroup->attrs; - foreach ($attributes as $attribute) { - $attributeName = $attribute->name->toString(); - $attributeName = self::SHORT_NAME_TO_FQN[$attributeName] ?? $attributeName; - if ($attributeName === Param::class) { - $args = $attribute->args; - $tagCreated = false; - if (isset($args[0])) { - $var = $param->var; - if ($var instanceof Node\Expr\Variable) { - $name = $var->name; - if (is_string($name)) { - $tagsToAdd[] = $this->createTag($nodeType, $attributeName, $args[0], useName: true, nameToUse: $name); - $tagCreated = true; - } - } - } - if ($tagCreated) { - $this->updatePositions($attribute); - } - } - } - } - } + $tagsToAdd = array_merge($tagsToAdd, $this->getParamTagsFromParams($node)); } if ($tagsToAdd !== []) { $this->addDocTagsToNode($tagsToAdd, $node); } + if ($useTagsToAdd !== [] && $node instanceof Stmt\Class_) { + $this->addUseDocTagsToNodeTraitUses($useTagsToAdd, $node); + } } return $node; } @@ -404,6 +418,41 @@ private function createTag( return $tag; } + #[Returns('string[]')] + private function getParamTagsFromParams(Stmt\ClassMethod|Stmt\Function_ $node): array + { + $nodeType = $node::class; + $tagsToAdd = []; + foreach ($node->getParams() as $param) { + $attributeGroups = $param->attrGroups; + foreach ($attributeGroups as $attributeGroup) { + $attributes = $attributeGroup->attrs; + foreach ($attributes as $attribute) { + $attributeName = $attribute->name->toString(); + $attributeName = self::SHORT_NAME_TO_FQN[$attributeName] ?? $attributeName; + if ($attributeName === Param::class) { + $args = $attribute->args; + $tagCreated = false; + if (isset($args[0])) { + $var = $param->var; + if ($var instanceof Node\Expr\Variable) { + $name = $var->name; + if (is_string($name)) { + $tagsToAdd[] = $this->createTag($nodeType, $attributeName, $args[0], useName: true, nameToUse: $name); + $tagCreated = true; + } + } + } + if ($tagCreated) { + $this->updatePositions($attribute); + } + } + } + } + } + return $tagsToAdd; + } + #[Param(tagsToAdd: 'string[]')] private function addDocTagsToNode(array $tagsToAdd, Node $node): void { @@ -434,6 +483,36 @@ private function addDocTagsToNode(array $tagsToAdd, Node $node): void ); $node->setDocComment($docComment); } + + #[Param(useTagsToAdd: 'string[]')] + private function addUseDocTagsToNodeTraitUses(array $useTagsToAdd, Stmt\Class_ $node): void + { + $this->initPositions(); + foreach ($node->stmts as $stmt) { + if ($stmt instanceof Stmt\TraitUse) { + foreach ($stmt->traits as $trait) { + foreach ($useTagsToAdd as $tagValue => $useTag) { + $tagParts = explode('<', (string)$tagValue); + $tagName = $tagParts[0]; + $parts = array_reverse(explode('\\', $tagName)); + $traitParts = array_reverse($trait->getParts()); + $useMatches = true; + foreach ($parts as $i => $part) { + if (!isset($traitParts[$i]) || $traitParts[$i] !== $part) { + $useMatches = false; + break; + } + } + if ($useMatches) { + $this->addDocTagsToNode([$useTag], $stmt); + unset($useTagsToAdd[$tagName]); + break; + } + } + } + } + } + } private function updatePositions(Node|Comment $node): void { diff --git a/tests/TemplateExtendsAttributeNodeVisitorTest.php b/tests/TemplateExtendsAttributeNodeVisitorTest.php new file mode 100644 index 0000000..58f9170 --- /dev/null +++ b/tests/TemplateExtendsAttributeNodeVisitorTest.php @@ -0,0 +1,32 @@ +addTemplateExtendsAttributeToNode($node); + $this->nodeVisitor->enterNode($node); + $docText = $this->getDocText($node); + $this->assertEquals("/**\n * @template-extends TemplateClass\n */", $docText); + } + + private function addTemplateExtendsAttributeToNode(Node\Stmt\Class_ $node): void + { + $args = [ + new Node\Arg(new Node\Scalar\String_('TemplateClass')) + ]; + $attributeName = new FullyQualified(TemplateExtends::class); + $attribute = new Attribute($attributeName, $args); + $node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]); + } +} diff --git a/tests/TemplateImplementsAttributeNodeVisitorTest.php b/tests/TemplateImplementsAttributeNodeVisitorTest.php new file mode 100644 index 0000000..95fffb8 --- /dev/null +++ b/tests/TemplateImplementsAttributeNodeVisitorTest.php @@ -0,0 +1,52 @@ +addTemplateImplementsAttributesToNode($node); + $this->nodeVisitor->enterNode($node); + $docText = $this->getDocText($node); + $this->assertEquals("/**\n * @template-implements TemplateInterface\n */", $docText); + } + + public function testAddsSeveralTemplateImplementsPHPDocs(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTemplateImplementsAttributesToNode($node, 2); + $this->nodeVisitor->enterNode($node); + $docText = $this->getDocText($node); + $this->assertEquals("/**\n * @template-implements TemplateInterface\n * @template-implements TemplateInterface\n */", $docText); + } + + public function testAddsMultipleTemplateImplementsPHPDocs(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTemplateImplementsAttributesToNode($node); + $this->addTemplateImplementsAttributesToNode($node); + $this->nodeVisitor->enterNode($node); + $docText = $this->getDocText($node); + $this->assertEquals("/**\n * @template-implements TemplateInterface\n * @template-implements TemplateInterface\n */", $docText); + } + + private function addTemplateImplementsAttributesToNode(Node\Stmt\Class_ $node, int $num = 1): void + { + $value = new Node\Scalar\String_('TemplateInterface'); + $args = []; + for ($i = 0; $i < $num; $i++) { + $args[] = new Node\Arg($value); + } + $attributeName = new FullyQualified(TemplateImplements::class); + $attribute = new Attribute($attributeName, $args); + $node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]); + } +} diff --git a/tests/TemplateUseAttributeNodeVisitorTest.php b/tests/TemplateUseAttributeNodeVisitorTest.php new file mode 100644 index 0000000..a330d61 --- /dev/null +++ b/tests/TemplateUseAttributeNodeVisitorTest.php @@ -0,0 +1,124 @@ +addTraitUsesToClass($node, ['test']); + $this->addTemplateUseAttributeToNode($node, 'test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, 'test'); + $this->assertEquals("/**\n * @template-use test\n */", $docText); + } + + public function testAddsTemplateUsePHPDocWithFQN(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTraitUsesToClass($node, ['\PhpStaticAnalysis\NodeVisitor\test']); + $this->addTemplateUseAttributeToNode($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->assertEquals("/**\n * @template-use \\PhpStaticAnalysis\\NodeVisitor\\test\n */", $docText); + } + + public function testAddsTemplateUsePHPDocWithPartialMatch(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTraitUsesToClass($node, ['\PhpStaticAnalysis\NodeVisitor\test']); + $this->addTemplateUseAttributeToNode($node, 'NodeVisitor\test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->assertEquals("/**\n * @template-use NodeVisitor\\test\n */", $docText); + } + + public function testDoesNotAddTemplateUsePHPDocWithoutFullMatch(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTraitUsesToClass($node, ['\PhpStaticAnalysis\NodeVisitor\test']); + $this->addTemplateUseAttributeToNode($node, '\Other\NodeVisitor\test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->assertEquals("", $docText); + } + + public function testAddsTemplateUsePHPDocToTheRightTraitUse(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTraitUsesToClass($node, [ + '\Other\NodeVisitor\test', + '\PhpStaticAnalysis\NodeVisitor\test' + ]); + $this->addTemplateUseAttributeToNode($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, '\Other\NodeVisitor\test'); + $this->assertEquals("", $docText); + $docText = $this->getUseDocText($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->assertEquals("/**\n * @template-use \\PhpStaticAnalysis\\NodeVisitor\\test\n */", $docText); + } + + public function testAddsSeveralTemplateUsePHPDocs(): void + { + $node = new Node\Stmt\Class_('Test'); + $this->addTraitUsesToClass($node, [ + '\Other\template', + '\PhpStaticAnalysis\NodeVisitor\test' + ]); + $this->addTemplateUseAttributeToNode($node, 'template'); + $this->addTemplateUseAttributeToNode($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->nodeVisitor->enterNode($node); + $docText = $this->getUseDocText($node, '\Other\template'); + $this->assertEquals("/**\n * @template-use template\n */", $docText); + $docText = $this->getUseDocText($node, '\PhpStaticAnalysis\NodeVisitor\test'); + $this->assertEquals("/**\n * @template-use \\PhpStaticAnalysis\\NodeVisitor\\test\n */", $docText); + } + + #[Param(traitNames: 'string[]')] + private function addTraitUsesToClass(Node\Stmt\Class_ $node, array $traitNames): void + { + foreach ($traitNames as $traitName) { + $trait = new FullyQualified($traitName); + $useTrait = new Node\Stmt\TraitUse([$trait]); + $node->stmts[] = $useTrait; + } + } + + private function addTemplateUseAttributeToNode(Node\Stmt\Class_ $node, string $templateName): void + { + $value = new Node\Scalar\String_($templateName . ''); + $arg = new Node\Arg($value); + $attributeName = new FullyQualified(TemplateUse::class); + $attribute = new Attribute($attributeName, [$arg]); + $node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]); + } + + private function getUseDocText(Node\Stmt\Class_ $node, string $traitName): string + { + $docText = ''; + foreach ($node->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\TraitUse) { + foreach ($stmt->traits as $trait) { + if ($traitName == (string)$trait) { + $docComment = $stmt->getDocComment(); + if ($docComment instanceof Doc) { + $docText = $docComment->getText(); + break; + } + } + } + } + } + return $docText; + } +}