Skip to content

Commit b9e61a6

Browse files
VincentLangletondrejmirtes
authored andcommitted
Add support for @psalm-inheritors and @phpstan-sealed tag
1 parent 233238c commit b9e61a6

File tree

5 files changed

+136
-0
lines changed

5 files changed

+136
-0
lines changed

src/Ast/PhpDoc/PhpDocNode.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,17 @@ public function getRequireImplementsTagValues(string $tagName = '@phpstan-requir
231231
);
232232
}
233233

234+
/**
235+
* @return SealedTagValueNode[]
236+
*/
237+
public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array
238+
{
239+
return array_filter(
240+
array_column($this->getTagsByName($tagName), 'value'),
241+
static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode,
242+
);
243+
}
244+
234245
/**
235246
* @return DeprecatedTagValueNode[]
236247
*/

src/Ast/PhpDoc/SealedTagValueNode.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
7+
use function trim;
8+
9+
class SealedTagValueNode implements PhpDocTagValueNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
public TypeNode $type;
15+
16+
/** @var string (may be empty) */
17+
public string $description;
18+
19+
public function __construct(TypeNode $type, string $description)
20+
{
21+
$this->type = $type;
22+
$this->description = $description;
23+
}
24+
25+
26+
public function __toString(): string
27+
{
28+
return trim("{$this->type} {$this->description}");
29+
}
30+
31+
}

src/Parser/PhpDocParser.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
403403
$tagValue = $this->parseRequireImplementsTagValue($tokens);
404404
break;
405405

406+
case '@psalm-inheritors':
407+
case '@phpstan-sealed':
408+
$tagValue = $this->parseSealedTagValue($tokens);
409+
break;
410+
406411
case '@deprecated':
407412
$tagValue = $this->parseDeprecatedTagValue($tokens);
408413
break;
@@ -933,6 +938,13 @@ private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpD
933938
return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
934939
}
935940

941+
private function parseSealedTagValue(TokenIterator $tokens): Ast\PhpDoc\SealedTagValueNode
942+
{
943+
$type = $this->typeParser->parse($tokens);
944+
$description = $this->parseOptionalDescription($tokens, true);
945+
return new Ast\PhpDoc\SealedTagValueNode($type, $description);
946+
}
947+
936948
private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
937949
{
938950
$description = $this->parseOptionalDescription($tokens, false);

src/Printer/Printer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
3737
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
3838
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
39+
use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode;
3940
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
4041
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
4142
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
@@ -327,6 +328,10 @@ private function printTagValue(PhpDocTagValueNode $node): string
327328
$type = $this->printType($node->type);
328329
return trim("{$type} {$node->description}");
329330
}
331+
if ($node instanceof SealedTagValueNode) {
332+
$type = $this->printType($node->type);
333+
return trim("{$type} {$node->description}");
334+
}
330335
if ($node instanceof ParamOutTagValueNode) {
331336
$type = $this->printType($node->type);
332337
return trim("{$type} {$node->parameterName} {$node->description}");

tests/PHPStan/Parser/PhpDocParserTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
4343
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
4444
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
45+
use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode;
4546
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
4647
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
4748
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
@@ -108,6 +109,7 @@ protected function setUp(): void
108109
* @dataProvider provideMixinTagsData
109110
* @dataProvider provideRequireExtendsTagsData
110111
* @dataProvider provideRequireImplementsTagsData
112+
* @dataProvider provideSealedTagsData
111113
* @dataProvider provideDeprecatedTagsData
112114
* @dataProvider providePropertyTagsData
113115
* @dataProvider provideMethodTagsData
@@ -2210,6 +2212,81 @@ public function provideRequireImplementsTagsData(): Iterator
22102212
];
22112213
}
22122214

2215+
public function provideSealedTagsData(): Iterator
2216+
{
2217+
yield [
2218+
'OK without description',
2219+
'/** @phpstan-sealed Foo|Bar */',
2220+
new PhpDocNode([
2221+
new PhpDocTagNode(
2222+
'@phpstan-sealed',
2223+
new SealedTagValueNode(
2224+
new UnionTypeNode([
2225+
new IdentifierTypeNode('Foo'),
2226+
new IdentifierTypeNode('Bar'),
2227+
]),
2228+
'',
2229+
),
2230+
),
2231+
]),
2232+
];
2233+
2234+
yield [
2235+
'OK with description',
2236+
'/** @phpstan-sealed Foo|Bar optional description */',
2237+
new PhpDocNode([
2238+
new PhpDocTagNode(
2239+
'@phpstan-sealed',
2240+
new SealedTagValueNode(
2241+
new UnionTypeNode([
2242+
new IdentifierTypeNode('Foo'),
2243+
new IdentifierTypeNode('Bar'),
2244+
]),
2245+
'optional description',
2246+
),
2247+
),
2248+
]),
2249+
];
2250+
2251+
yield [
2252+
'OK with psalm-prefix description',
2253+
'/** @psalm-inheritors Foo|Bar optional description */',
2254+
new PhpDocNode([
2255+
new PhpDocTagNode(
2256+
'@psalm-inheritors',
2257+
new SealedTagValueNode(
2258+
new UnionTypeNode([
2259+
new IdentifierTypeNode('Foo'),
2260+
new IdentifierTypeNode('Bar'),
2261+
]),
2262+
'optional description',
2263+
),
2264+
),
2265+
]),
2266+
];
2267+
2268+
yield [
2269+
'invalid without type and description',
2270+
'/** @phpstan-sealed */',
2271+
new PhpDocNode([
2272+
new PhpDocTagNode(
2273+
'@phpstan-sealed',
2274+
new InvalidTagValueNode(
2275+
'',
2276+
new ParserException(
2277+
'*/',
2278+
Lexer::TOKEN_CLOSE_PHPDOC,
2279+
20,
2280+
Lexer::TOKEN_IDENTIFIER,
2281+
null,
2282+
1,
2283+
),
2284+
),
2285+
),
2286+
]),
2287+
];
2288+
}
2289+
22132290
public function provideDeprecatedTagsData(): Iterator
22142291
{
22152292
yield [

0 commit comments

Comments
 (0)