Skip to content

Commit bd84b62

Browse files
authored
Added support for @phpstan-require-extends and @phpstan-require-implements PHPDoc tags
1 parent 77db537 commit bd84b62

File tree

7 files changed

+270
-0
lines changed

7 files changed

+270
-0
lines changed

src/Ast/PhpDoc/PhpDocNode.php

+25
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,31 @@ static function (PhpDocTagValueNode $value): bool {
187187
);
188188
}
189189

190+
/**
191+
* @return RequireExtendsTagValueNode[]
192+
*/
193+
public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array
194+
{
195+
return array_filter(
196+
array_column($this->getTagsByName($tagName), 'value'),
197+
static function (PhpDocTagValueNode $value): bool {
198+
return $value instanceof RequireExtendsTagValueNode;
199+
}
200+
);
201+
}
202+
203+
/**
204+
* @return RequireImplementsTagValueNode[]
205+
*/
206+
public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array
207+
{
208+
return array_filter(
209+
array_column($this->getTagsByName($tagName), 'value'),
210+
static function (PhpDocTagValueNode $value): bool {
211+
return $value instanceof RequireImplementsTagValueNode;
212+
}
213+
);
214+
}
190215

191216
/**
192217
* @return DeprecatedTagValueNode[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 RequireExtendsTagValueNode implements PhpDocTagValueNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var TypeNode */
15+
public $type;
16+
17+
/** @var string (may be empty) */
18+
public $description;
19+
20+
public function __construct(TypeNode $type, string $description)
21+
{
22+
$this->type = $type;
23+
$this->description = $description;
24+
}
25+
26+
27+
public function __toString(): string
28+
{
29+
return trim("{$this->type} {$this->description}");
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 RequireImplementsTagValueNode implements PhpDocTagValueNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var TypeNode */
15+
public $type;
16+
17+
/** @var string (may be empty) */
18+
public $description;
19+
20+
public function __construct(TypeNode $type, string $description)
21+
{
22+
$this->type = $type;
23+
$this->description = $description;
24+
}
25+
26+
27+
public function __toString(): string
28+
{
29+
return trim("{$this->type} {$this->description}");
30+
}
31+
32+
}

src/Parser/PhpDocParser.php

+24
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
408408
$tagValue = $this->parseMixinTagValue($tokens);
409409
break;
410410

411+
case '@psalm-require-extends':
412+
case '@phpstan-require-extends':
413+
$tagValue = $this->parseRequireExtendsTagValue($tokens);
414+
break;
415+
416+
case '@psalm-require-implements':
417+
case '@phpstan-require-implements':
418+
$tagValue = $this->parseRequireImplementsTagValue($tokens);
419+
break;
420+
411421
case '@deprecated':
412422
$tagValue = $this->parseDeprecatedTagValue($tokens);
413423
break;
@@ -877,6 +887,20 @@ private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagV
877887
return new Ast\PhpDoc\MixinTagValueNode($type, $description);
878888
}
879889

