1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\Annotations\TypeParser; |
6
|
|
|
|
7
|
|
|
use Doctrine\Annotations\Metadata\Type\BooleanType; |
8
|
|
|
use Doctrine\Annotations\Metadata\Type\FloatType; |
9
|
|
|
use Doctrine\Annotations\Metadata\Type\IntegerType; |
10
|
|
|
use Doctrine\Annotations\Metadata\Type\IntersectionType; |
11
|
|
|
use Doctrine\Annotations\Metadata\Type\ListType; |
12
|
|
|
use Doctrine\Annotations\Metadata\Type\MapType; |
13
|
|
|
use Doctrine\Annotations\Metadata\Type\MixedType; |
14
|
|
|
use Doctrine\Annotations\Metadata\Type\NullType; |
15
|
|
|
use Doctrine\Annotations\Metadata\Type\ObjectType; |
16
|
|
|
use Doctrine\Annotations\Metadata\Type\StringType; |
17
|
|
|
use Doctrine\Annotations\Metadata\Type\Type; |
18
|
|
|
use Doctrine\Annotations\Metadata\Type\UnionType; |
19
|
|
|
use Doctrine\Annotations\Parser\Ast\Reference; |
20
|
|
|
use Doctrine\Annotations\Parser\Reference\ReferenceResolver; |
21
|
|
|
use Doctrine\Annotations\Parser\Scope; |
22
|
|
|
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; |
23
|
|
|
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; |
24
|
|
|
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; |
25
|
|
|
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; |
26
|
|
|
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; |
27
|
|
|
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; |
28
|
|
|
use PHPStan\PhpDocParser\Ast\Type\TypeNode; |
29
|
|
|
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; |
30
|
|
|
use PHPStan\PhpDocParser\Lexer\Lexer; |
31
|
|
|
use PHPStan\PhpDocParser\Parser\PhpDocParser; |
32
|
|
|
use PHPStan\PhpDocParser\Parser\TokenIterator; |
33
|
|
|
use function array_map; |
34
|
|
|
use function assert; |
35
|
|
|
use function count; |
36
|
|
|
use function ltrim; |
37
|
|
|
use function strcasecmp; |
38
|
|
|
use function strtolower; |
39
|
|
|
|
40
|
|
|
final class PHPStanTypeParser implements TypeParser |
41
|
|
|
{ |
42
|
|
|
/** @var Lexer */ |
43
|
|
|
private $lexer; |
44
|
|
|
|
45
|
|
|
/** @var PhpDocParser */ |
46
|
|
|
private $phpDocParser; |
47
|
|
|
|
48
|
|
|
/** @var ReferenceResolver */ |
49
|
|
|
private $referenceResolver; |
50
|
|
|
|
51
|
30 |
|
public function __construct(Lexer $lexer, PhpDocParser $phpDocParser, ReferenceResolver $referenceResolver) |
52
|
|
|
{ |
53
|
30 |
|
$this->lexer = $lexer; |
54
|
30 |
|
$this->phpDocParser = $phpDocParser; |
55
|
30 |
|
$this->referenceResolver = $referenceResolver; |
56
|
30 |
|
} |
57
|
|
|
|
58
|
27 |
|
public function parsePropertyType(string $docBlock, Scope $scope) : Type |
59
|
|
|
{ |
60
|
27 |
|
$tags = $this->parse($docBlock)->getVarTagValues(); |
61
|
|
|
|
62
|
27 |
|
assert(count($tags) <= 1, 'multiple @var tags not allowed'); |
63
|
|
|
|
64
|
27 |
|
if (count($tags) === 0) { |
65
|
3 |
|
return new MixedType(); |
66
|
|
|
} |
67
|
|
|
|
68
|
24 |
|
return $this->resolveType($tags[0]->type, $scope); |
69
|
|
|
} |
70
|
|
|
|
71
|
27 |
|
private function parse(string $docBlock) : PhpDocNode |
72
|
|
|
{ |
73
|
27 |
|
return $this->phpDocParser->parse(new TokenIterator($this->lexer->tokenize($docBlock))); |
74
|
|
|
} |
75
|
|
|
|
76
|
24 |
|
private function resolveType(TypeNode $typeNode, Scope $scope) : Type |
77
|
|
|
{ |
78
|
24 |
|
if ($typeNode instanceof IdentifierTypeNode && strcasecmp($typeNode->name, 'null') === 0) { |
79
|
8 |
|
return new NullType(); |
80
|
|
|
} |
81
|
|
|
|
82
|
24 |
|
if ($typeNode instanceof NullableTypeNode) { |
83
|
1 |
|
return new UnionType($this->resolveType($typeNode->type, $scope), new NullType()); |
84
|
|
|
} |
85
|
|
|
|
86
|
24 |
|
if ($typeNode instanceof UnionTypeNode) { |
87
|
9 |
|
return new UnionType( |
88
|
9 |
|
...array_map( |
89
|
|
|
function (TypeNode $type) use ($scope) : Type { |
90
|
9 |
|
return $this->resolveType($type, $scope); |
91
|
9 |
|
}, |
92
|
9 |
|
$typeNode->types |
93
|
|
|
) |
94
|
|
|
); |
95
|
|
|
} |
96
|
|
|
|
97
|
24 |
|
if ($typeNode instanceof IntersectionTypeNode) { |
98
|
2 |
|
return new IntersectionType( |
99
|
2 |
|
...array_map( |
100
|
|
|
function (TypeNode $type) use ($scope) : Type { |
101
|
2 |
|
return $this->resolveType($type, $scope); |
102
|
2 |
|
}, |
103
|
2 |
|
$typeNode->types |
104
|
|
|
) |
105
|
|
|
); |
106
|
|
|
} |
107
|
|
|
|
108
|
24 |
|
if ($typeNode instanceof ArrayTypeNode) { |
109
|
6 |
|
return $this->resolveArrayTypeNode($typeNode, $scope); |
110
|
|
|
} |
111
|
|
|
|
112
|
24 |
|
if ($typeNode instanceof GenericTypeNode) { |
113
|
5 |
|
return $this->resolveGenericNode($typeNode, $scope); |
114
|
|
|
} |
115
|
|
|
|
116
|
24 |
|
assert($typeNode instanceof IdentifierTypeNode, 'Unsupported node type'); |
117
|
|
|
|
118
|
24 |
|
return $this->resolveIdentifierNode($typeNode, $scope); |
119
|
|
|
} |
120
|
|
|
|
121
|
5 |
|
private function resolveGenericNode(GenericTypeNode $typeNode, Scope $scope) : Type |
122
|
|
|
{ |
123
|
5 |
|
assert( |
124
|
5 |
|
strcasecmp($typeNode->type->name, 'array') === 0 |
125
|
5 |
|
|| strcasecmp($typeNode->type->name, 'iterable') === 0 |
126
|
|
|
); |
127
|
|
|
|
128
|
5 |
|
if (count($typeNode->genericTypes) === 1) { |
129
|
3 |
|
return new ListType($this->resolveType($typeNode->genericTypes[0], $scope)); |
130
|
|
|
} |
131
|
|
|
|
132
|
3 |
|
if (count($typeNode->genericTypes) === 2) { |
133
|
3 |
|
assert($typeNode->genericTypes[0] instanceof IdentifierTypeNode); |
134
|
|
|
|
135
|
3 |
|
return new MapType( |
136
|
3 |
|
$this->resolveIdentifierNode($typeNode->genericTypes[0], $scope), |
|
|
|
|
137
|
3 |
|
$this->resolveType($typeNode->genericTypes[1], $scope) |
138
|
|
|
); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
assert(false, '>2 generic type args'); |
|
|
|
|
142
|
|
|
} |
143
|
|
|
|
144
|
6 |
|
private function resolveArrayTypeNode(ArrayTypeNode $typeNode, Scope $scope) : Type |
145
|
|
|
{ |
146
|
6 |
|
return new ListType($this->resolveType($typeNode->type, $scope)); |
147
|
|
|
} |
148
|
|
|
|
149
|
24 |
|
private function resolveIdentifierNode(TypeNode $typeNode, Scope $scope) : Type |
150
|
|
|
{ |
151
|
24 |
|
assert($typeNode instanceof IdentifierTypeNode); |
152
|
|
|
|
153
|
24 |
|
$canonicalName = strtolower($typeNode->name); |
154
|
|
|
|
155
|
24 |
|
switch ($canonicalName) { |
156
|
24 |
|
case 'bool': |
157
|
24 |
|
case 'boolean': |
158
|
1 |
|
return new BooleanType(); |
159
|
24 |
|
case 'int': |
160
|
13 |
|
case 'integer': |
161
|
15 |
|
return new IntegerType(); |
162
|
13 |
|
case 'float': |
163
|
13 |
|
case 'double': |
164
|
12 |
|
case 'real': |
165
|
3 |
|
return new FloatType(); |
166
|
11 |
|
case 'string': |
167
|
5 |
|
return new StringType(); |
168
|
8 |
|
case 'array': |
169
|
2 |
|
return new ListType(new MixedType()); |
170
|
7 |
|
case 'mixed': |
171
|
2 |
|
return new MixedType(); // TODO not really a scalar |
172
|
|
|
} |
173
|
|
|
|
174
|
6 |
|
$fullyQualified = $typeNode->name[0] === '\\'; |
175
|
6 |
|
return new ObjectType( |
176
|
6 |
|
$this->referenceResolver->resolve( |
177
|
6 |
|
new Reference( |
178
|
6 |
|
$fullyQualified ? ltrim($typeNode->name, '\\') : $typeNode->name, |
179
|
6 |
|
$fullyQualified |
180
|
|
|
), |
181
|
6 |
|
$scope |
182
|
|
|
) |
183
|
|
|
); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|