Skip to content

Commit 24a72c2

Browse files
committed
FEATURE: Parse references to union types
1 parent 178d579 commit 24a72c2

6 files changed

Lines changed: 170 additions & 32 deletions

File tree

src/Language/AST/Node/TypeReference/TypeNameNodes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public function getSize(): int
5858
return count($this->items);
5959
}
6060

61+
public function getLast(): TypeNameNode
62+
{
63+
return $this->items[$this->getSize() - 1];
64+
}
65+
6166
public function toTypeNames(): TypeNames
6267
{
6368
if ($this->cachedTypeNames === null) {

src/Language/Parser/TypeReference/TypeReferenceParser.php

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,49 +41,26 @@ final class TypeReferenceParser
4141
*/
4242
public function parse(\Iterator $tokens): TypeReferenceNode
4343
{
44-
$isOptional = false;
45-
if (Scanner::type($tokens) === TokenType::QUESTIONMARK) {
46-
$startingToken = $tokens->current();
47-
$isOptional = true;
48-
Scanner::skipOne($tokens);
49-
}
50-
51-
Scanner::assertType($tokens, TokenType::STRING);
44+
$startingToken = $tokens->current();
45+
$questionmarkToken = $this->extractQuestionmarkToken($tokens);
46+
$isOptional = !is_null($questionmarkToken);
5247

53-
$typeNameToken = $finalToken = $tokens->current();
54-
$startingToken = $startingToken ?? $typeNameToken;
48+
$typeNameNodes = $this->parseTypeNames($tokens);
5549

56-
Scanner::skipOne($tokens);
57-
58-
$isArray = false;
59-
if (!Scanner::isEnd($tokens) && Scanner::type($tokens) === TokenType::BRACKET_SQUARE_OPEN) {
60-
Scanner::skipOne($tokens);
61-
Scanner::assertType($tokens, TokenType::BRACKET_SQUARE_CLOSE);
62-
63-
$finalToken = $tokens->current();
64-
$isArray = true;
65-
66-
Scanner::skipOne($tokens);
67-
}
50+
$closingArrayToken = $this->extractClosingArrayToken($tokens);
51+
$isArray = !is_null($closingArrayToken);
6852

6953
try {
7054
return new TypeReferenceNode(
7155
attributes: new NodeAttributes(
7256
pathToSource: $startingToken->sourcePath,
7357
rangeInSource: Range::from(
7458
$startingToken->boundaries->start,
75-
$finalToken->boundaries->end
76-
)
77-
),
78-
names: new TypeNameNodes(
79-
new TypeNameNode(
80-
attributes: new NodeAttributes(
81-
pathToSource: $typeNameToken->sourcePath,
82-
rangeInSource: $typeNameToken->boundaries
83-
),
84-
value: TypeName::from($typeNameToken->value)
59+
$closingArrayToken?->boundaries->end
60+
?? $typeNameNodes->getLast()->attributes->rangeInSource->end
8561
)
8662
),
63+
names: $typeNameNodes,
8764
isArray: $isArray,
8865
isOptional: $isOptional
8966
);
@@ -94,4 +71,76 @@ public function parse(\Iterator $tokens): TypeReferenceNode
9471
);
9572
}
9673
}
74+
75+
/**
76+
* @param \Iterator<mixed,Token> $tokens
77+
* @return Token
78+
*/
79+
public function extractQuestionmarkToken(\Iterator $tokens): ?Token
80+
{
81+
if (Scanner::type($tokens) === TokenType::QUESTIONMARK) {
82+
$questionmarkToken = $tokens->current();
83+
Scanner::skipOne($tokens);
84+
85+
return $questionmarkToken;
86+
}
87+
88+
return null;
89+
}
90+
91+
/**
92+
* @param \Iterator<mixed,Token> $tokens
93+
* @return TypeNameNodes
94+
*/
95+
public function parseTypeNames(\Iterator $tokens): TypeNameNodes
96+
{
97+
$items = [];
98+
while (true) {
99+
Scanner::assertType($tokens, TokenType::STRING);
100+
101+
$typeNameToken = $tokens->current();
102+
$items[] = new TypeNameNode(
103+
attributes: new NodeAttributes(
104+
pathToSource: $typeNameToken->sourcePath,
105+
rangeInSource: $typeNameToken->boundaries
106+
),
107+
value: TypeName::from($typeNameToken->value)
108+
);
109+
110+
Scanner::skipOne($tokens);
111+
112+
if (Scanner::isEnd($tokens)) {
113+
break;
114+
}
115+
116+
if (Scanner::type($tokens) === TokenType::PIPE) {
117+
Scanner::skipOne($tokens);
118+
continue;
119+
}
120+
121+
break;
122+
}
123+
124+
return new TypeNameNodes(...$items);
125+
}
126+
127+
/**
128+
* @param \Iterator<mixed,Token> $tokens
129+
* @return Token
130+
*/
131+
public function extractClosingArrayToken(\Iterator $tokens): ?Token
132+
{
133+
if (!Scanner::isEnd($tokens) && Scanner::type($tokens) === TokenType::BRACKET_SQUARE_OPEN) {
134+
Scanner::skipOne($tokens);
135+
Scanner::assertType($tokens, TokenType::BRACKET_SQUARE_CLOSE);
136+
137+
$closingArrayToken = $tokens->current();
138+
139+
Scanner::skipOne($tokens);
140+
141+
return $closingArrayToken;
142+
}
143+
144+
return null;
145+
}
97146
}

