Skip to content

Commit b25264b

Browse files
Add DefineType and ImportType attributes
1 parent 176cc97 commit b25264b

File tree

5 files changed

+197
-3
lines changed

5 files changed

+197
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"require": {
2626
"php": ">=8.0",
2727
"nikic/php-parser": "^4 || ^5",
28-
"php-static-analysis/attributes": "^0.1.17 || dev-main"
28+
"php-static-analysis/attributes": "^0.2.2 || dev-main"
2929
},
3030
"require-dev": {
3131
"php-static-analysis/phpstan-extension": "dev-main",

src/AttributeNodeVisitor.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
use PhpParser\Node\Scalar\String_;
1212
use PhpParser\Node\Stmt;
1313
use PhpParser\NodeVisitorAbstract;
14+
use PhpStaticAnalysis\Attributes\DefineType;
1415
use PhpStaticAnalysis\Attributes\Deprecated;
1516
use PhpStaticAnalysis\Attributes\Immutable;
17+
use PhpStaticAnalysis\Attributes\ImportType;
1618
use PhpStaticAnalysis\Attributes\Impure;
1719
use PhpStaticAnalysis\Attributes\Internal;
1820
use PhpStaticAnalysis\Attributes\IsReadOnly;
@@ -50,6 +52,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
5052
private const ARGS_TWO_WITH_TYPE = 'two with type';
5153
private const ARGS_MANY_IN_USE = "many in use";
5254
private const ARGS_MANY_WITH_NAME = "many with name";
55+
private const ARGS_MANY_IN_TYPE = "many in type";
5356
private const ARGS_MANY_WITHOUT_NAME = "many without name";
5457
private const ARGS_MANY_WITHOUT_NAME_AND_PREFIX = "many without name and prexif";
5558

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

6669
private const ALLOWED_ATTRIBUTES_PER_NODE_TYPE = [
6770
Stmt\Class_::class => [
71+
DefineType::class,
6872
Deprecated::class,
6973
Immutable::class,
74+
ImportType::class,
7075
Internal::class,
7176
Method::class,
7277
Mixin::class,
@@ -79,6 +84,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
7984
TemplateExtends::class,
8085
TemplateImplements::class,
8186
TemplateUse::class,
87+
Type::class,
8288
],
8389
Stmt\ClassConst::class => [
8490
Deprecated::class,
@@ -111,8 +117,10 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
111117
Type::class,
112118
],
113119
Stmt\Interface_::class => [
120+
DefineType::class,
114121
Deprecated::class,
115122
Immutable::class,
123+
ImportType::class,
116124
Internal::class,
117125
Method::class,
118126
Mixin::class,
@@ -122,6 +130,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
122130
Template::class,
123131
TemplateContravariant::class,
124132
TemplateCovariant::class,
133+
Type::class,
125134
],
126135
Stmt\Property::class => [
127136
Deprecated::class,
@@ -131,8 +140,10 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
131140
Type::class,
132141
],
133142
Stmt\Trait_::class => [
143+
DefineType::class,
134144
Deprecated::class,
135145
Immutable::class,
146+
ImportType::class,
136147
Internal::class,
137148
Method::class,
138149
Mixin::class,
@@ -144,12 +155,15 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
144155
Template::class,
145156
TemplateContravariant::class,
146157
TemplateCovariant::class,
158+
Type::class,
147159
],
148160
];
149161

150162
private const SHORT_NAME_TO_FQN = [
163+
'DefineType' => DefineType::class,
151164
'Deprecated' => Deprecated::class,
152165
'Immutable' => Immutable::class,
166+
'ImportType' => ImportType::class,
153167
'Impure' => Impure::class,
154168
'Internal' => Internal::class,
155169
'IsReadOnly' => IsReadOnly::class,
@@ -176,12 +190,18 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
176190
];
177191

178192
private const ANNOTATION_PER_ATTRIBUTE = [
193+
DefineType::class => [
194+
'all' => 'type',
195+
],
179196
Deprecated::class => [
180197
'all' => 'deprecated',
181198
],
182199
Immutable::class => [
183200
'all' => 'immutable',
184201
],
202+
ImportType::class => [
203+
'all' => 'import-type',
204+
],
185205
Impure::class => [
186206
'all' => 'impure',
187207
],
@@ -250,6 +270,7 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
250270
'all' => 'throws',
251271
],
252272
Type::class => [
273+
Stmt\Class_::class => 'type',
253274
Stmt\ClassConst::class => 'var',
254275
Stmt\ClassMethod::class => 'return',
255276
Stmt\Function_::class => 'return',
@@ -258,12 +279,18 @@ class AttributeNodeVisitor extends NodeVisitorAbstract
258279
];
259280

260281
private const ARGUMENTS_PER_ATTRIBUTE = [
282+
DefineType::class => [
283+
'all' => self::ARGS_MANY_IN_TYPE,
284+
],
261285
Deprecated::class => [
262286
'all' => self::ARGS_NONE,
263287
],
264288
Immutable::class => [
265289
'all' => self::ARGS_NONE_WITH_PREFIX,
266290
],
291+
ImportType::class => [
292+
'all' => self::ARGS_MANY_IN_TYPE,
293+
],
267294
Impure::class => [
268295
'all' => self::ARGS_NONE_WITH_PREFIX,
269296
],
@@ -447,6 +474,12 @@ public function enterNode(Node $node)
447474
}
448475
}
449476
break;
477+
case self::ARGS_MANY_IN_TYPE:
478+
foreach ($args as $arg) {
479+
$tagsToAdd[] = $this->createTag($nodeType, $attributeName, $arg, prefixWithName: true, prefix: $this->toolType);
480+
$tagCreated = true;
481+
}
482+
break;
450483
}
451484
if ($tagCreated) {
452485
$this->updatePositions($attribute);
@@ -474,7 +507,8 @@ private function createTag(
474507
Arg $of = null,
475508
bool $useName = false,
476509
string $nameToUse = null,
477-
string $prefix = null
510+
string $prefix = null,
511+
bool $prefixWithName = false
478512
): string {
479513
if (array_key_exists($nodeType, self::ANNOTATION_PER_ATTRIBUTE[$attributeName])) {
480514
$tagName = self::ANNOTATION_PER_ATTRIBUTE[$attributeName][$nodeType];
@@ -502,6 +536,15 @@ private function createTag(
502536
$type = '\\' . $type;
503537
}
504538
}
539+
if ($prefixWithName) {
540+
$alias = $argument->name;
541+
if ($alias instanceof Node\Identifier) {
542+
if ($attributeName === ImportType::class) {
543+
$type = 'from ' . $type;
544+
}
545+
$type = $alias->toString() . ' ' . $type;
546+
}
547+
}
505548
if ($type !== '') {
506549
$tag .= ' ' . $type;
507550
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\NodeVisitor;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Attribute;
7+
use PhpParser\Node\AttributeGroup;
8+
use PhpParser\Node\Identifier;
9+
use PhpParser\Node\Name\FullyQualified;
10+
use PhpStaticAnalysis\Attributes\DefineType;
11+
12+
class DefineTypeAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
13+
{
14+
public function testAddsDefineTypePHPDoc(): void
15+
{
16+
$node = new Node\Stmt\Class_('Test');
17+
$this->addDefineTypeAttributesToNode($node);
18+
$this->nodeVisitor->enterNode($node);
19+
$docText = $this->getDocText($node);
20+
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
21+
}
22+
23+
public function testAddsDefineTypePHPDocWithoutArgumentName(): void
24+
{
25+
$node = new Node\Stmt\Class_('Test');
26+
$this->addDefineTypeAttributesToNode($node, useArgumentName: false);
27+
$this->nodeVisitor->enterNode($node);
28+
$docText = $this->getDocText($node);
29+
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
30+
}
31+
32+
public function testAddsSeveralDefineTypePHPDocs(): void
33+
{
34+
$node = new Node\Stmt\Class_('Test');
35+
$this->addDefineTypeAttributesToNode($node, 2);
36+
$this->nodeVisitor->enterNode($node);
37+
$docText = $this->getDocText($node);
38+
$this->assertEquals("/**\n * @type StringArray string[]\n * @type StringArray string[]\n */", $docText);
39+
}
40+
41+
public function testAddsMultipleDefineTypePHPDocs(): void
42+
{
43+
$node = new Node\Stmt\Class_('Test');
44+
$this->addDefineTypeAttributesToNode($node);
45+
$this->addDefineTypeAttributesToNode($node);
46+
$this->nodeVisitor->enterNode($node);
47+
$docText = $this->getDocText($node);
48+
$this->assertEquals("/**\n * @type StringArray string[]\n * @type StringArray string[]\n */", $docText);
49+
}
50+
51+
private function addDefineTypeAttributesToNode(Node\Stmt\Class_ $node, int $num = 1, bool $useArgumentName = true): void
52+
{
53+
$args = [];
54+
if ($useArgumentName) {
55+
$name = new Identifier('StringArray');
56+
$value = new Node\Scalar\String_('string[]');
57+
for ($i = 0; $i < $num; $i++) {
58+
$args[] = new Node\Arg($value, name: $name);
59+
}
60+
} else {
61+
$value = new Node\Scalar\String_('StringArray string[]');
62+
for ($i = 0; $i < $num; $i++) {
63+
$args[] = new Node\Arg($value);
64+
}
65+
}
66+
$attributeName = new FullyQualified(DefineType::class);
67+
$attribute = new Attribute($attributeName, $args);
68+
$node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]);
69+
}
70+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace test\PhpStaticAnalysis\NodeVisitor;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Attribute;
7+
use PhpParser\Node\AttributeGroup;
8+
use PhpParser\Node\Identifier;
9+
use PhpParser\Node\Name\FullyQualified;
10+
use PhpStaticAnalysis\Attributes\ImportType;
11+
12+
class ImportTypeAttributeNodeVisitorTest extends AttributeNodeVisitorTestBase
13+
{
14+
public function testAddsImportTypePHPDoc(): void
15+
{
16+
$node = new Node\Stmt\Class_('Test');
17+
$this->addImportTypeAttributesToNode($node);
18+
$this->nodeVisitor->enterNode($node);
19+
$docText = $this->getDocText($node);
20+
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n */", $docText);
21+
}
22+
23+
public function testAddsImportTypePHPDocWithoutArgumentName(): void
24+
{
25+
$node = new Node\Stmt\Class_('Test');
26+
$this->addImportTypeAttributesToNode($node, useArgumentName: false);
27+
$this->nodeVisitor->enterNode($node);
28+
$docText = $this->getDocText($node);
29+
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n */", $docText);
30+
}
31+
32+
public function testAddsSeveralImportTypePHPDocs(): void
33+
{
34+
$node = new Node\Stmt\Class_('Test');
35+
$this->addImportTypeAttributesToNode($node, 2);
36+
$this->nodeVisitor->enterNode($node);
37+
$docText = $this->getDocText($node);
38+
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n * @import-type StringArray from StringClass\n */", $docText);
39+
}
40+
41+
public function testAddsMultipleImportTypePHPDocs(): void
42+
{
43+
$node = new Node\Stmt\Class_('Test');
44+
$this->addImportTypeAttributesToNode($node);
45+
$this->addImportTypeAttributesToNode($node);
46+
$this->nodeVisitor->enterNode($node);
47+
$docText = $this->getDocText($node);
48+
$this->assertEquals("/**\n * @import-type StringArray from StringClass\n * @import-type StringArray from StringClass\n */", $docText);
49+
}
50+
51+
private function addImportTypeAttributesToNode(Node\Stmt\Class_ $node, int $num = 1, bool $useArgumentName = true): void
52+
{
53+
$args = [];
54+
if ($useArgumentName) {
55+
$name = new Identifier('StringArray');
56+
$value = new Node\Scalar\String_('StringClass');
57+
for ($i = 0; $i < $num; $i++) {
58+
$args[] = new Node\Arg($value, name: $name);
59+
}
60+
} else {
61+
$value = new Node\Scalar\String_('StringArray from StringClass');
62+
for ($i = 0; $i < $num; $i++) {
63+
$args[] = new Node\Arg($value);
64+
}
65+
}
66+
$attributeName = new FullyQualified(ImportType::class);
67+
$attribute = new Attribute($attributeName, $args);
68+
$node->attrGroups = array_merge($node->attrGroups, [new AttributeGroup([$attribute])]);
69+
}
70+
}

tests/TypeAttributeNodeVisitorTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ public function testAddsVarPHPDoc(): void
1919
$this->assertEquals("/**\n * @var string\n */", $docText);
2020
}
2121

22+
public function testAddsTypePHPDoc(): void
23+
{
24+
$node = new Node\Stmt\Class_('Test');
25+
$this->addTypeAttributeToNode($node);
26+
$this->nodeVisitor->enterNode($node);
27+
$docText = $this->getDocText($node);
28+
$this->assertEquals("/**\n * @type StringArray string[]\n */", $docText);
29+
}
30+
2231
public function testAddsReturnPHPDocWithTypeAttribute(): void
2332
{
2433
$node = new Node\Stmt\ClassMethod('Test');
@@ -28,9 +37,11 @@ public function testAddsReturnPHPDocWithTypeAttribute(): void
2837
$this->assertEquals("/**\n * @return string\n */", $docText);
2938
}
3039

31-
private function addTypeAttributeToNode(Node\Stmt\Property|Node\Stmt\ClassMethod $node): void
40+
private function addTypeAttributeToNode(Node\Stmt\Property|Node\Stmt\ClassMethod|Node\Stmt\Class_ $node): void
3241
{
3342
$args = [
43+
$node instanceof Node\Stmt\Class_ ?
44+
new Node\Arg(new Node\Scalar\String_('StringArray string[]')) :
3445
new Node\Arg(new Node\Scalar\String_('string'))
3546
];
3647
$attributeName = new FullyQualified(Type::class);

0 commit comments

Comments
 (0)