Skip to content

Commit af74624

Browse files
authored
Support properties/methods in assert annotations
1 parent 7daca1d commit af74624

File tree

6 files changed

+245
-10
lines changed

6 files changed

+245
-10
lines changed

Diff for: src/Ast/PhpDoc/AssertTagMethodValueNode.php

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 AssertTagMethodValueNode implements PhpDocTagValueNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var TypeNode */
15+
public $type;
16+
17+
/** @var string */
18+
public $parameter;
19+
20+
/** @var string */
21+
public $method;
22+
23+
/** @var bool */
24+
public $isNegated;
25+
26+
/** @var string (may be empty) */
27+
public $description;
28+
29+
public function __construct(TypeNode $type, string $parameter, string $method, bool $isNegated, string $description)
30+
{
31+
$this->type = $type;
32+
$this->parameter = $parameter;
33+
$this->method = $method;
34+
$this->isNegated = $isNegated;
35+
$this->description = $description;
36+
}
37+
38+
39+
public function __toString(): string
40+
{
41+
$isNegated = $this->isNegated ? '!' : '';
42+
return trim("{$this->type} {$isNegated}{$this->parameter}->{$this->method}() {$this->description}");
43+
}
44+
45+
}

Diff for: src/Ast/PhpDoc/AssertTagPropertyValueNode.php

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 AssertTagPropertyValueNode implements PhpDocTagValueNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var TypeNode */
15+
public $type;
16+
17+
/** @var string */
18+
public $parameter;
19+
20+
/** @var string */
21+
public $property;
22+
23+
/** @var bool */
24+
public $isNegated;
25+
26+
/** @var string (may be empty) */
27+
public $description;
28+
29+
public function __construct(TypeNode $type, string $parameter, string $property, bool $isNegated, string $description)
30+
{
31+
$this->type = $type;
32+
$this->parameter = $parameter;
33+
$this->property = $property;
34+
$this->isNegated = $isNegated;
35+
$this->description = $description;
36+
}
37+
38+
39+
public function __toString(): string
40+
{
41+
$isNegated = $this->isNegated ? '!' : '';
42+
return trim("{$this->type} {$isNegated}{$this->parameter}->{$this->property} {$this->description}");
43+
}
44+
45+
}

Diff for: src/Ast/PhpDoc/PhpDocNode.php

+28
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,34 @@ static function (PhpDocTagValueNode $value): bool {
300300
}
301301

302302

303+
/**
304+
* @return AssertTagPropertyValueNode[]
305+
*/
306+
public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array
307+
{
308+
return array_filter(
309+
array_column($this->getTagsByName($tagName), 'value'),
310+
static function (PhpDocTagValueNode $value): bool {
311+
return $value instanceof AssertTagPropertyValueNode;
312+
}
313+
);
314+
}
315+
316+
317+
/**
318+
* @return AssertTagMethodValueNode[]
319+
*/
320+
public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array
321+
{
322+
return array_filter(
323+
array_column($this->getTagsByName($tagName), 'value'),
324+
static function (PhpDocTagValueNode $value): bool {
325+
return $value instanceof AssertTagMethodValueNode;
326+
}
327+
);
328+
}
329+
330+
303331
public function __toString(): string
304332
{
305333
$children = array_map(

Diff for: src/Lexer/Lexer.php

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Lexer
4646
public const TOKEN_OPEN_CURLY_BRACKET = 31;
4747
public const TOKEN_CLOSE_CURLY_BRACKET = 32;
4848
public const TOKEN_NEGATED = 33;
49+
public const TOKEN_ARROW = 34;
4950

5051
public const TOKEN_LABELS = [
5152
self::TOKEN_REFERENCE => '\'&\'',
@@ -66,6 +67,7 @@ class Lexer
6667
self::TOKEN_VARIADIC => '\'...\'',
6768
self::TOKEN_DOUBLE_COLON => '\'::\'',
6869
self::TOKEN_DOUBLE_ARROW => '\'=>\'',
70+
self::TOKEN_ARROW => '\'->\'',
6971
self::TOKEN_EQUAL => '\'=\'',
7072
self::TOKEN_OPEN_PHPDOC => '\'/**\'',
7173
self::TOKEN_CLOSE_PHPDOC => '\'*/\'',
@@ -138,6 +140,7 @@ private function generateRegexp(): string
138140
self::TOKEN_VARIADIC => '\\.\\.\\.',
139141
self::TOKEN_DOUBLE_COLON => '::',
140142
self::TOKEN_DOUBLE_ARROW => '=>',
143+
self::TOKEN_ARROW => '->',
141144
self::TOKEN_EQUAL => '=',
142145
self::TOKEN_COLON => ':',
143146

Diff for: src/Parser/PhpDocParser.php

+47-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
77
use PHPStan\PhpDocParser\Lexer\Lexer;
88
use PHPStan\ShouldNotHappenException;
9+
use function array_key_exists;
910
use function array_values;
1011
use function count;
1112
use function trim;
@@ -446,13 +447,56 @@ private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc
446447
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs);
447448
}
448449

449-
private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\AssertTagValueNode
450+
/**
451+
* @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
452+
*/
453+
private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
450454
{
451455
$isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
452456
$type = $this->typeParser->parse($tokens);
453-
$parameter = $this->parseRequiredVariableName($tokens);
457+
$parameter = $this->parseAssertParameter($tokens);
454458
$description = $this->parseOptionalDescription($tokens);
455-
return new Ast\PhpDoc\AssertTagValueNode($type, $parameter, $isNegated, $description);
459+
460+
if (array_key_exists('method', $parameter)) {
461+
return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description);
462+
} elseif (array_key_exists('property', $parameter)) {
463+
return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description);
464+
}
465+
466+
return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description);
467+
}
468+
469+
/**
470+
* @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
471+
*/
472+
private function parseAssertParameter(TokenIterator $tokens): array
473+
{
474+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
475+
$parameter = '$this';
476+
$requirePropertyOrMethod = true;
477+
$tokens->next();
478+
} else {
479+
$parameter = $tokens->currentTokenValue();
480+
$requirePropertyOrMethod = false;
481+
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
482+
}
483+
484+
if ($requirePropertyOrMethod || $tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
485+
$tokens->consumeTokenType(Lexer::TOKEN_ARROW);
486+
487+
$propertyOrMethod = $tokens->currentTokenValue();
488+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
489+
490+
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
491+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
492+
493+
return ['parameter' => $parameter, 'method' => $propertyOrMethod];
494+
}
495+
496+
return ['parameter' => $parameter, 'property' => $propertyOrMethod];
497+
}
498+
499+
return ['parameter' => $parameter];
456500
}
457501

