Skip to content

Commit d042fed

Browse files
committed
Add syntax for subtraction type
1 parent 536889f commit d042fed

File tree

7 files changed

+208
-2
lines changed

7 files changed

+208
-2
lines changed

doc/grammars/type.abnf

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Union
1616
Intersection
1717
= 1*(TokenIntersection Atomic)
1818

19+
Subtraction
20+
= TokenSubtraction Atomic
21+
1922
Conditional
2023
= 1*ByteHorizontalWs TokenIs [TokenNot] Atomic TokenNullable Type TokenColon ParenthesizedType
2124

@@ -141,6 +144,9 @@ TokenUnion
141144
TokenIntersection
142145
= "&" *ByteHorizontalWs
143146

147+
TokenSubtraction
148+
= "~" *ByteHorizontalWs
149+
144150
TokenNullable
145151
= "?" *ByteHorizontalWs
146152

src/Ast/Type/SubtractionTypeNode.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
7+
class SubtractionTypeNode implements TypeNode
8+
{
9+
10+
use NodeAttributes;
11+
12+
/** @var TypeNode */
13+
public $type;
14+
15+
/** @var TypeNode */
16+
public $subtractedType;
17+
18+
public function __construct(TypeNode $type, TypeNode $subtractedType)
19+
{
20+
$this->type = $type;
21+
$this->subtractedType = $subtractedType;
22+
}
23+
24+
25+
public function __toString(): string
26+
{
27+
return $this->type . '~' . $this->subtractedType;
28+
}
29+
30+
}

src/Lexer/Lexer.php

+3
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ class Lexer
4949
public const TOKEN_CLOSE_CURLY_BRACKET = 34;
5050
public const TOKEN_NEGATED = 35;
5151
public const TOKEN_ARROW = 36;
52+
public const TOKEN_SUBTRACTION = 37;
5253

5354
public const TOKEN_LABELS = [
5455
self::TOKEN_REFERENCE => '\'&\'',
5556
self::TOKEN_UNION => '\'|\'',
5657
self::TOKEN_INTERSECTION => '\'&\'',
58+
self::TOKEN_SUBTRACTION => '\'~\'',
5759
self::TOKEN_NULLABLE => '\'?\'',
5860
self::TOKEN_NEGATED => '\'!\'',
5961
self::TOKEN_OPEN_PARENTHESES => '\'(\'',
@@ -147,6 +149,7 @@ private function generateRegexp(): string
147149
self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))',
148150
self::TOKEN_UNION => '\\|',
149151
self::TOKEN_INTERSECTION => '&',
152+
self::TOKEN_SUBTRACTION => '\\~',
150153
self::TOKEN_NULLABLE => '\\?',
151154
self::TOKEN_NEGATED => '!',
152155

src/Parser/TypeParser.php

+31
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
5959

6060
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
6161
$type = $this->parseIntersection($tokens, $type);
62+
63+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SUBTRACTION)) {
64+
$type = $this->parseSubtraction($tokens, $type);
6265
}
6366
}
6467

@@ -111,6 +114,9 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
111114

112115
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
113116
$type = $this->subParseIntersection($tokens, $type);
117+
118+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SUBTRACTION)) {
119+
$type = $this->subParseSubtraction($tokens, $type);
114120
}
115121
}
116122
}
@@ -312,6 +318,31 @@ private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $
312318
}
313319

314320

