Repository: phpactor/phpactor (php)

Issue:
Could not parse docblock
```
@var null|'always'|'auto' 
```

Pull request:
Fix docblock parsing
Fix #1449 and support literal types in docblocks

Patch:
diff --git a/lib/DocblockParser/Ast/Token.php b/lib/DocblockParser/Ast/Token.php
index 96388998..9a1c0206 100644
--- a/lib/DocblockParser/Ast/Token.php
+++ b/lib/DocblockParser/Ast/Token.php
@@ -17,6 +17,8 @@ final class Token implements Element
     public const T_COMMA = 'COMMA';
     public const T_LIST = 'LIST';
     public const T_LABEL = 'LABEL';
+    public const T_INTEGER = 'INTEGER';
+    public const T_FLOAT = 'FLOAT';
     public const T_WHITESPACE = 'WHITESPACE';
     public const T_BRACKET_SQUARE_OPEN = 'BRACKET_SQUARE_OPEN';
     public const T_BRACKET_SQUARE_CLOSE = 'BRACKET_SQUARE_CLOSE';
@@ -24,6 +26,7 @@ final class Token implements Element
     public const T_BRACKET_ANGLE_CLOSE = 'BRACKET_ANGLE_CLOSE';
     public const T_BRACKET_CURLY_OPEN = 'BRACKET_CURLY_OPEN';
     public const T_BRACKET_CURLY_CLOSE = 'BRACKET_CURLY_CLOSE';
+    public const T_QUOTED_STRING = 'QUOTED_STRING';
     public const T_PAREN_OPEN = 'PAREN_OPEN';
     public const T_PAREN_CLOSE = 'PAREN_CLOSE';
     public const T_INVALID = 'INVALID';
diff --git a/lib/DocblockParser/Ast/Tokens.php b/lib/DocblockParser/Ast/Tokens.php
index 6ccb067f..0eaebf1e 100644
--- a/lib/DocblockParser/Ast/Tokens.php
+++ b/lib/DocblockParser/Ast/Tokens.php
@@ -74,12 +74,13 @@ final class Tokens implements IteratorAggregate
 
         if (null !== $type && $token->type !== $type) {
             throw new RuntimeException(sprintf(
-                'Expected type "%s" at position "%s": "%s"',
+                'Expected type "%s" at position "%s": "%s" got "%s"',
                 $type,
                 $this->position,
                 implode('', array_map(function (Token $token) {
                     return $token->value;
-                }, $this->tokens))
+                }, $this->tokens)),
+                $token->type,
             ));
         }
 
diff --git a/lib/DocblockParser/Ast/Type/LiteralFloatNode.php b/lib/DocblockParser/Ast/Type/LiteralFloatNode.php
new file mode 100644
index 00000000..8d9fce94
--- /dev/null
+++ b/lib/DocblockParser/Ast/Type/LiteralFloatNode.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Phpactor\DocblockParser\Ast\Type;
+
+use Phpactor\DocblockParser\Ast\Token;
+use Phpactor\DocblockParser\Ast\TypeNode;
+
+class LiteralFloatNode extends TypeNode
+{
+    protected const CHILD_NAMES = [
+        'token',
+    ];
+
+    public Token $token;
+
+    public function __construct(Token $token)
+    {
+        $this->token = $token;
+    }
+}
diff --git a/lib/DocblockParser/Ast/Type/LiteralIntegerNode.php b/lib/DocblockParser/Ast/Type/LiteralIntegerNode.php
new file mode 100644
index 00000000..5e268dbb
--- /dev/null
+++ b/lib/DocblockParser/Ast/Type/LiteralIntegerNode.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Phpactor\DocblockParser\Ast\Type;
+
+use Phpactor\DocblockParser\Ast\Token;
+use Phpactor\DocblockParser\Ast\TypeNode;
+
+class LiteralIntegerNode extends TypeNode
+{
+    protected const CHILD_NAMES = [
+        'token',
+    ];
+
+    public Token $token;
+
+    public function __construct(Token $token)
+    {
+        $this->token = $token;
+    }
+}
diff --git a/lib/DocblockParser/Ast/Type/LiteralStringNode.php b/lib/DocblockParser/Ast/Type/LiteralStringNode.php
new file mode 100644
index 00000000..d016b074
--- /dev/null
+++ b/lib/DocblockParser/Ast/Type/LiteralStringNode.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Phpactor\DocblockParser\Ast\Type;
+
+use Phpactor\DocblockParser\Ast\Token;
+use Phpactor\DocblockParser\Ast\TypeNode;
+
+class LiteralStringNode extends TypeNode
+{
+    protected const CHILD_NAMES = [
+        'token',
+    ];
+
+    public Token $token;
+
+    public function __construct(Token $token)
+    {
+        $this->token = $token;
+    }
+}
diff --git a/lib/DocblockParser/Lexer.php b/lib/DocblockParser/Lexer.php
index 4c2178e2..9f393275 100644
--- a/lib/DocblockParser/Lexer.php
+++ b/lib/DocblockParser/Lexer.php
@@ -8,6 +8,8 @@ use RuntimeException;
 
 final class Lexer
 {
+    private const PATTERN_LABEL = '[a-zA-Z\\\][-a-zA-Z0-9_\\\]*';
+
     /**
      * @var string[]
      */
@@ -25,7 +27,11 @@ final class Lexer
         '=', // equals
         '(', ')', '\{', '\}', '\[', '\]', '<', '>', // brackets
         '\$[a-zA-Z0-9_\x80-\xff]+', // variable
-        '[a-zA-Z\\\][-a-zA-Z0-9_\\\]*', // label
+        self::PATTERN_LABEL, // label
+        '"' . self::PATTERN_LABEL . '"',
+        '\'' . self::PATTERN_LABEL . '\'',
+        '[0-9]+\.[0-9]+',
+        '[0-9]+',
     ];
     private const TOKEN_VALUE_MAP = [
         ']' => Token::T_BRACKET_SQUARE_CLOSE,
@@ -109,6 +115,17 @@ final class Lexer
             return self::TOKEN_VALUE_MAP[$value];
         }
 
