From b25264bc6ef93b6754f216627b85d7869770b854 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Mon, 4 Mar 2024 19:22:43 +0100 Subject: [PATCH] Add DefineType and ImportType attributes --- composer.json | 2 +- src/AttributeNodeVisitor.php | 45 ++++++++++++- tests/DefineTypeAttributeNodeVisitorTest.php | 70 ++++++++++++++++++++ tests/ImportTypeAttributeNodeVisitorTest.php | 70 ++++++++++++++++++++ tests/TypeAttributeNodeVisitorTest.php | 13 +++- 5 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 tests/DefineTypeAttributeNodeVisitorTest.php create mode 100644 tests/ImportTypeAttributeNodeVisitorTest.php diff --git a/composer.json b/composer.json index 879ecd0..14e126c 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.17 || dev-main" + "php-static-analysis/attributes": "^0.2.2 || dev-main" }, "require-dev": { "php-static-analysis/phpstan-extension": "dev-main", diff --git a/src/AttributeNodeVisitor.php b/src/AttributeNodeVisitor.php index 89644b6..e3d0de3 100644 --- a/src/AttributeNodeVisitor.php +++ b/src/AttributeNodeVisitor.php @@ -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; @@ -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"; @@ -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, @@ -79,6 +84,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract TemplateExtends::class, TemplateImplements::class, TemplateUse::class, + Type::class, ], Stmt\ClassConst::class => [ Deprecated::class, @@ -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, @@ -122,6 +130,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract Template::class, TemplateContravariant::class, TemplateCovariant::class, + Type::class, ], Stmt\Property::class => [ Deprecated::class, @@ -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, @@ -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, @@ -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', ], @@ -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', @@ -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, ], @@ -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); @@ -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]; @@ -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; } diff --git a/tests/DefineTypeAttributeNodeVisitorTest.php b/tests/DefineTypeAttributeNodeVisitorTest.php new file mode 100644 index 0000000..e593a75 --- /dev/null +++ b/tests/DefineTypeAttributeNodeVisitorTest.php @@ -0,0 +1,70 @@ +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])]); + } +} diff --git a/tests/ImportTypeAttributeNodeVisitorTest.php b/tests/ImportTypeAttributeNodeVisitorTest.php new file mode 100644 index 0000000..5872391 --- /dev/null +++ b/tests/ImportTypeAttributeNodeVisitorTest.php @@ -0,0 +1,70 @@ +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])]); + } +} diff --git a/tests/TypeAttributeNodeVisitorTest.php b/tests/TypeAttributeNodeVisitorTest.php index ffd0262..8e0435e 100644 --- a/tests/TypeAttributeNodeVisitorTest.php +++ b/tests/TypeAttributeNodeVisitorTest.php @@ -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'); @@ -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);