src/Parser/Tokenizer/TokenType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ enum TokenType: string
8686
case EQUALS = 'EQUALS';
8787
case SLASH_FORWARD = 'SLASH_FORWARD';
8888
case DOLLAR = 'DOLLAR';
89+
case PIPE = 'PIPE';
8990

9091
case OPTCHAIN = 'OPTCHAIN';
9192
case NULLISH_COALESCE = 'NULLISH_COALESCE';

src/Parser/Tokenizer/Tokenizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ public static function symbol(\Iterator $fragments, ?Buffer $buffer = null): \It
274274
'=' => $buffer->flush(TokenType::EQUALS),
275275
'?' => $buffer->flush(TokenType::QUESTIONMARK),
276276
'$' => $buffer->flush(TokenType::DOLLAR),
277+
'|' => $buffer->flush(TokenType::PIPE),
277278
default => self::flushRemainder($buffer)
278279
};
279280
}

test/Unit/Language/AST/Node/TypeReference/TypeNameNodesTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,30 @@ public function providesItsOwnSize(): void
115115
$this->assertEquals(3, $typeNameNodes->getSize());
116116
}
117117

118+
/**
119+
* @test
120+
*/
121+
public function providesItsLastItemIfTheresOnlyOne(): void
122+
{
123+
$foo = $this->createTypeNameNode('Foo');
124+
$typeNameNodes = new TypeNameNodes($foo);
125+
126+
$this->assertSame($foo, $typeNameNodes->getLast());
127+
}
128+
129+
/**
130+
* @test
131+
*/
132+
public function providesItsLastItemIfTheresMultiple(): void
133+
{
134+
$foo = $this->createTypeNameNode('Foo');
135+
$bar = $this->createTypeNameNode('Bar');
136+
$baz = $this->createTypeNameNode('Baz');
137+
$typeNameNodes = new TypeNameNodes($foo, $bar, $baz);
138+
139+
$this->assertSame($baz, $typeNameNodes->getLast());
140+
}
141+
118142
/**
119143
* @test
120144
*/

test/Unit/Language/Parser/TypeReference/TypeReferenceParserTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,64 @@ public function producesAstNodeForOptionalTypeReference(): void
155155
);
156156
}
157157

158+
/**
159+
* @test
160+
*/
161+
public function producesAstNodeForUnionTypeReference(): void
162+
{
163+
$typeReferenceParser = new TypeReferenceParser();
164+
$tokens = Tokenizer::fromSource(Source::fromString('Foo|Bar|Baz'))->getIterator();
165+
166+
$expectedTypeReferenceNode = new TypeReferenceNode(
167+
attributes: new NodeAttributes(
168+
pathToSource: Path::fromString(':memory:'),
169+
rangeInSource: Range::from(
170+
new Position(0, 0),
171+
new Position(0, 10)
172+
)
173+
),
174+
names: new TypeNameNodes(
175+
new TypeNameNode(
176+
attributes: new NodeAttributes(
177+
pathToSource: Path::fromString(':memory:'),
178+
rangeInSource: Range::from(
179+
new Position(0, 0),
180+
new Position(0, 2)
181+
)
182+
),
183+
value: TypeName::from('Foo')
184+
),
185+
new TypeNameNode(
186+
attributes: new NodeAttributes(
187+
pathToSource: Path::fromString(':memory:'),
188+
rangeInSource: Range::from(
189+
new Position(0, 4),
190+
new Position(0, 6)
191+
)
192+
),
193+
value: TypeName::from('Bar')
194+
),
195+
new TypeNameNode(
196+
attributes: new NodeAttributes(
197+
pathToSource: Path::fromString(':memory:'),
198+
rangeInSource: Range::from(
199+
new Position(0, 8),
200+
new Position(0, 10)
201+
)
202+
),
203+
value: TypeName::from('Baz')
204+
)
205+
),
206+
isArray: false,
207+
isOptional: false
208+
);
209+
210+
$this->assertEquals(
211+
$expectedTypeReferenceNode,
212+
$typeReferenceParser->parse($tokens)
213+
);
214+
}
215+
158216
/**
159217
* @test
160218
*/

0 commit comments

Comments
 (0)