890+
private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
891+
{
892+
$type = $this->typeParser->parse($tokens);
893+
$description = $this->parseOptionalDescription($tokens, true);
894+
return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
895+
}
896+
897+
private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
898+
{
899+
$type = $this->typeParser->parse($tokens);
900+
$description = $this->parseOptionalDescription($tokens, true);
901+
return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
902+
}
903+
880904
private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
881905
{
882906
$description = $this->parseOptionalDescription($tokens);

src/Printer/Printer.php

+10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
2929
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
3030
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
31+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
32+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
3133
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
3234
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
3335
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
@@ -283,6 +285,14 @@ private function printTagValue(PhpDocTagValueNode $node): string
283285
$type = $this->printType($node->type);
284286
return trim("{$type} {$node->description}");
285287
}
288+
if ($node instanceof RequireExtendsTagValueNode) {
289+
$type = $this->printType($node->type);
290+
return trim("{$type} {$node->description}");
291+
}
292+
if ($node instanceof RequireImplementsTagValueNode) {
293+
$type = $this->printType($node->type);
294+
return trim("{$type} {$node->description}");
295+
}
286296
if ($node instanceof ParamOutTagValueNode) {
287297
$type = $this->printType($node->type);
288298
return trim("{$type} {$node->parameterName} {$node->description}");

tests/PHPStan/Ast/ToString/PhpDocToStringTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
2929
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
3030
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
31+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
32+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
3133
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
3234
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
3335
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
@@ -213,6 +215,15 @@ public static function provideClassCases(): Generator
213215
['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
214216
];
215217

218+
yield from [
219+
['PHPUnit\\TestCase', new RequireExtendsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')],
220+
['Foo\\Bar Baz', new RequireExtendsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
221+
];
222+
yield from [
223+
['PHPUnit\\TestCase', new RequireImplementsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')],
224+
['Foo\\Bar Baz', new RequireImplementsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
225+
];
226+
216227
yield from [
217228
['Foo array<string>', new TypeAliasTagValueNode('Foo', $arrayOfStrings)],
218229
['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)],

tests/PHPStan/Parser/PhpDocParserTest.php

+136
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
3636
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
3737
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
38+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
39+
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
3840
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
3941
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
4042
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
@@ -100,6 +102,8 @@ protected function setUp(): void
100102
* @dataProvider provideReturnTagsData
101103
* @dataProvider provideThrowsTagsData
102104
* @dataProvider provideMixinTagsData
105+
* @dataProvider provideRequireExtendsTagsData
106+
* @dataProvider provideRequireImplementsTagsData
103107
* @dataProvider provideDeprecatedTagsData
104108
* @dataProvider providePropertyTagsData
105109
* @dataProvider provideMethodTagsData
@@ -1908,6 +1912,138 @@ public function provideMixinTagsData(): Iterator
19081912
];
19091913
}
19101914

1915+
public function provideRequireExtendsTagsData(): Iterator
1916+
{
1917+
yield [
1918+
'OK without description',
1919+
'/** @phpstan-require-extends Foo */',
1920+
new PhpDocNode([
1921+
new PhpDocTagNode(
1922+
'@phpstan-require-extends',
1923+
new RequireExtendsTagValueNode(
1924+
new IdentifierTypeNode('Foo'),
1925+
''
1926+
)
1927+
),
1928+
]),
1929+
];
1930+
1931+
yield [
1932+
'OK with description',
1933+
'/** @phpstan-require-extends Foo optional description */',
1934+
new PhpDocNode([
1935+
new PhpDocTagNode(
1936+
'@phpstan-require-extends',
1937+
new RequireExtendsTagValueNode(
1938+
new IdentifierTypeNode('Foo'),
1939+
'optional description'
1940+
)
1941+
),
1942+
]),
1943+
];
1944+
1945+
yield [
1946+
'OK with psalm-prefix description',
1947+
'/** @psalm-require-extends Foo optional description */',
1948+
new PhpDocNode([
1949+
new PhpDocTagNode(
1950+
'@psalm-require-extends',
1951+
new RequireExtendsTagValueNode(
1952+
new IdentifierTypeNode('Foo'),
1953+
'optional description'
1954+
)
1955+
),
1956+
]),
1957+
];
1958+
1959+
yield [
1960+
'invalid without type and description',
1961+
'/** @phpstan-require-extends */',
1962+
new PhpDocNode([
1963+
new PhpDocTagNode(
1964+
'@phpstan-require-extends',
1965+
new InvalidTagValueNode(
1966+
'',
1967+
new ParserException(
1968+
'*/',
1969+
Lexer::TOKEN_CLOSE_PHPDOC,
1970+
29,
1971+
Lexer::TOKEN_IDENTIFIER,
1972+
null,
1973+
1
1974+
)
1975+
)
1976+
),
1977+
]),
1978+
];
1979+
}
1980+
1981+
public function provideRequireImplementsTagsData(): Iterator
1982+
{
1983+
yield [
1984+
'OK without description',
1985+
'/** @phpstan-require-implements Foo */',
1986+
new PhpDocNode([
1987+
new PhpDocTagNode(
1988+
'@phpstan-require-implements',
1989+
new RequireImplementsTagValueNode(
1990+
new IdentifierTypeNode('Foo'),
1991+
''
1992+
)
1993+
),
1994+
]),
1995+
];
1996+
1997+
yield [
1998+
'OK with description',
1999+
'/** @phpstan-require-implements Foo optional description */',
2000+
new PhpDocNode([
2001+
new PhpDocTagNode(
2002+
'@phpstan-require-implements',
2003+
new RequireImplementsTagValueNode(
2004+
new IdentifierTypeNode('Foo'),
2005+
'optional description'
2006+
)
2007+
),
2008+
]),
2009+
];
2010+
2011+
yield [
2012+
'OK with psalm-prefix description',
2013+
'/** @psalm-require-implements Foo optional description */',
2014+
new PhpDocNode([
2015+
new PhpDocTagNode(
2016+
'@psalm-require-implements',
2017+
new RequireImplementsTagValueNode(
2018+
new IdentifierTypeNode('Foo'),
2019+
'optional description'
2020+
)
2021+
),
2022+
]),
2023+
];
2024+
2025+
yield [
2026+
'invalid without type and description',
2027+
'/** @phpstan-require-implements */',
2028+
new PhpDocNode([
2029+
new PhpDocTagNode(
2030+
'@phpstan-require-implements',
2031+
new InvalidTagValueNode(
2032+
'',
2033+
new ParserException(
2034+
'*/',
2035+
Lexer::TOKEN_CLOSE_PHPDOC,
2036+
32,
2037+
Lexer::TOKEN_IDENTIFIER,
2038+
null,
2039+
1
2040+
)
2041+
)
2042+
),
2043+
]),
2044+
];
2045+
}
2046+
19112047
public function provideDeprecatedTagsData(): Iterator
19122048
{
19132049
yield [

0 commit comments

Comments
 (0)