+        if (is_numeric($value)) {
+            if (false !== strpos($value, '.')) {
+                return Token::T_FLOAT;
+            }
+            return Token::T_INTEGER;
+        }
+
+        if ($value[0] === '"' || $value[0] == '\'') {
+            return Token::T_QUOTED_STRING;
+        }
+
         if ($value[0] === '$') {
             return Token::T_VARIABLE;
         }
diff --git a/lib/DocblockParser/Parser.php b/lib/DocblockParser/Parser.php
index 0a3a791d..4799349d 100644
--- a/lib/DocblockParser/Parser.php
+++ b/lib/DocblockParser/Parser.php
@@ -27,6 +27,9 @@ use Phpactor\DocblockParser\Ast\TagNode;
 use Phpactor\DocblockParser\Ast\TypeNode;
 use Phpactor\DocblockParser\Ast\Type\GenericNode;
 use Phpactor\DocblockParser\Ast\Type\ListNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralFloatNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralIntegerNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralStringNode;
 use Phpactor\DocblockParser\Ast\Type\NullNode;
 use Phpactor\DocblockParser\Ast\Type\NullableNode;
 use Phpactor\DocblockParser\Ast\Type\ParenthesizedType;
@@ -236,7 +239,7 @@ final class Parser
             return new ParenthesizedType($open, $type, $close);
         }
 
-        $type = $this->tokens->chomp(Token::T_LABEL);
+        $type = $this->tokens->chomp();
 
         if (null === $this->tokens->current) {
             return $this->createTypeFromToken($type);
@@ -328,6 +331,15 @@ final class Parser
         if (in_array($type->value, self::SCALAR_TYPES)) {
             return new ScalarNode($type);
         }
+        if ($type->type === Token::T_QUOTED_STRING) {
+            return new LiteralStringNode($type);
+        }
+        if ($type->type === Token::T_FLOAT) {
+            return new LiteralFloatNode($type);
+        }
+        if ($type->type === Token::T_INTEGER) {
+            return new LiteralIntegerNode($type);
+        }
 
         return new ClassNode($type);
     }
@@ -476,6 +488,9 @@ final class Parser
     {
         return $this->tokens->if(Token::T_LABEL) ||
             $this->tokens->if(Token::T_NULLABLE) ||
+            $this->tokens->if(Token::T_QUOTED_STRING) ||
+            $this->tokens->if(Token::T_INTEGER) ||
+            $this->tokens->if(Token::T_FLOAT) ||
             $this->tokens->if(Token::T_PAREN_OPEN);
     }
 
diff --git a/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php b/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
index 499a202b..5d2f336f 100644
--- a/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
+++ b/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
@@ -3,6 +3,9 @@
 namespace Phpactor\WorseReflection\Bridge\Phpactor\DocblockParser;
 
 use Phpactor\DocblockParser\Ast\Type\ArrayShapeNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralFloatNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralIntegerNode;
+use Phpactor\DocblockParser\Ast\Type\LiteralStringNode;
 use Phpactor\DocblockParser\Ast\Type\ParenthesizedType;
 use Phpactor\WorseReflection\Core\Type\ArrayKeyType;
 use Phpactor\DocblockParser\Ast\Node;
@@ -26,8 +29,10 @@ use Phpactor\WorseReflection\Core\Type\CallableType;
 use Phpactor\WorseReflection\Core\Type\ClassStringType;
 use Phpactor\WorseReflection\Core\Type\ClassType;
 use Phpactor\WorseReflection\Core\Type\ClosureType;
