Skip to content

Commit

Permalink
Add DefineType and ImportType attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-granados committed Mar 4, 2024
1 parent 176cc97 commit b25264b
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 3 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.17 || dev-main"
"php-static-analysis/attributes": "^0.2.2 || dev-main"
},
"require-dev": {
"php-static-analysis/phpstan-extension": "dev-main",
Expand Down
45 changes: 44 additions & 1 deletion src/AttributeNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
use PhpParser\NodeVisitorAbstract;
use PhpStaticAnalysis\Attributes\DefineType;
use PhpStaticAnalysis\Attributes\Deprecated;
use PhpStaticAnalysis\Attributes\Immutable;
use PhpStaticAnalysis\Attributes\ImportType;
use PhpStaticAnalysis\Attributes\Impure;
use PhpStaticAnalysis\Attributes\Internal;
use PhpStaticAnalysis\Attributes\IsReadOnly;
Expand Down Expand Up @@ -50,6 +52,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
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_IN_TYPE = "many in type";
private const ARGS_MANY_WITHOUT_NAME = "many without name";
private const ARGS_MANY_WITHOUT_NAME_AND_PREFIX = "many without name and prexif";

Expand All @@ -65,8 +68,10 @@ class AttributeNodeVisitor extends NodeVisitorAbstract

private const ALLOWED_ATTRIBUTES_PER_NODE_TYPE = [
Stmt\Class_::class => [
DefineType::class,
Deprecated::class,
Immutable::class,
ImportType::class,
Internal::class,
Method::class,
Mixin::class,
Expand All @@ -79,6 +84,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
TemplateExtends::class,
TemplateImplements::class,
TemplateUse::class,
Type::class,
],
Stmt\ClassConst::class => [
Deprecated::class,
Expand Down Expand Up @@ -111,8 +117,10 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
Type::class,
],
Stmt\Interface_::class => [
DefineType::class,
Deprecated::class,
Immutable::class,
ImportType::class,
Internal::class,
Method::class,
Mixin::class,
Expand All @@ -122,6 +130,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
Template::class,
TemplateContravariant::class,
TemplateCovariant::class,
Type::class,
],
Stmt\Property::class => [
Deprecated::class,
Expand All @@ -131,8 +140,10 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
Type::class,
],
Stmt\Trait_::class => [
DefineType::class,
Deprecated::class,
Immutable::class,
ImportType::class,
Internal::class,
Method::class,
Mixin::class,
Expand All @@ -144,12 +155,15 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
Template::class,
TemplateContravariant::class,
TemplateCovariant::class,
Type::class,
],
];

private const SHORT_NAME_TO_FQN = [
'DefineType' => DefineType::class,
'Deprecated' => Deprecated::class,
'Immutable' => Immutable::class,
'ImportType' => ImportType::class,
'Impure' => Impure::class,
'Internal' => Internal::class,
'IsReadOnly' => IsReadOnly::class,
Expand All @@ -176,12 +190,18 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
];

private const ANNOTATION_PER_ATTRIBUTE = [
DefineType::class => [
'all' => 'type',
],
Deprecated::class => [
'all' => 'deprecated',
],
Immutable::class => [
'all' => 'immutable',
],
ImportType::class => [
'all' => 'import-type',
],
Impure::class => [
'all' => 'impure',
],
Expand Down Expand Up @@ -250,6 +270,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
'all' => 'throws',
],
Type::class => [
Stmt\Class_::class => 'type',
Stmt\ClassConst::class => 'var',
Stmt\ClassMethod::class => 'return',
Stmt\Function_::class => 'return',
Expand All @@ -258,12 +279,18 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
];

private const ARGUMENTS_PER_ATTRIBUTE = [
DefineType::class => [
'all' => self::ARGS_MANY_IN_TYPE,
],
Deprecated::class => [
'all' => self::ARGS_NONE,
],
Immutable::class => [
'all' => self::ARGS_NONE_WITH_PREFIX,
],
ImportType::class => [
'all' => self::ARGS_MANY_IN_TYPE,
],
Impure::class => [
'all' => self::ARGS_NONE_WITH_PREFIX,
],
Expand Down Expand Up @@ -447,6 +474,12 @@ public function enterNode(Node $node)
}
}
break;
case self::ARGS_MANY_IN_TYPE:
foreach ($args as $arg) {
$tagsToAdd[] = $this->createTag($nodeType, $attributeName, $arg, prefixWithName: true, prefix: $this->toolType);
$tagCreated = true;
}
break;
}
if ($tagCreated) {
$this->updatePositions($attribute);
Expand Down Expand Up @@ -474,7 +507,8 @@ private function createTag(
Arg $of = null,
bool $useName = false,
string $nameToUse = null,
string $prefix = null
string $prefix = null,
bool $prefixWithName = false
): string {
if (array_key_exists($nodeType, self::ANNOTATION_PER_ATTRIBUTE[$attributeName])) {
$tagName = self::ANNOTATION_PER_ATTRIBUTE[$attributeName][$nodeType];
Expand Down Expand Up @@ -502,6 +536,15 @@ private function createTag(
$type = '\\' . $type;
}
}
if ($prefixWithName) {
$alias = $argument->name;
if ($alias instanceof Node\Identifier) {
if ($attributeName === ImportType::class) {
$type = 'from ' . $type;
}
$type = $alias->toString() . ' ' . $type;
}
}
if ($type !== '') {
$tag .= ' ' . $type;
}
Expand Down
70 changes: 70 additions & 0 deletions tests/DefineTypeAttributeNodeVisitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace test\PhpStaticAnalysis\NodeVisitor;

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PhpStaticAnalysis\Attributes\DefineType;

class DefineTypeAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
{
public function testAddsDefineTypePHPDoc(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addDefineTypeAttributesToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
}

public function testAddsDefineTypePHPDocWithoutArgumentName(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addDefineTypeAttributesToNode($node, useArgumentName: false);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
}

public function testAddsSeveralDefineTypePHPDocs(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addDefineTypeAttributesToNode($node, 2);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @type StringArray string[]\n * @type StringArray string[]\n */", $docText);
}

public function testAddsMultipleDefineTypePHPDocs(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addDefineTypeAttributesToNode($node);
$this->addDefineTypeAttributesToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @type StringArray string[]\n * @type StringArray string[]\n */", $docText);
}

private function addDefineTypeAttributesToNode(Node\Stmt\Class_ $node, int $num = 1, bool $useArgumentName = true): void
{
$args = [];
if ($useArgumentName) {
$name = new Identifier('StringArray');
$value = new Node\Scalar\String_('string[]');
for ($i = 0; $i < $num; $i++) {
$args[] = new Node\Arg($value, name: $name);
}
} else {
$value = new Node\Scalar\String_('StringArray string[]');
for ($i = 0; $i < $num; $i++) {
$args[] = new Node\Arg($value);
}
}
$attributeName = new FullyQualified(DefineType::class);
$attribute = new Attribute($attributeName, $args);
$node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]);
}
}
70 changes: 70 additions & 0 deletions tests/ImportTypeAttributeNodeVisitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace test\PhpStaticAnalysis\NodeVisitor;

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PhpStaticAnalysis\Attributes\ImportType;

class ImportTypeAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
{
public function testAddsImportTypePHPDoc(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addImportTypeAttributesToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n */", $docText);
}

public function testAddsImportTypePHPDocWithoutArgumentName(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addImportTypeAttributesToNode($node, useArgumentName: false);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n */", $docText);
}

public function testAddsSeveralImportTypePHPDocs(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addImportTypeAttributesToNode($node, 2);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n * @import-type StringArray from StringClass\n */", $docText);
}

public function testAddsMultipleImportTypePHPDocs(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addImportTypeAttributesToNode($node);
$this->addImportTypeAttributesToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n * @import-type StringArray from StringClass\n */", $docText);
}

private function addImportTypeAttributesToNode(Node\Stmt\Class_ $node, int $num = 1, bool $useArgumentName = true): void
{
$args = [];
if ($useArgumentName) {
$name = new Identifier('StringArray');
$value = new Node\Scalar\String_('StringClass');
for ($i = 0; $i < $num; $i++) {
$args[] = new Node\Arg($value, name: $name);
}
} else {
$value = new Node\Scalar\String_('StringArray from StringClass');
for ($i = 0; $i < $num; $i++) {
$args[] = new Node\Arg($value);
}
}
$attributeName = new FullyQualified(ImportType::class);
$attribute = new Attribute($attributeName, $args);
$node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]);
}
}
13 changes: 12 additions & 1 deletion tests/TypeAttributeNodeVisitorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public function testAddsVarPHPDoc(): void
$this->assertEquals("/**\n * @var string\n */", $docText);
}

public function testAddsTypePHPDoc(): void
{
$node = new Node\Stmt\Class_('Test');
$this->addTypeAttributeToNode($node);
$this->nodeVisitor->enterNode($node);
$docText = $this->getDocText($node);
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
}

public function testAddsReturnPHPDocWithTypeAttribute(): void
{
$node = new Node\Stmt\ClassMethod('Test');
Expand All @@ -28,9 +37,11 @@ public function testAddsReturnPHPDocWithTypeAttribute(): void
$this->assertEquals("/**\n * @return string\n */", $docText);
}

private function addTypeAttributeToNode(Node\Stmt\Property|Node\Stmt\ClassMethod $node): void
private function addTypeAttributeToNode(Node\Stmt\Property|Node\Stmt\ClassMethod|Node\Stmt\Class_ $node): void
{
$args = [
$node instanceof Node\Stmt\Class_ ?
new Node\Arg(new Node\Scalar\String_('StringArray string[]')) :
new Node\Arg(new Node\Scalar\String_('string'))
];
$attributeName = new FullyQualified(Type::class);
Expand Down

0 comments on commit b25264b

Please sign in to comment.