Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/grammars/common.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ Wildcard
| <T_WILDCARD_WS>
;

/**
* An asterisk written right after a name, with no space in between, whether or
* not whitespace follows it.
*/
NameWildcard
: <T_NAME_WILDCARD>
| <T_NAME_WILDCARD_WS>
;

/**
* A "(" whether or not whitespace precedes it.
*/
Expand Down
34 changes: 27 additions & 7 deletions doc/grammars/const-expr.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,24 @@ ConstantArrayItem
* - Foo::BAR
* - Foo::BAR_*
* - BAR
* - BAR_*
*/
ConstantFetch
: !ArrayKeyword() Identifier() <T_DOUBLE_COLON> ClassConstantName()
| !ArrayKeyword() Identifier()
| !ArrayKeyword() Identifier() GlobalConstantNameAfterIdentifier()?
;

/**
* The rest of the name of a global constant written with asterisks standing
* for any part of it, like the "*_ERROR" of:
* - E_*_ERROR
*
* The first asterisk has to follow the name with no space in between, so that
* "FOO *" stays the constant "FOO" followed by an asterisk.
*/
GlobalConstantNameAfterIdentifier
: <T_NAME_WILDCARD_WS>
| <T_NAME_WILDCARD> ConstantNameAfterWildcard()?
;

/**
Expand All @@ -104,15 +118,21 @@ ConstantFetch
*/
ClassConstantName
: <T_WILDCARD_WS>
| <T_WILDCARD> ClassConstantNameAfterWildcard()?
| Identifier() ClassConstantNameAfterIdentifier()?
| <T_WILDCARD> ConstantNameAfterWildcard()?
| Identifier() ConstantNameAfterIdentifier()?
;

ClassConstantNameAfterWildcard
: Identifier() ClassConstantNameAfterIdentifier()?
ConstantNameAfterWildcard
: Identifier() ConstantNameAfterIdentifier()?
;

ClassConstantNameAfterIdentifier
/**
* An asterisk after a name goes on with the name of the constant, whether or not
* there is a space between the two.
*/
ConstantNameAfterIdentifier
: <T_WILDCARD_WS>
| <T_WILDCARD> ClassConstantNameAfterWildcard()?
| <T_WILDCARD> ConstantNameAfterWildcard()?
| <T_NAME_WILDCARD_WS>
| <T_NAME_WILDCARD> ConstantNameAfterWildcard()?
;
6 changes: 5 additions & 1 deletion doc/grammars/lexemes.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,16 @@
* followed by something the type says nothing about;
* - "Foo[0]" reads an offset while "Foo [0]" does not;
* - "Foo::BAR*" goes on reading the name of the constant while "Foo::BAR* "
* ends it.
* ends it;
* - "FOO*" is the name of a global constant written with an asterisk while
* "FOO *" is the name "FOO" followed by an asterisk.
*/
%token T_OPEN_PARENTHESES_WS (?<=[\x09\x20])\(
%token T_OPEN_CURLY_BRACKET_WS (?<=[\x09\x20])\{
%token T_OPEN_SQUARE_BRACKET_WS (?<=[\x09\x20])\[
%token T_WILDCARD_WS \*(?=[\x09\x20])
%token T_NAME_WILDCARD (?<=[0-9a-z_\x80-\xFF-])\*(?![\x09\x20])
%token T_NAME_WILDCARD_WS (?<=[0-9a-z_\x80-\xFF-])\*(?=[\x09\x20])

/**
* A "<" opening what the hand-written parser recognizes as an HTML tag, that is
Expand Down
2 changes: 2 additions & 0 deletions doc/grammars/phpdoc-block.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,8 @@ NonTagTextToken
| <T_COLON>
| <T_WILDCARD>
| <T_WILDCARD_WS>
| <T_NAME_WILDCARD>
| <T_NAME_WILDCARD_WS>
| <T_OPEN_PHPDOC>
| <T_OTHER>
;
Expand Down
14 changes: 7 additions & 7 deletions doc/grammars/types.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ Atomic
/**
* A name standing for a type rather than for the class of a constant.
*
* A name is only ever read as a constant expression when a "::" follows it, so
* a name written without one is a type and stays a type: whatever the reading
* A name is only ever read as a constant expression when a "::" follows it, or
* an asterisk with no space in between, so a name written without one is a type and stays a type: whatever the reading
* fails at after it is an error rather than a reason to read the name again as
* something else.
*/
PlainName
: Identifier() !<T_DOUBLE_COLON>
: Identifier() !<T_DOUBLE_COLON> !NameWildcard()
;

ParenthesizedAtomic
Expand All @@ -132,8 +132,8 @@ IdentifierAtomic
: <T_KEYWORD_ARRAY> !<T_DOUBLE_COLON> ArrayShape() ArrayOrOffsetAccess()?
| ListShapeIdentifier() !<T_DOUBLE_COLON> ListShape() ArrayOrOffsetAccess()?
| <T_KEYWORD_OBJECT> !<T_DOUBLE_COLON> ObjectShape() ArrayOrOffsetAccess()?
| ShapeIdentifier() !<T_DOUBLE_COLON> !<T_OPEN_CURLY_BRACKET> IdentifierTail()
| PlainIdentifier() !<T_DOUBLE_COLON> IdentifierTail()
| ShapeIdentifier() !<T_DOUBLE_COLON> !NameWildcard() !<T_OPEN_CURLY_BRACKET> IdentifierTail()
| PlainIdentifier() !<T_DOUBLE_COLON> !NameWildcard() IdentifierTail()
;

/**
Expand Down Expand Up @@ -358,8 +358,8 @@ CallableReturnType
| <T_KEYWORD_ARRAY> !<T_DOUBLE_COLON> ArrayShape() ArrayOrOffsetAccess()?
| ListShapeIdentifier() !<T_DOUBLE_COLON> ListShape() ArrayOrOffsetAccess()?
| <T_KEYWORD_OBJECT> !<T_DOUBLE_COLON> ObjectShape() ArrayOrOffsetAccess()?
| ShapeIdentifier() !<T_DOUBLE_COLON> !<T_OPEN_CURLY_BRACKET> CallableReturnTypeTail()
| PlainIdentifier() !<T_DOUBLE_COLON> CallableReturnTypeTail()
| ShapeIdentifier() !<T_DOUBLE_COLON> !NameWildcard() !<T_OPEN_CURLY_BRACKET> CallableReturnTypeTail()
| PlainIdentifier() !<T_DOUBLE_COLON> !NameWildcard() CallableReturnTypeTail()
| !PlainName() ConstantAtomic()
;

Expand Down
78 changes: 49 additions & 29 deletions src/Parser/ConstExprParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,36 +144,18 @@ public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
}

if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = '';
$lastType = null;
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$classConstantName .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;

continue;
}

if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$classConstantName .= '*';
$lastType = Lexer::TOKEN_WILDCARD;

if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}

continue;
}

if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_WILDCARD) && !$tokens->isPrecededByHorizontalWhitespace()) {
// a global constant name pattern like PHP_ROUND_*
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode('', $this->parseConstantNamePattern($tokens, $identifier, Lexer::TOKEN_IDENTIFIER)),
$startLine,
$startIndex,
);
}

break;
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = $this->parseConstantNamePattern($tokens, '', null);

return $this->enrichWithAttributes(
$tokens,
Expand Down Expand Up @@ -205,6 +187,44 @@ public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode
);
}

/**
* Reads the rest of a constant name in which * stands for any sequence of characters.
*
* @param Lexer::TOKEN_IDENTIFIER|Lexer::TOKEN_WILDCARD|null $lastType the token $name ends with, null when it is empty
*/
private function parseConstantNamePattern(TokenIterator $tokens, string $name, ?int $lastType): string
{
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$name .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;

continue;
}

if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$name .= '*';
$lastType = Lexer::TOKEN_WILDCARD;

if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}

continue;
}

if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}

