Skip to content

Commit 445ccb7

Browse files
authored
Support conditional types
* Support conditional types Part of implementation for phpstan/phpstan#3853 * Add fuzzy tests * Only support "is" / "is not" * Rename trueType/falseType to if/else * Add tests to TypeParserTest * cs * Only allow parenthesized conditional types * Remove multiline support It'll be easier to implement this for more types properly separately * Support conditional for parameters
1 parent 691b019 commit 445ccb7

8 files changed

+442
-10
lines changed

doc/grammars/type.abnf

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,26 @@ Type
66
= Atomic [Union / Intersection]
77
/ Nullable
88

9+
ParenthesizedType
10+
= Atomic [Union / Intersection / Conditional]
11+
/ Nullable
12+
913
Union
1014
= 1*(TokenUnion Atomic)
1115

1216
Intersection
1317
= 1*(TokenIntersection Atomic)
1418

19+
Conditional
20+
= 1*ByteHorizontalWs TokenIs [TokenNot] Atomic TokenNullable Atomic TokenColon Atomic
21+
1522
Nullable
1623
= TokenNullable TokenIdentifier [Generic]
1724

1825
Atomic
1926
= TokenIdentifier [Generic / Callable / Array]
2027
/ TokenThisVariable
21-
/ TokenParenthesesOpen Type TokenParenthesesClose [Array]
28+
/ TokenParenthesesOpen ParenthesizedType TokenParenthesesClose [Array]
2229

2330
Generic
2431
= TokenAngleBracketOpen Type *(TokenComma Type) TokenAngleBracketClose
@@ -175,6 +182,12 @@ TokenDoubleColon
175182
TokenThisVariable
176183
= %x24.74.68.69.73 *ByteHorizontalWs
177184

185+
TokenIs
186+
= %x69.73 1*ByteHorizontalWs
187+
188+
TokenNot
189+
= %x6E.6F.74 1*ByteHorizontalWs
190+
178191
TokenIdentifier
179192
= [ByteBackslash] ByteIdentifierFirst *ByteIdentifierSecond *(ByteBackslash ByteIdentifierFirst *ByteIdentifierSecond) *ByteHorizontalWs
180193

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use function sprintf;
7+
8+
class ConditionalTypeForParameterNode implements TypeNode
9+
{
10+
11+
use NodeAttributes;
12+
13+
/** @var string */
14+
public $parameterName;
15+
16+
/** @var TypeNode */
17+
public $targetType;
18+
19+
/** @var TypeNode */
20+
public $if;
21+
22+
/** @var TypeNode */
23+
public $else;
24+
25+
/** @var bool */
26+
public $negated;
27+
28+
public function __construct(string $parameterName, TypeNode $targetType, TypeNode $if, TypeNode $false, bool $negated)
29+
{
30+
$this->parameterName = $parameterName;
31+
$this->targetType = $targetType;
32+
$this->if = $if;
33+
$this->else = $false;
34+
$this->negated = $negated;
35+
}
36+
37+
public function __toString(): string
38+
{
39+
return sprintf(
40+
'%s %s %s ? %s : %s',
41+
$this->parameterName,
42+
$this->negated ? 'is not' : 'is',
43+
$this->targetType,
44+
$this->if,
45+
$this->else
46+
);
47+
}
48+
49+
}

src/Ast/Type/ConditionalTypeNode.php

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use function sprintf;
7+
8+
class ConditionalTypeNode implements TypeNode
9+
{
10+
11+
use NodeAttributes;
12+
13+
/** @var TypeNode */
14+
public $subjectType;
15+
16+
/** @var TypeNode */
17+
public $targetType;
18+
19+
/** @var TypeNode */
20+
public $if;
21+
22+
/** @var TypeNode */
23+
public $else;
24+
25+
/** @var bool */
26+
public $negated;
27+
28+
public function __construct(TypeNode $subjectType, TypeNode $targetType, TypeNode $if, TypeNode $false, bool $negated)
29+
{
30+
$this->subjectType = $subjectType;
31+
$this->targetType = $targetType;
32+
$this->if = $if;
33+
$this->else = $false;
34+
$this->negated = $negated;
35+
}
36+
37+
public function __toString(): string
38+
{
39+
return sprintf(
40+
'%s %s %s ? %s : %s',
41+
$this->subjectType,
42+
$this->negated ? 'is not' : 'is',
43+
$this->targetType,
44+
$this->if,
45+
$this->else
46+
);
47+
}
48+
49+
}

src/Parser/ParserException.php

+24-6
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,28 @@ class ParserException extends Exception
2525
/** @var int */
2626
private $expectedTokenType;
2727