321+
/** @phpstan-impure */
322+
private function parseSubtraction(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
323+
{
324+
$tokens->consumeTokenType(Lexer::TOKEN_SUBTRACTION);
325+
326+
$subtractedType = $this->parseAtomic($tokens);
327+
328+
return new Ast\Type\SubtractionTypeNode($type, $subtractedType);
329+
}
330+
331+
332+
/** @phpstan-impure */
333+
private function subParseSubtraction(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
334+
{
335+
$tokens->consumeTokenType(Lexer::TOKEN_SUBTRACTION);
336+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
337+
338+
$subtractedType = $this->parseAtomic($tokens);
339+
340+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
341+
342+
return new Ast\Type\SubtractionTypeNode($type, $subtractedType);
343+
}
344+
345+
315346
/** @phpstan-impure */
316347
private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
317348
{

src/Printer/Printer.php

+36-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
5858
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
5959
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
60+
use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode;
6061
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
6162
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
6263
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
@@ -129,32 +130,47 @@ final class Printer
129130
CallableTypeNode::class,
130131
UnionTypeNode::class,
131132
IntersectionTypeNode::class,
133+
SubtractionTypeNode::class,
132134
],
133135
ArrayTypeNode::class . '->type' => [
134136
CallableTypeNode::class,
135137
UnionTypeNode::class,
136138
IntersectionTypeNode::class,
139+
SubtractionTypeNode::class,
137140
ConstTypeNode::class,
138141
NullableTypeNode::class,
139142
],
140143
OffsetAccessTypeNode::class . '->type' => [
141144
CallableTypeNode::class,
142145
UnionTypeNode::class,
143146
IntersectionTypeNode::class,
147+
SubtractionTypeNode::class,
144148
NullableTypeNode::class,
145149
],
150+
SubtractionTypeNode::class . '->type' => [
151+
UnionTypeNode::class,
152+
IntersectionTypeNode::class,
153+
SubtractionTypeNode::class,
154+
],
155+
SubtractionTypeNode::class . '->subtractedType' => [
156+
UnionTypeNode::class,
157+
IntersectionTypeNode::class,
158+
SubtractionTypeNode::class,
159+
],
146160
];
147161

148162
/** @var array<string, list<class-string<TypeNode>>> */
149163
private $parenthesesListMap = [
150164
IntersectionTypeNode::class . '->types' => [
151165
IntersectionTypeNode::class,
152166
UnionTypeNode::class,
167+
SubtractionTypeNode::class,
153168
NullableTypeNode::class,
154169
],
155170
UnionTypeNode::class . '->types' => [
156171
IntersectionTypeNode::class,
157172
UnionTypeNode::class,
173+
SubtractionTypeNode::class,
158174
NullableTypeNode::class,
159175
],
160176
];
@@ -387,7 +403,12 @@ private function printType(TypeNode $node): string
387403
return $this->printOffsetAccessType($node->type) . '[]';
388404
}
389405
if ($node instanceof CallableTypeNode) {
390-
if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) {
406+
if (
407+
$node->returnType instanceof CallableTypeNode
408+
|| $node->returnType instanceof UnionTypeNode
409+
|| $node->returnType instanceof IntersectionTypeNode
410+
|| $node->returnType instanceof SubtractionTypeNode
411+
) {
391412
$returnType = $this->wrapInParentheses($node->returnType);
392413
} else {
393414
$returnType = $this->printType($node->returnType);
@@ -450,6 +471,7 @@ private function printType(TypeNode $node): string
450471
if (
451472
$type instanceof IntersectionTypeNode
452473
|| $type instanceof UnionTypeNode
474+
|| $type instanceof SubtractionTypeNode
453475
|| $type instanceof NullableTypeNode
454476
) {
455477
$items[] = $this->wrapInParentheses($type);
@@ -461,11 +483,14 @@ private function printType(TypeNode $node): string
461483

462484
return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items);
463485
}
486+
if ($node instanceof SubtractionTypeNode) {
487+
return $this->printSubtractionType($node->type) . '~' . $this->printSubtractionType($node->subtractedType);
488+
}
464489
if ($node instanceof InvalidTypeNode) {
465490
return (string) $node;
466491
}
467492
if ($node instanceof NullableTypeNode) {
468-
if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) {
493+
if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode || $node->type instanceof SubtractionTypeNode) {
469494
return '?(' . $this->printType($node->type) . ')';
470495
}
471496

@@ -519,6 +544,15 @@ private function printOffsetAccessType(TypeNode $type): string
519544
return $this->printType($type);
520545
}
521546

