diff --git a/src/Ast/PhpDoc/AssertTagMethodValueNode.php b/src/Ast/PhpDoc/AssertTagMethodValueNode.php new file mode 100644 index 00000000..b961d311 --- /dev/null +++ b/src/Ast/PhpDoc/AssertTagMethodValueNode.php @@ -0,0 +1,45 @@ +type = $type; + $this->parameter = $parameter; + $this->method = $method; + $this->isNegated = $isNegated; + $this->description = $description; + } + + + public function __toString(): string + { + $isNegated = $this->isNegated ? '!' : ''; + return trim("{$this->type} {$isNegated}{$this->parameter}->{$this->method}() {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/AssertTagPropertyValueNode.php b/src/Ast/PhpDoc/AssertTagPropertyValueNode.php new file mode 100644 index 00000000..9aacb081 --- /dev/null +++ b/src/Ast/PhpDoc/AssertTagPropertyValueNode.php @@ -0,0 +1,45 @@ +type = $type; + $this->parameter = $parameter; + $this->property = $property; + $this->isNegated = $isNegated; + $this->description = $description; + } + + + public function __toString(): string + { + $isNegated = $this->isNegated ? '!' : ''; + return trim("{$this->type} {$isNegated}{$this->parameter}->{$this->property} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index a0caef68..ac06e19f 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -300,6 +300,34 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return AssertTagPropertyValueNode[] + */ + public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof AssertTagPropertyValueNode; + } + ); + } + + + /** + * @return AssertTagMethodValueNode[] + */ + public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof AssertTagMethodValueNode; + } + ); + } + + public function __toString(): string { $children = array_map( diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index d05f3f9f..1d77efe6 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -46,6 +46,7 @@ class Lexer public const TOKEN_OPEN_CURLY_BRACKET = 31; public const TOKEN_CLOSE_CURLY_BRACKET = 32; public const TOKEN_NEGATED = 33; + public const TOKEN_ARROW = 34; public const TOKEN_LABELS = [ self::TOKEN_REFERENCE => '\'&\'', @@ -66,6 +67,7 @@ class Lexer self::TOKEN_VARIADIC => '\'...\'', self::TOKEN_DOUBLE_COLON => '\'::\'', self::TOKEN_DOUBLE_ARROW => '\'=>\'', + self::TOKEN_ARROW => '\'->\'', self::TOKEN_EQUAL => '\'=\'', self::TOKEN_OPEN_PHPDOC => '\'/**\'', self::TOKEN_CLOSE_PHPDOC => '\'*/\'', @@ -138,6 +140,7 @@ private function generateRegexp(): string self::TOKEN_VARIADIC => '\\.\\.\\.', self::TOKEN_DOUBLE_COLON => '::', self::TOKEN_DOUBLE_ARROW => '=>', + self::TOKEN_ARROW => '->', self::TOKEN_EQUAL => '=', self::TOKEN_COLON => ':', diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index ee7848c1..7c5284d1 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\ShouldNotHappenException; +use function array_key_exists; use function array_values; use function count; use function trim; @@ -446,13 +447,56 @@ private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs); } - private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\AssertTagValueNode + /** + * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode + */ + private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { $isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED); $type = $this->typeParser->parse($tokens); - $parameter = $this->parseRequiredVariableName($tokens); + $parameter = $this->parseAssertParameter($tokens); $description = $this->parseOptionalDescription($tokens); - return new Ast\PhpDoc\AssertTagValueNode($type, $parameter, $isNegated, $description); + + if (array_key_exists('method', $parameter)) { + return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description); + } elseif (array_key_exists('property', $parameter)) { + return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description); + } + + return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description); + } + + /** + * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string} + */ + private function parseAssertParameter(TokenIterator $tokens): array + { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { + $parameter = '$this'; + $requirePropertyOrMethod = true; + $tokens->next(); + } else { + $parameter = $tokens->currentTokenValue(); + $requirePropertyOrMethod = false; + $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); + } + + if ($requirePropertyOrMethod || $tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) { + $tokens->consumeTokenType(Lexer::TOKEN_ARROW); + + $propertyOrMethod = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); + + return ['parameter' => $parameter, 'method' => $propertyOrMethod]; + } + + return ['parameter' => $parameter, 'property' => $propertyOrMethod]; + } + + return ['parameter' => $parameter]; } private function parseOptionalVariableName(TokenIterator $tokens): string diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index ccf71651..fa9c06fb 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -8,6 +8,8 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Node; +use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; @@ -3812,24 +3814,92 @@ public function provideAssertTagsData(): Iterator ]; yield [ - 'invalid $this->method()', - '/** @phpstan-assert Type $this->method() */', + 'OK $var->method()', + '/** @phpstan-assert Type $var->method() */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagMethodValueNode( + new IdentifierTypeNode('Type'), + '$var', + 'method', + false, + '' + ) + ), + ]), + ]; + + yield [ + 'OK $var->property', + '/** @phpstan-assert Type $var->property */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagPropertyValueNode( + new IdentifierTypeNode('Type'), + '$var', + 'property', + false, + '' + ) + ), + ]), + ]; + + yield [ + 'invalid $this', + '/** @phpstan-assert Type $this */', new PhpDocNode([ new PhpDocTagNode( '@phpstan-assert', new InvalidTagValueNode( - 'Type $this->method()', + 'Type $this', new ParserException( - '$this', - Lexer::TOKEN_THIS_VARIABLE, - 25, - Lexer::TOKEN_VARIABLE + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 31, + Lexer::TOKEN_ARROW ) ) ), ]), ]; + yield [ + 'OK $this->method()', + '/** @phpstan-assert Type $this->method() */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagMethodValueNode( + new IdentifierTypeNode('Type'), + '$this', + 'method', + false, + '' + ) + ), + ]), + ]; + + yield [ + 'OK $this->property', + '/** @phpstan-assert Type $this->property */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagPropertyValueNode( + new IdentifierTypeNode('Type'), + '$this', + 'property', + false, + '' + ) + ), + ]), + ]; + yield [ 'OK assert-if-true', '/** @phpstan-assert-if-true Type $var */',