Skip to content
Closed
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
16 changes: 14 additions & 2 deletions doc/grammars/types.pp3
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,24 @@
* "int\n|string" is one type while "int\nstring" is a type followed by a
* description. Whichever of the two is written, a "|" or a "&" left unread is
* an error rather than the end of the type, which is what the predicates say.
*
* A "?" written first carries over the "|" that may follow it, because "?A|B" is
* the same set of values whichever of the two the "?" is read to belong to. An
* "&" is not carried over, as "(?A)&B" and "?(A&B)" are not the same set.
*/
Type
: Nullable()
: Nullable() ( NullableTail() | !<T_UNION> !<T_INTERSECTION> )
| Atomic() ( TypeTail() | !<T_UNION> !<T_INTERSECTION> )
;

TypeTail
: Trivia() ( Union() | Intersection() )
;

NullableTail
: Trivia() Union()
;

Union
: ( <T_UNION> Atomic() ( Trivia() &<T_UNION> )? )+ !<T_UNION>
;
Expand All @@ -56,7 +64,7 @@ Nullable
* may be written and the only place a line break needs no "|" after it.
*/
SubType
: Nullable()
: Nullable() SubNullableTail()
| <T_VARIABLE> ConditionalForParameter()
| Atomic() ( Conditional() | SubTypeTail() )
;
Expand All @@ -65,6 +73,10 @@ SubTypeTail
: Trivia() ( SubUnion() | SubIntersection() )?
;

SubNullableTail
: Trivia() SubUnion()?
;

SubUnion
: ( <T_UNION> Trivia() Atomic() Trivia() )+ !<T_UNION>
;
Expand Down
64 changes: 45 additions & 19 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,44 +34,58 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$nullable = $tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE);
if ($nullable) {
$type = $this->parseNullable($tokens);

} else {
$type = $this->parseAtomic($tokens);
}

$tokens->pushSavePoint();
$tokens->skipNewLineTokensAndConsumeComments();
$tokens->pushSavePoint();
$tokens->skipNewLineTokensAndConsumeComments();

try {
$enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type);
try {
$enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type, $nullable);

} catch (ParserException $parserException) {
$enrichedType = null;
}
} catch (ParserException $parserException) {
$enrichedType = null;
}

if ($enrichedType !== null) {
$type = $enrichedType;
$tokens->dropSavePoint();
if ($enrichedType !== null) {
$type = $enrichedType;
$tokens->dropSavePoint();

} else {
$tokens->rollback();
$type = $this->enrichTypeOnUnionOrIntersection($tokens, $type) ?? $type;
}
} else {
$tokens->rollback();
$type = $this->enrichTypeOnUnionOrIntersection($tokens, $type, $nullable) ?? $type;
}

return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}

/** @phpstan-impure */
private function enrichTypeOnUnionOrIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): ?Ast\Type\TypeNode
/**
* A "?" written before a type carries over the "|" that may follow it, because
* "?A|B" is the same set of values whichever of the two the "?" is read to
* belong to: "(?A)|B" and "?(A|B)" are both "A|B|null".
*
* An "&" is left alone, as "(?A)&B" and "?(A&B)" are not the same set, and
* PHP itself writes the latter as "(A&B)|null" rather than as "?A&B".
*
* @phpstan-impure
*/
private function enrichTypeOnUnionOrIntersection(
TokenIterator $tokens,
Ast\Type\TypeNode $type,
bool $unionOnly = false
): ?Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
return $this->parseUnion($tokens, $type);

}

if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
if (!$unionOnly && $tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
return $this->parseIntersection($tokens, $type);
}

Expand Down Expand Up @@ -112,7 +126,11 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode

if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
$tokens->skipNewLineTokensAndConsumeComments();

if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
$type = $this->subParseUnion($tokens, $type);
}
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());

Expand Down Expand Up @@ -395,11 +413,19 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par
/** @phpstan-impure */
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();

$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);

$type = $this->parseAtomic($tokens);

return new Ast\Type\NullableTypeNode($type);
return $this->enrichWithAttributes(
$tokens,
new Ast\Type\NullableTypeNode($type),
$startLine,
$startIndex,
);
}

/** @phpstan-impure */
Expand Down
71 changes: 71 additions & 0 deletions tests/PHPStan/Parser/TypeParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,77 @@ public function provideParseData(): array
new IdentifierTypeNode('int'),
),
],
[
'?int|null',
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('int')),
new IdentifierTypeNode('null'),
]),
],
[
'?Foo|Bar',
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('Bar'),
]),
],
[
'?Foo|Bar|Baz',
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('Bar'),
new IdentifierTypeNode('Baz'),
]),
],
[
"?Foo\n|Bar",
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('Bar'),
]),
],
[
'(?Foo|Bar)',
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('Bar'),
]),
],
[
'array<?Foo|null>',
new GenericTypeNode(
new IdentifierTypeNode('array'),
[
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('null'),
]),
],
[
GenericTypeNode::VARIANCE_INVARIANT,
],
),
],
[
'array{a: ?Foo|null}',
ArrayShapeNode::createSealed([
new ArrayShapeItemNode(
new IdentifierTypeNode('a'),
false,
new UnionTypeNode([
new NullableTypeNode(new IdentifierTypeNode('Foo')),
new IdentifierTypeNode('null'),
]),
),
]),
],
[
// "(?Foo)&Bar" and "?(Foo&Bar)" are different types, so the "&"
// is left for the caller to reject rather than read into either.
'?Foo&Bar',
new NullableTypeNode(new IdentifierTypeNode('Foo')),
Lexer::TOKEN_INTERSECTION,
],
[
'?Foo<Bar>',
new NullableTypeNode(
Expand Down
Loading