Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 445ccb7

Browse files
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

File tree

8 files changed

+442
-10
lines changed

8 files changed

+442
-10
lines changed

‎doc/grammars/type.abnf‎

Lines changed: 14 additions & 1 deletion
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

Lines changed: 49 additions & 0 deletions
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‎

Lines changed: 49 additions & 0 deletions
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‎

Lines changed: 24 additions & 6 deletions
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‎

Lines changed: 22 additions & 2 deletions
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‎

Lines changed: 76 additions & 1 deletion
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)

AltStyle によって変換されたページ (->オリジナル) /