Skip to content

Commit

Permalink
Added support for @phpstan-require-extends and `@phpstan-require-im…
Browse files Browse the repository at this point in the history
…plements` PHPDoc tags
  • Loading branch information
staabm authored Jan 4, 2024
1 parent 77db537 commit bd84b62
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/Ast/PhpDoc/PhpDocNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,31 @@ static function (PhpDocTagValueNode $value): bool {
);
}

/**
* @return RequireExtendsTagValueNode[]
*/
public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof RequireExtendsTagValueNode;
}
);
}

/**
* @return RequireImplementsTagValueNode[]
*/
public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof RequireImplementsTagValueNode;
}
);
}

/**
* @return DeprecatedTagValueNode[]
Expand Down
32 changes: 32 additions & 0 deletions src/Ast/PhpDoc/RequireExtendsTagValueNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class RequireExtendsTagValueNode implements PhpDocTagValueNode
{

use NodeAttributes;

/** @var TypeNode */
public $type;

/** @var string (may be empty) */
public $description;

public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}


public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}

}
32 changes: 32 additions & 0 deletions src/Ast/PhpDoc/RequireImplementsTagValueNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class RequireImplementsTagValueNode implements PhpDocTagValueNode
{

use NodeAttributes;

/** @var TypeNode */
public $type;

/** @var string (may be empty) */
public $description;

public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}


public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}

}
24 changes: 24 additions & 0 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
$tagValue = $this->parseMixinTagValue($tokens);
break;

case '@psalm-require-extends':
case '@phpstan-require-extends':
$tagValue = $this->parseRequireExtendsTagValue($tokens);
break;

case '@psalm-require-implements':
case '@phpstan-require-implements':
$tagValue = $this->parseRequireImplementsTagValue($tokens);
break;

case '@deprecated':
$tagValue = $this->parseDeprecatedTagValue($tokens);
break;
Expand Down Expand Up @@ -877,6 +887,20 @@ private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagV
return new Ast\PhpDoc\MixinTagValueNode($type, $description);
}

private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
}

private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
}

private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
{
$description = $this->parseOptionalDescription($tokens);
Expand Down
10 changes: 10 additions & 0 deletions src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
Expand Down Expand Up @@ -283,6 +285,14 @@ private function printTagValue(PhpDocTagValueNode $node): string
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireExtendsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireImplementsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof ParamOutTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->parameterName} {$node->description}");
Expand Down
11 changes: 11 additions & 0 deletions tests/PHPStan/Ast/ToString/PhpDocToStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
Expand Down Expand Up @@ -213,6 +215,15 @@ public static function provideClassCases(): Generator
['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
];

yield from [
['PHPUnit\\TestCase', new RequireExtendsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')],
['Foo\\Bar Baz', new RequireExtendsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
];
yield from [
['PHPUnit\\TestCase', new RequireImplementsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')],
['Foo\\Bar Baz', new RequireImplementsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')],
];

yield from [
['Foo array<string>', new TypeAliasTagValueNode('Foo', $arrayOfStrings)],
['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)],
Expand Down
136 changes: 136 additions & 0 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
Expand Down Expand Up @@ -100,6 +102,8 @@ protected function setUp(): void
* @dataProvider provideReturnTagsData
* @dataProvider provideThrowsTagsData
* @dataProvider provideMixinTagsData
* @dataProvider provideRequireExtendsTagsData
* @dataProvider provideRequireImplementsTagsData
* @dataProvider provideDeprecatedTagsData
* @dataProvider providePropertyTagsData
* @dataProvider provideMethodTagsData
Expand Down Expand Up @@ -1908,6 +1912,138 @@ public function provideMixinTagsData(): Iterator
];
}

public function provideRequireExtendsTagsData(): Iterator
{
yield [
'OK without description',
'/** @phpstan-require-extends Foo */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-extends',
new RequireExtendsTagValueNode(
new IdentifierTypeNode('Foo'),
''
)
),
]),
];

yield [
'OK with description',
'/** @phpstan-require-extends Foo optional description */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-extends',
new RequireExtendsTagValueNode(
new IdentifierTypeNode('Foo'),
'optional description'
)
),
]),
];

yield [
'OK with psalm-prefix description',
'/** @psalm-require-extends Foo optional description */',
new PhpDocNode([
new PhpDocTagNode(
'@psalm-require-extends',
new RequireExtendsTagValueNode(
new IdentifierTypeNode('Foo'),
'optional description'
)
),
]),
];

yield [
'invalid without type and description',
'/** @phpstan-require-extends */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-extends',
new InvalidTagValueNode(
'',
new ParserException(
'*/',
Lexer::TOKEN_CLOSE_PHPDOC,
29,
Lexer::TOKEN_IDENTIFIER,
null,
1
)
)
),
]),
];
}

public function provideRequireImplementsTagsData(): Iterator
{
yield [
'OK without description',
'/** @phpstan-require-implements Foo */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-implements',
new RequireImplementsTagValueNode(
new IdentifierTypeNode('Foo'),
''
)
),
]),
];

yield [
'OK with description',
'/** @phpstan-require-implements Foo optional description */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-implements',
new RequireImplementsTagValueNode(
new IdentifierTypeNode('Foo'),
'optional description'
)
),
]),
];

yield [
'OK with psalm-prefix description',
'/** @psalm-require-implements Foo optional description */',
new PhpDocNode([
new PhpDocTagNode(
'@psalm-require-implements',
new RequireImplementsTagValueNode(
new IdentifierTypeNode('Foo'),
'optional description'
)
),
]),
];

yield [
'invalid without type and description',
'/** @phpstan-require-implements */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-require-implements',
new InvalidTagValueNode(
'',
new ParserException(
'*/',
Lexer::TOKEN_CLOSE_PHPDOC,
32,
Lexer::TOKEN_IDENTIFIER,
null,
1
)
)
),
]),
];
}

public function provideDeprecatedTagsData(): Iterator
{
yield [
Expand Down

0 comments on commit bd84b62

Please sign in to comment.