Skip to content

Commit

Permalink
Support properties/methods in assert annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen authored Aug 9, 2022
1 parent 7daca1d commit af74624
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 10 deletions.
45 changes: 45 additions & 0 deletions src/Ast/PhpDoc/AssertTagMethodValueNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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 AssertTagMethodValueNode implements PhpDocTagValueNode
{

use NodeAttributes;

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

/** @var string */
public $parameter;

/** @var string */
public $method;

/** @var bool */
public $isNegated;

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

public function __construct(TypeNode $type, string $parameter, string $method, bool $isNegated, string $description)
{
$this->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}");
}

}
45 changes: 45 additions & 0 deletions src/Ast/PhpDoc/AssertTagPropertyValueNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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 AssertTagPropertyValueNode implements PhpDocTagValueNode
{

use NodeAttributes;

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

/** @var string */
public $parameter;

/** @var string */
public $property;

/** @var bool */
public $isNegated;

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

public function __construct(TypeNode $type, string $parameter, string $property, bool $isNegated, string $description)
{
$this->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}");
}

}
28 changes: 28 additions & 0 deletions src/Ast/PhpDoc/PhpDocNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 => '\'&\'',
Expand All @@ -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 => '\'*/\'',
Expand Down Expand Up @@ -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 => ':',

Expand Down
50 changes: 47 additions & 3 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
84 changes: 77 additions & 7 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */',
Expand Down

0 comments on commit af74624

Please sign in to comment.