From d042fedc447206cc98b7aef34b1a01bbf326863e Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 May 2024 13:54:34 +0200 Subject: [PATCH] Add syntax for subtraction type --- doc/grammars/type.abnf | 6 +++ src/Ast/Type/SubtractionTypeNode.php | 30 ++++++++++++ src/Lexer/Lexer.php | 3 ++ src/Parser/TypeParser.php | 31 +++++++++++++ src/Printer/Printer.php | 38 ++++++++++++++- tests/PHPStan/Parser/TypeParserTest.php | 61 +++++++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 41 +++++++++++++++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/Ast/Type/SubtractionTypeNode.php diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 36118d2b..9a1918fd 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -16,6 +16,9 @@ Union Intersection = 1*(TokenIntersection Atomic) +Subtraction + = TokenSubtraction Atomic + Conditional = 1*ByteHorizontalWs TokenIs [TokenNot] Atomic TokenNullable Type TokenColon ParenthesizedType @@ -141,6 +144,9 @@ TokenUnion TokenIntersection = "&" *ByteHorizontalWs +TokenSubtraction + = "~" *ByteHorizontalWs + TokenNullable = "?" *ByteHorizontalWs diff --git a/src/Ast/Type/SubtractionTypeNode.php b/src/Ast/Type/SubtractionTypeNode.php new file mode 100644 index 00000000..ddbd72e2 --- /dev/null +++ b/src/Ast/Type/SubtractionTypeNode.php @@ -0,0 +1,30 @@ +type = $type; + $this->subtractedType = $subtractedType; + } + + + public function __toString(): string + { + return $this->type . '~' . $this->subtractedType; + } + +} diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 32539faf..181c2dbc 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -49,11 +49,13 @@ class Lexer public const TOKEN_CLOSE_CURLY_BRACKET = 34; public const TOKEN_NEGATED = 35; public const TOKEN_ARROW = 36; + public const TOKEN_SUBTRACTION = 37; public const TOKEN_LABELS = [ self::TOKEN_REFERENCE => '\'&\'', self::TOKEN_UNION => '\'|\'', self::TOKEN_INTERSECTION => '\'&\'', + self::TOKEN_SUBTRACTION => '\'~\'', self::TOKEN_NULLABLE => '\'?\'', self::TOKEN_NEGATED => '\'!\'', self::TOKEN_OPEN_PARENTHESES => '\'(\'', @@ -147,6 +149,7 @@ private function generateRegexp(): string self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))', self::TOKEN_UNION => '\\|', self::TOKEN_INTERSECTION => '&', + self::TOKEN_SUBTRACTION => '\\~', self::TOKEN_NULLABLE => '\\?', self::TOKEN_NEGATED => '!', diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 5669fe45..77a60332 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -59,6 +59,9 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { $type = $this->parseIntersection($tokens, $type); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SUBTRACTION)) { + $type = $this->parseSubtraction($tokens, $type); } } @@ -111,6 +114,9 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { $type = $this->subParseIntersection($tokens, $type); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SUBTRACTION)) { + $type = $this->subParseSubtraction($tokens, $type); } } } @@ -312,6 +318,31 @@ private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $ } + /** @phpstan-impure */ + private function parseSubtraction(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode + { + $tokens->consumeTokenType(Lexer::TOKEN_SUBTRACTION); + + $subtractedType = $this->parseAtomic($tokens); + + return new Ast\Type\SubtractionTypeNode($type, $subtractedType); + } + + + /** @phpstan-impure */ + private function subParseSubtraction(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode + { + $tokens->consumeTokenType(Lexer::TOKEN_SUBTRACTION); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $subtractedType = $this->parseAtomic($tokens); + + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + return new Ast\Type\SubtractionTypeNode($type, $subtractedType); + } + + /** @phpstan-impure */ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode { diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 044d07f8..a59ca80e 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -57,6 +57,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -129,11 +130,13 @@ final class Printer CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, + SubtractionTypeNode::class, ], ArrayTypeNode::class . '->type' => [ CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, + SubtractionTypeNode::class, ConstTypeNode::class, NullableTypeNode::class, ], @@ -141,8 +144,19 @@ final class Printer CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, + SubtractionTypeNode::class, NullableTypeNode::class, ], + SubtractionTypeNode::class . '->type' => [ + UnionTypeNode::class, + IntersectionTypeNode::class, + SubtractionTypeNode::class, + ], + SubtractionTypeNode::class . '->subtractedType' => [ + UnionTypeNode::class, + IntersectionTypeNode::class, + SubtractionTypeNode::class, + ], ]; /** @var array>> */ @@ -150,11 +164,13 @@ final class Printer IntersectionTypeNode::class . '->types' => [ IntersectionTypeNode::class, UnionTypeNode::class, + SubtractionTypeNode::class, NullableTypeNode::class, ], UnionTypeNode::class . '->types' => [ IntersectionTypeNode::class, UnionTypeNode::class, + SubtractionTypeNode::class, NullableTypeNode::class, ], ]; @@ -387,7 +403,12 @@ private function printType(TypeNode $node): string return $this->printOffsetAccessType($node->type) . '[]'; } if ($node instanceof CallableTypeNode) { - if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) { + if ( + $node->returnType instanceof CallableTypeNode + || $node->returnType instanceof UnionTypeNode + || $node->returnType instanceof IntersectionTypeNode + || $node->returnType instanceof SubtractionTypeNode + ) { $returnType = $this->wrapInParentheses($node->returnType); } else { $returnType = $this->printType($node->returnType); @@ -450,6 +471,7 @@ private function printType(TypeNode $node): string if ( $type instanceof IntersectionTypeNode || $type instanceof UnionTypeNode + || $type instanceof SubtractionTypeNode || $type instanceof NullableTypeNode ) { $items[] = $this->wrapInParentheses($type); @@ -461,11 +483,14 @@ private function printType(TypeNode $node): string return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items); } + if ($node instanceof SubtractionTypeNode) { + return $this->printSubtractionType($node->type) . '~' . $this->printSubtractionType($node->subtractedType); + } if ($node instanceof InvalidTypeNode) { return (string) $node; } if ($node instanceof NullableTypeNode) { - if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) { + if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode || $node->type instanceof SubtractionTypeNode) { return '?(' . $this->printType($node->type) . ')'; } @@ -519,6 +544,15 @@ private function printOffsetAccessType(TypeNode $type): string return $this->printType($type); } + private function printSubtractionType(TypeNode $type): string + { + if ($type instanceof UnionTypeNode || $type instanceof IntersectionTypeNode) { + return $this->wrapInParentheses($type); + } + + return $this->printType($type); + } + private function printConstExpr(ConstExprNode $node): string { // this is fine - ConstExprNode classes do not contain nodes that need smart printer logic diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index d6c66bb8..15980a0b 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -26,6 +26,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -276,6 +277,66 @@ public function provideParseData(): array ]), Lexer::TOKEN_INTERSECTION, ], + [ + 'string~int', + new SubtractionTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ), + ], + [ + 'string ~ int', + new SubtractionTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ), + ], + [ + '(string ~ int)', + new SubtractionTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ), + ], + [ + '(' . PHP_EOL . + ' string' . PHP_EOL . + ' ~' . PHP_EOL . + ' int' . PHP_EOL . + ')', + new SubtractionTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ), + ], + [ + 'string~int~float', + new SubtractionTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ), + Lexer::TOKEN_SUBTRACTION, + ], + [ + '(string&int)~float', + new SubtractionTypeNode( + new IntersectionTypeNode([ + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int'), + ]), + new IdentifierTypeNode('float') + ), + ], + [ + 'float~(string&int)', + new SubtractionTypeNode( + new IdentifierTypeNode('float'), + new IntersectionTypeNode([ + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int'), + ]) + ), + ], [ 'string[]', new ArrayTypeNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index d73481e2..fd61a11d 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -39,6 +39,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\SubtractionTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -1427,6 +1428,46 @@ public function enterNode(Node $node) }, ]; + yield [ + '/** @param Foo~Bar $a */', + '/** @param Foo~(Lorem|Ipsum) $a */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof SubtractionTypeNode) { + $node->subtractedType = new UnionTypeNode([ + new IdentifierTypeNode('Lorem'), + new IdentifierTypeNode('Ipsum'), + ]); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @param Foo~Bar $a */', + '/** @param (Lorem|Ipsum)~Bar $a */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof SubtractionTypeNode) { + $node->type = new UnionTypeNode([ + new IdentifierTypeNode('Lorem'), + new IdentifierTypeNode('Ipsum'), + ]); + } + + return $node; + } + + }, + ]; + yield [ '/** @var ArrayObject */', '/** @var ArrayObject> */',