458502
private function parseOptionalVariableName(TokenIterator $tokens): string

Diff for: tests/PHPStan/Parser/PhpDocParserTest.php

+77-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
99
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
1010
use PHPStan\PhpDocParser\Ast\Node;
11+
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode;
12+
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode;
1113
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
1214
use PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode;
1315
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
@@ -3812,24 +3814,92 @@ public function provideAssertTagsData(): Iterator
38123814
];
38133815

38143816
yield [
3815-
'invalid $this->method()',
3816-
'/** @phpstan-assert Type $this->method() */',
3817+
'OK $var->method()',
3818+
'/** @phpstan-assert Type $var->method() */',
3819+
new PhpDocNode([
3820+
new PhpDocTagNode(
3821+
'@phpstan-assert',
3822+
new AssertTagMethodValueNode(
3823+
new IdentifierTypeNode('Type'),
3824+
'$var',
3825+
'method',
3826+
false,
3827+
''
3828+
)
3829+
),
3830+
]),
3831+
];
3832+
3833+
yield [
3834+
'OK $var->property',
3835+
'/** @phpstan-assert Type $var->property */',
3836+
new PhpDocNode([
3837+
new PhpDocTagNode(
3838+
'@phpstan-assert',
3839+
new AssertTagPropertyValueNode(
3840+
new IdentifierTypeNode('Type'),
3841+
'$var',
3842+
'property',
3843+
false,
3844+
''
3845+
)
3846+
),
3847+
]),
3848+
];
3849+
3850+
yield [
3851+
'invalid $this',
3852+
'/** @phpstan-assert Type $this */',
38173853
new PhpDocNode([
38183854
new PhpDocTagNode(
38193855
'@phpstan-assert',
38203856
new InvalidTagValueNode(
3821-
'Type $this->method()',
3857+
'Type $this',
38223858
new ParserException(
3823-
'$this',
3824-
Lexer::TOKEN_THIS_VARIABLE,
3825-
25,
3826-
Lexer::TOKEN_VARIABLE
3859+
'*/',
3860+
Lexer::TOKEN_CLOSE_PHPDOC,
3861+
31,
3862+
Lexer::TOKEN_ARROW
38273863
)
38283864
)
38293865
),
38303866
]),
38313867
];
38323868

3869+
yield [
3870+
'OK $this->method()',
3871+
'/** @phpstan-assert Type $this->method() */',
3872+
new PhpDocNode([
3873+
new PhpDocTagNode(
3874+
'@phpstan-assert',
3875+
new AssertTagMethodValueNode(
3876+
new IdentifierTypeNode('Type'),
3877+
'$this',
3878+
'method',
3879+
false,
3880+
''
3881+
)
3882+
),
3883+
]),
3884+
];
3885+
3886+
yield [
3887+
'OK $this->property',
3888+
'/** @phpstan-assert Type $this->property */',
3889+
new PhpDocNode([
3890+
new PhpDocTagNode(
3891+
'@phpstan-assert',
3892+
new AssertTagPropertyValueNode(
3893+
new IdentifierTypeNode('Type'),
3894+
'$this',
3895+
'property',
3896+
false,
3897+
''
3898+
)
3899+
),
3900+
]),
3901+
];
3902+
38333903
yield [
38343904
'OK assert-if-true',
38353905
'/** @phpstan-assert-if-true Type $var */',

0 commit comments

Comments
 (0)