Skip to content

Commit 7402e40

Browse files
committed
Support conditional types
Part of implementation for phpstan/phpstan#3853
1 parent 4bda9e3 commit 7402e40

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed
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 $trueType;
21+
22+
/** @var TypeNode */
23+
public $falseType;
24+
25+
/** @var bool */
26+
public $negated;
27+
28+
public function __construct(TypeNode $subjectType, TypeNode $targetType, TypeNode $trueType, TypeNode $falseType, bool $negated)
29+
{
30+
$this->subjectType = $subjectType;
31+
$this->targetType = $targetType;
32+
$this->trueType = $trueType;
33+
$this->falseType = $falseType;
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->trueType,
45+
$this->falseType
46+
);
47+
}
48+
49+
}

src/Parser/TypeParser.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
3333

3434
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
3535
$type = $this->parseIntersection($tokens, $type);
36+
} elseif ($tokens->isCurrentTokenValue('is')) {
37+
$type = $this->parseConditional($tokens, $type);
3638
}
3739
}
3840

@@ -44,7 +46,9 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
4446
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
4547
{
4648
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
49+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
4750
$type = $this->parse($tokens);
51+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
4852
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
4953

5054
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
@@ -157,6 +161,35 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ
157161
}
158162

159163

164+
/** @phpstan-impure */
165+
private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
166+
{
167+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
168+
169+
$negated = false;
170+
if ($tokens->isCurrentTokenValue('not')) {
171+
$negated = true;
172+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
173+
}
174+
175+
$targetType = $this->parseAtomic($tokens);
176+
177+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
178+
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
179+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
180+
181+
$trueType = $this->parseAtomic($tokens);
182+
183+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
184+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
185+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
186+
187+
$falseType = $this->parseAtomic($tokens);
188+
189+
return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $trueType, $falseType, $negated);
190+
}
191+
192+
160193
/** @phpstan-impure */
161194
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
162195
{

tests/PHPStan/Parser/PhpDocParserTest.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
3232
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
3333
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
34+
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
3435
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
3536
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
3637
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
@@ -1119,6 +1120,78 @@ public function provideReturnTagsData(): Iterator
11191120
),
11201121
]),
11211122
];
1123+
1124+
yield [
1125+
'OK with conditional type',
1126+
'/** @return Foo is Bar ? never : int */',
1127+
new PhpDocNode([
1128+
new PhpDocTagNode(
1129+
'@return',
1130+
new ReturnTagValueNode(
1131+
new ConditionalTypeNode(
1132+
new IdentifierTypeNode('Foo'),
1133+
new IdentifierTypeNode('Bar'),
1134+
new IdentifierTypeNode('never'),
1135+
new IdentifierTypeNode('int'),
1136+
false
1137+
),
1138+
''
1139+
)
1140+
),
1141+
]),
1142+
];
1143+
1144+
yield [
1145+
'OK with negated conditional type',
1146+
'/** @return Foo is not Bar ? never : int */',
1147+
new PhpDocNode([
1148+
new PhpDocTagNode(
1149+
'@return',
1150+
new ReturnTagValueNode(
1151+
new ConditionalTypeNode(
1152+
new IdentifierTypeNode('Foo'),
1153+
new IdentifierTypeNode('Bar'),
1154+
new IdentifierTypeNode('never'),
1155+
new IdentifierTypeNode('int'),
1156+
true
1157+
),
1158+
''
1159+
)
1160+
),
1161+
]),
1162+
];
1163+
1164+
yield [
1165+
'OK with multiline conditional type',
1166+
'/**
1167+
* @return (
1168+
* T is self::TYPE_STRING
1169+
* ? string
1170+
* : (T is self::TYPE_INT ? int : bool)
1171+
* )
1172+
*/',
1173+
new PhpDocNode([
1174+
new PhpDocTagNode(
1175+
'@return',
1176+
new ReturnTagValueNode(
1177+
new ConditionalTypeNode(
1178+
new IdentifierTypeNode('T'),
1179+
new ConstTypeNode(new ConstFetchNode('self', 'TYPE_STRING')),
1180+
new IdentifierTypeNode('string'),
1181+
new ConditionalTypeNode(
1182+
new IdentifierTypeNode('T'),
1183+
new ConstTypeNode(new ConstFetchNode('self', 'TYPE_INT')),
1184+
new IdentifierTypeNode('int'),
1185+
new IdentifierTypeNode('bool'),
1186+
false
1187+
),
1188+
false
1189+
),
1190+
''
1191+
)
1192+
),
1193+
]),
1194+
];
11221195
}
11231196

11241197

0 commit comments

Comments
 (0)