break;
}

return $name;
}

private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
{
$items = [];
Expand Down
18 changes: 16 additions & 2 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex);

if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
if (!$this->isConstantFetchAfterName($tokens)) {
$tokens->dropSavePoint(); // because of ConstFetchNode
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$tokens->pushSavePoint();
Expand Down Expand Up @@ -267,6 +267,20 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
}
}

/**
* Whether the name just read starts a constant expression rather than a type: a class constant
* like Foo::BAR, or a global constant pattern like FOO_* whose asterisk follows the name with
* no space in between.
*/
private function isConstantFetchAfterName(TokenIterator $tokens): bool
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
return true;
}

return $tokens->isCurrentTokenType(Lexer::TOKEN_WILDCARD) && !$tokens->isPrecededByHorizontalWhitespace();
}

/** @phpstan-impure */
private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
Expand Down Expand Up @@ -692,7 +706,7 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);

if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
if (!$this->isConstantFetchAfterName($tokens)) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric(
$tokens,
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Parser/ConstExprParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,21 @@ public function provideFetchNodeParseData(): Iterator
'self::CLASS_CONSTANT',
new ConstFetchNode('self', 'CLASS_CONSTANT'),
];

yield [
'GLOBAL_*',
new ConstFetchNode('', 'GLOBAL_*'),
];

yield [
'GLOBAL_*_CONSTANT',
new ConstFetchNode('', 'GLOBAL_*_CONSTANT'),
];

yield [
'Foo\\Bar\\GLOBAL_*',
new ConstFetchNode('', 'Foo\\Bar\\GLOBAL_*'),
];
}

}
48 changes: 48 additions & 0 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ public function provideParamTagsData(): Iterator
]),
];

yield [
'OK with a global constant pattern',
'/** @param PHP_ROUND_* $mode */',
new PhpDocNode([
new PhpDocTagNode(
'@param',
new ParamTagValueNode(
new ConstTypeNode(new ConstFetchNode('', 'PHP_ROUND_*')),
false,
'$mode',
'',
false,
),
),
]),
];

yield [
'OK variadic without description',
'/** @param Foo ...$foo */',
Expand Down Expand Up @@ -1392,6 +1409,37 @@ public function providePropertyTagsData(): Iterator

public function provideReturnTagsData(): Iterator
{
yield [
'OK with a global constant pattern',
'/** @return PHP_ROUND_*|null */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new UnionTypeNode([
new ConstTypeNode(new ConstFetchNode('', 'PHP_ROUND_*')),
new IdentifierTypeNode('null'),
]),
'',
),
),
]),
];

yield [
'OK with an asterisk opening the description',
'/** @return string *emphasized* description */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new IdentifierTypeNode('string'),
'*emphasized* description',
),
),
]),
];

yield [
'OK without description',
'/** @return Foo */',
Expand Down
Loading
Loading