Skip to content

Commit

Permalink
Add the TemplateExtends, TemplateImplements and TemplateUse attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-granados committed Feb 23, 2024
1 parent c8ab752 commit d0538e5
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 29 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
135 changes: 107 additions & 28 deletions src/AttributeNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";

Expand All @@ -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,
Expand Down Expand Up @@ -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,
];

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
],
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down
32 changes: 32 additions & 0 deletions tests/TemplateExtendsAttributeNodeVisitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace test\PhpStaticAnalysis\NodeVisitor;

use Exception;
use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Name\FullyQualified;
use PhpStaticAnalysis\Attributes\TemplateExtends;

class TemplateExtendsAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
{
public function testAddsTemplateExtendsPHPDoc(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addTemplateExtendsAttributeToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @template-extends TemplateClass<int>\n */", $docText);
}

private function addTemplateExtendsAttributeToNode(Node\Stmt\Class_ $node): void
{
$args = [
new Node\Arg(new Node\Scalar\String_('TemplateClass<int>'))
];
$attributeName = new FullyQualified(TemplateExtends::class);
$attribute = new Attribute($attributeName, $args);
$node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]);
}
}
52 changes: 52 additions & 0 deletions tests/TemplateImplementsAttributeNodeVisitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace test\PhpStaticAnalysis\NodeVisitor;

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Name\FullyQualified;
use PhpStaticAnalysis\Attributes\TemplateImplements;

class TemplateImplementsAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
{
public function testAddsTemplateImplementsPHPDoc(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addTemplateImplementsAttributesToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @template-implements TemplateInterface<int>\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<int>\n * @template-implements TemplateInterface<int>\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<int>\n * @template-implements TemplateInterface<int>\n */", $docText);
}

private function addTemplateImplementsAttributesToNode(Node\Stmt\Class_ $node, int $num = 1): void
{
$value = new Node\Scalar\String_('TemplateInterface<int>');
$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])]);
}
}
Loading

0 comments on commit d0538e5

Please sign in to comment.