28+
/** @var string|null */
29+
private $expectedTokenValue;
30+
2831
public function __construct(
2932
string $currentTokenValue,
3033
int $currentTokenType,
3134
int $currentOffset,
32-
int $expectedTokenType
35+
int $expectedTokenType,
36+
?string $expectedTokenValue = null
3337
)
3438
{
3539
$this->currentTokenValue = $currentTokenValue;
3640
$this->currentTokenType = $currentTokenType;
3741
$this->currentOffset = $currentOffset;
3842
$this->expectedTokenType = $expectedTokenType;
39-
40-
$json = json_encode($currentTokenValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
41-
assert($json !== false);
43+
$this->expectedTokenValue = $expectedTokenValue;
4244

4345
parent::__construct(sprintf(
44-
'Unexpected token %s, expected %s at offset %d',
45-
$json,
46+
'Unexpected token %s, expected %s%s at offset %d',
47+
$this->formatValue($currentTokenValue),
4648
Lexer::TOKEN_LABELS[$expectedTokenType],
49+
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
4750
$currentOffset
4851
));
4952
}
@@ -72,4 +75,19 @@ public function getExpectedTokenType(): int
7275
return $this->expectedTokenType;
7376
}
7477

78+
79+
public function getExpectedTokenValue(): ?string
80+
{
81+
return $this->expectedTokenValue;
82+
}
83+
84+
85+
private function formatValue(string $value): string
86+
{
87+
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
88+
assert($json !== false);
89+
90+
return $json;
91+
}
92+
7593
}

src/Parser/TokenIterator.php

+22-2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,25 @@ public function consumeTokenType(int $tokenType): void
9494
}
9595

9696

97+
/**
98+
* @throws ParserException
99+
*/
100+
public function consumeTokenValue(int $tokenType, string $tokenValue): void
101+
{
102+
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
103+
$this->throwError($tokenType, $tokenValue);
104+
}
105+
106+
$this->index++;
107+
108+
if (($this->tokens[$this->index][Lexer::TYPE_OFFSET] ?? -1) !== Lexer::TOKEN_HORIZONTAL_WS) {
109+
return;
110+
}
111+
112+
$this->index++;
113+
}
114+
115+
97116
/** @phpstan-impure */
98117
public function tryConsumeTokenValue(string $tokenValue): bool
99118
{
@@ -191,13 +210,14 @@ public function rollback(): void
191210
/**
192211
* @throws ParserException
193212
*/
194-
private function throwError(int $expectedTokenType): void
213+
private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
195214
{
196215
throw new ParserException(
197216
$this->currentTokenValue(),
198217
$this->currentTokenType(),
199218
$this->currentTokenOffset(),
200-
$expectedTokenType
219+
$expectedTokenType,
220+
$expectedTokenValue
201221
);
202222
}
203223

src/Parser/TypeParser.php

+76-1
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,37 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
3939
return $type;
4040
}
4141

42+
/** @phpstan-impure */
43+
private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
44+
{
45+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
46+
$type = $this->parseNullable($tokens);
47+
48+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
49+
$type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());
50+
51+
} else {
52+
$type = $this->parseAtomic($tokens);
53+
54+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
55+
$type = $this->parseUnion($tokens, $type);
56+
57+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
58+
$type = $this->parseIntersection($tokens, $type);
59+
} elseif ($tokens->isCurrentTokenValue('is')) {
60+
$type = $this->parseConditional($tokens, $type);
61+
}
62+
}
63+
64+
return $type;
65+
}
66+
4267

4368
/** @phpstan-impure */
4469
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
4570
{
4671
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
47-
$type = $this->parse($tokens);
72+
$type = $this->subParse($tokens);
4873
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
4974

5075
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
@@ -157,6 +182,56 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ
157182
}
158183

159184

185+
/** @phpstan-impure */
186+
private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
187+
{
188+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
189+
190+
$negated = false;
191+
if ($tokens->isCurrentTokenValue('not')) {
192+
$negated = true;
193+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
194+
}
195+
196+
$targetType = $this->parseAtomic($tokens);
197+
198+
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
199+
200+
$ifType = $this->parseAtomic($tokens);
201+
202+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
203+
204+
$elseType = $this->parseAtomic($tokens);
205+
206+
return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated);
207+
}
208+
209+
/** @phpstan-impure */
210+
private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode
211+
{
212+
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
213+
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');
214+
215+
$negated = false;
216+
if ($tokens->isCurrentTokenValue('not')) {
217+
$negated = true;
218+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
219+
}
220+
221+
$targetType = $this->parseAtomic($tokens);
222+
223+
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
224+
225+
$ifType = $this->parseAtomic($tokens);
226+
227+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
228+
229+
$elseType = $this->parseAtomic($tokens);
230+
231+
return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
232+
}
233+
234+
160235
/** @phpstan-impure */
161236
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
162237
{

0 commit comments

Comments
 (0)