547+
private function printSubtractionType(TypeNode $type): string
548+
{
549+
if ($type instanceof UnionTypeNode || $type instanceof IntersectionTypeNode) {
550+
return $this->wrapInParentheses($type);
551+
}
552+
553+
return $this->printType($type);
554+
}
555+
522556
private function printConstExpr(ConstExprNode $node): string
523557
{
524558
// this is fine - ConstExprNode classes do not contain nodes that need smart printer logic

tests/PHPStan/Parser/TypeParserTest.php

+61
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
2727
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
2828
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
29+
use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode;
2930
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
3031
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
3132
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
@@ -276,6 +277,66 @@ public function provideParseData(): array
276277
]),
277278
Lexer::TOKEN_INTERSECTION,
278279
],
280+
[
281+
'string~int',
282+
new SubtractionTypeNode(
283+
new IdentifierTypeNode('string'),
284+
new IdentifierTypeNode('int')
285+
),
286+
],
287+
[
288+
'string ~ int',
289+
new SubtractionTypeNode(
290+
new IdentifierTypeNode('string'),
291+
new IdentifierTypeNode('int')
292+
),
293+
],
294+
[
295+
'(string ~ int)',
296+
new SubtractionTypeNode(
297+
new IdentifierTypeNode('string'),
298+
new IdentifierTypeNode('int')
299+
),
300+
],
301+
[
302+
'(' . PHP_EOL .
303+
' string' . PHP_EOL .
304+
' ~' . PHP_EOL .
305+
' int' . PHP_EOL .
306+
')',
307+
new SubtractionTypeNode(
308+
new IdentifierTypeNode('string'),
309+
new IdentifierTypeNode('int')
310+
),
311+
],
312+
[
313+
'string~int~float',
314+
new SubtractionTypeNode(
315+
new IdentifierTypeNode('string'),
316+
new IdentifierTypeNode('int')
317+
),
318+
Lexer::TOKEN_SUBTRACTION,
319+
],
320+
[
321+
'(string&int)~float',
322+
new SubtractionTypeNode(
323+
new IntersectionTypeNode([
324+
new IdentifierTypeNode('string'),
325+
new IdentifierTypeNode('int'),
326+
]),
327+
new IdentifierTypeNode('float')
328+
),
329+
],
330+
[
331+
'float~(string&int)',
332+
new SubtractionTypeNode(
333+
new IdentifierTypeNode('float'),
334+
new IntersectionTypeNode([
335+
new IdentifierTypeNode('string'),
336+
new IdentifierTypeNode('int'),
337+
])
338+
),
339+
],
279340
[
280341
'string[]',
281342
new ArrayTypeNode(

tests/PHPStan/Printer/PrinterTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
4040
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
4141
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
42+
use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode;
4243
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
4344
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
4445
use PHPStan\PhpDocParser\Lexer\Lexer;
@@ -1427,6 +1428,46 @@ public function enterNode(Node $node)
14271428
},
14281429
];
14291430

1431+
yield [
1432+
'/** @param Foo~Bar $a */',
1433+
'/** @param Foo~(Lorem|Ipsum) $a */',
1434+
new class extends AbstractNodeVisitor {
1435+
1436+
public function enterNode(Node $node)
1437+
{
1438+
if ($node instanceof SubtractionTypeNode) {
1439+
$node->subtractedType = new UnionTypeNode([
1440+
new IdentifierTypeNode('Lorem'),
1441+
new IdentifierTypeNode('Ipsum'),
1442+
]);
1443+
}
1444+
1445+
return $node;
1446+
}
1447+
1448+
},
1449+
];
1450+
1451+
yield [
1452+
'/** @param Foo~Bar $a */',
1453+
'/** @param (Lorem|Ipsum)~Bar $a */',
1454+
new class extends AbstractNodeVisitor {
1455+
1456+
public function enterNode(Node $node)
1457+
{
1458+
if ($node instanceof SubtractionTypeNode) {
1459+
$node->type = new UnionTypeNode([
1460+
new IdentifierTypeNode('Lorem'),
1461+
new IdentifierTypeNode('Ipsum'),
1462+
]);
1463+
}
1464+
1465+
return $node;
1466+
}
1467+
1468+
},
1469+
];
1470+
14301471
yield [
14311472
'/** @var ArrayObject<int[]> */',
14321473
'/** @var ArrayObject<array<int>> */',

0 commit comments

Comments
 (0)