+use Phpactor\WorseReflection\Core\Type\FloatLiteralType;
 use Phpactor\WorseReflection\Core\Type\FloatType;
 use Phpactor\WorseReflection\Core\Type\GenericClassType;
+use Phpactor\WorseReflection\Core\Type\IntLiteralType;
 use Phpactor\WorseReflection\Core\Type\IntType;
 use Phpactor\WorseReflection\Core\Type\IterablePrimitiveType;
 use Phpactor\WorseReflection\Core\Type\MissingType;
@@ -39,6 +44,7 @@ use Phpactor\WorseReflection\Core\Type\ReflectedClassType;
 use Phpactor\WorseReflection\Core\Type\ResourceType;
 use Phpactor\WorseReflection\Core\Type\SelfType;
 use Phpactor\WorseReflection\Core\Type\StaticType;
+use Phpactor\WorseReflection\Core\Type\StringLiteralType;
 use Phpactor\WorseReflection\Core\Type\StringType;
 use Phpactor\WorseReflection\Core\Type\UnionType;
 use Phpactor\WorseReflection\Core\Type\VoidType;
@@ -90,6 +96,15 @@ class TypeConverter
         if ($type instanceof ParenthesizedType) {
             return $this->convertParenthesized($type, $scope);
         }
+        if ($type instanceof LiteralStringNode) {
+            return $this->convertLiteralString($type, $scope);
+        }
+        if ($type instanceof LiteralIntegerNode) {
+            return $this->convertLiteralInteger($type, $scope);
+        }
+        if ($type instanceof LiteralFloatNode) {
+            return $this->convertLiteralFloat($type, $scope);
+        }
 
         return new MissingType();
     }
@@ -253,4 +268,22 @@ class TypeConverter
     {
         return new PhpactorParenthesizedType($this->convert($type->node));
     }
+
+    private function convertLiteralString(LiteralStringNode $type, ?ReflectionScope $scope): Type
+    {
+        $quote = substr($type->token->value, 0, 1);
+        $string = trim($type->token->value, $quote);
+
+        return new StringLiteralType($string);
+    }
+
+    private function convertLiteralInteger(LiteralIntegerNode $type, ?ReflectionScope $scope): Type
+    {
+        return new IntLiteralType((int)$type->token->value);
+    }
+
+    private function convertLiteralFloat(LiteralFloatNode $type, ?ReflectionScope $scope): Type
+    {
+        return new FloatLiteralType((float)$type->token->value);
+    }
 }


Test patch:
diff --git a/lib/DocblockParser/Tests/Unit/LexerTest.php b/lib/DocblockParser/Tests/Unit/LexerTest.php
index f3c04390..157ac7fc 100644
--- a/lib/DocblockParser/Tests/Unit/LexerTest.php
+++ b/lib/DocblockParser/Tests/Unit/LexerTest.php
@@ -101,5 +101,24 @@ class LexerTest extends TestCase
                 [Token::T_BRACKET_CURLY_CLOSE, '}'],
             ]
         ];
+        yield [
+            '"foobar"',
+            [
+                [Token::T_QUOTED_STRING, '"foobar"'],
+            ]
+        ];
+        yield [
+            '123',
+            [
+                [Token::T_INTEGER, '123'],
+            ]
+        ];
+
+        yield [
+            '123.4',
+            [
+                [Token::T_FLOAT, '123.4'],
+            ]
+        ];
     }
 }
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals.test b/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals.test
new file mode 100644
index 00000000..f7f1c211
--- /dev/null
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals.test
@@ -0,0 +1,10 @@
+/** @var null|"always"|"auto" */
+---
+Docblock: = 
+ ElementList: = /** 
+  VarTag: = @var
+   UnionNode: = 
+    TypeList: = 
+     NullNode: = null|
+     LiteralStringNode: = "always"|
+     LiteralStringNode: = "auto" */
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals2.test b/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals2.test
new file mode 100644
index 00000000..394a7b93
--- /dev/null
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/union_literals2.test
@@ -0,0 +1,9 @@
+/** @var 123|123.3 */
+---
+Docblock: = 
+ ElementList: = /** 
+  VarTag: = @var
+   UnionNode: = 
+    TypeList: = 
+     LiteralIntegerNode: = 123|
+     LiteralFloatNode: = 123.3 */
diff --git a/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php b/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
index 88be2be0..d887ecb7 100644
--- a/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
+++ b/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
@@ -169,6 +169,11 @@ class DocblockParserFactoryTest extends IntegrationTestCase
                 ,
             'array{foo:int,bar:string}',
         ];
+
+        yield 'literals' => [
+            '/** @return null|"foo"|123|123.3 */',
+            'null|"foo"|123|123.3',
+        ];
     }
 
     public function testMethods(): void