Skip to content

Commit 7897f32

Browse files
committed
Support more types
1 parent 5df8b0c commit 7897f32

File tree

8 files changed

+180
-101
lines changed

8 files changed

+180
-101
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* @api
1111
*/
12-
interface TypeContext
12+
interface Context
1313
{
1414
/**
1515
* @param non-empty-string $unresolvedName

src/CustomParser.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Typhoon\PHPStanTypeParser;
6+
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use Typhoon\Type\Type;
9+
10+
/**
11+
* @api
12+
*/
13+
interface CustomParser
14+
{
15+
/**
16+
* @param callable(TypeNode): Type $parse
17+
*/
18+
public function parse(TypeNode $node, callable $parse, Context $context): ?Type;
19+
}
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@
44

55
namespace Typhoon\PHPStanTypeParser;
66

7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
78
use Typhoon\Type\Type;
89

910
/**
1011
* @api
1112
*/
12-
final readonly class CustomTypeParsers implements CustomTypeParser
13+
final readonly class CustomParsers implements CustomParser
1314
{
1415
/**
15-
* @param iterable<CustomTypeParser> $customTypeParsers
16+
* @param iterable<CustomParser> $customTypeParsers
1617
*/
1718
public function __construct(
1819
private iterable $customTypeParsers = [],
1920
) {}
2021

21-
public function parseCustomType(string $unresolvedName, array $templateArguments, TypeContext $context): ?Type
22+
public function parse(TypeNode $node, callable $parse, Context $context): ?Type
2223
{
2324
foreach ($this->customTypeParsers as $customTypeParser) {
24-
$type = $customTypeParser->parseCustomType($unresolvedName, $templateArguments, $context);
25+
$type = $customTypeParser->parse($node, $parse, $context);
2526

2627
if ($type !== null) {
2728
return $type;

src/CustomTypeParser.php

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 117 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,46 @@
1111
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
1212
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
1313
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
14+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
15+
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
1416
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
1517
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1618
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1719
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
1820
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
21+
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
1922
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
2023
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
21-
use Typhoon\PHPStanTypeParser\CustomTypeParser;
22-
use Typhoon\PHPStanTypeParser\TypeContext;
24+
use Typhoon\PHPStanTypeParser\Context;
25+
use Typhoon\PHPStanTypeParser\CustomParser;
2326
use Typhoon\Type\ArrayDefaultT;
2427
use Typhoon\Type\ArrayT;
28+
use Typhoon\Type\IterableDefaultT;
29+
use Typhoon\Type\IterableT;
30+
use Typhoon\Type\ListT;
2531
use Typhoon\Type\Type;
2632
use function Typhoon\Type\andT;
33+
use function Typhoon\Type\arrayT;
34+
use function Typhoon\Type\classConstantMaskT;
35+
use function Typhoon\Type\classConstantT;
36+
use function Typhoon\Type\constantT;
2737
use function Typhoon\Type\floatRangeT;
2838
use function Typhoon\Type\floatT;
2939
use function Typhoon\Type\intRangeT;
3040
use function Typhoon\Type\intT;
3141
use function Typhoon\Type\nullOrT;
42+
use function Typhoon\Type\offsetT;
3243
use function Typhoon\Type\orT;
3344
use function Typhoon\Type\stringT;
3445
use const Typhoon\Type\arrayKeyT;
3546
use const Typhoon\Type\arrayT;
3647
use const Typhoon\Type\boolT;
48+
use const Typhoon\Type\callableT;
49+
use const Typhoon\Type\closureT;
3750
use const Typhoon\Type\falseT;
3851
use const Typhoon\Type\floatT;
3952
use const Typhoon\Type\intT;
53+
use const Typhoon\Type\iterableT;
4054
use const Typhoon\Type\literalStringT;
4155
use const Typhoon\Type\lowercaseStringT;
4256
use const Typhoon\Type\mixedT;
@@ -62,27 +76,30 @@
6276
* @internal
6377
* @psalm-internal Typhoon\PHPStanTypeParser
6478
*/
65-
final readonly class ContextualTypeParser
79+
final readonly class ContextualParser
6680
{
6781
public function __construct(
68-
private CustomTypeParser $customTypeParser,
69-
private TypeContext $context,
82+
private CustomParser $customTypeParser,
83+
private Context $context,
7084
) {}
7185

72-
public function parseTypeNode(TypeNode $node): Type
86+
public function parse(TypeNode $node): Type
7387
{
74-
return match (true) {
75-
$node instanceof NullableTypeNode => nullOrT($this->parseTypeNode($node->type)),
76-
$node instanceof ConstTypeNode => self::parseConstExpr($node->constExpr),
77-
$node instanceof IdentifierTypeNode => $this->parseIdentifier($node->name),
78-
$node instanceof GenericTypeNode => $this->parseIdentifier($node->type->name, $node->genericTypes),
79-
$node instanceof UnionTypeNode => orT(...array_map($this->parseTypeNode(...), $node->types)),
80-
$node instanceof IntersectionTypeNode => andT(...array_map($this->parseTypeNode(...), $node->types)),
81-
default => throw new \LogicException(\sprintf('`%s` is not supported', $node::class)),
82-
};
88+
return $this->customTypeParser->parse($node, $this->parse(...), $this->context)
89+
?? match (true) {
90+
$node instanceof NullableTypeNode => nullOrT($this->parse($node->type)),
91+
$node instanceof ConstTypeNode => $this->parseConstExpr($node->constExpr),
92+
$node instanceof IdentifierTypeNode => $this->identifier($node->name),
93+
$node instanceof GenericTypeNode => $this->identifier($node->type->name, $node->genericTypes),
94+
$node instanceof UnionTypeNode => orT(...array_map($this->parse(...), $node->types)),
95+
$node instanceof IntersectionTypeNode => andT(...array_map($this->parse(...), $node->types)),
96+
$node instanceof ArrayTypeNode => arrayT(value: $this->parse($node->type)),
97+
$node instanceof OffsetAccessTypeNode => offsetT($this->parse($node->type), $this->parse($node->offset)),
98+
default => throw new \LogicException(\sprintf('`%s` is not supported', $node::class)),
99+
};
83100
}
84101

85-
private static function parseConstExpr(ConstExprNode $node): Type
102+
private function parseConstExpr(ConstExprNode $node): Type
86103
{
87104
return match (true) {
88105
$node instanceof ConstExprNullNode => nullT,
@@ -97,17 +114,37 @@ private static function parseConstExpr(ConstExprNode $node): Type
97114
default => throw new \LogicException(),
98115
},
99116
$node instanceof ConstExprStringNode => stringT($node->value),
117+
$node instanceof ConstFetchNode => $this->constantFetch($node),
100118
default => throw new \LogicException(\sprintf('PhpDoc node %s is not supported', $node::class)),
101119
};
102120
}
103121

122+
private function constantFetch(ConstFetchNode $node): Type
123+
{
124+
if ($node->className === '') {
125+
return constantT($node->name);
126+
}
127+
128+
$class = $this->context->resolveClassName($node->className);
129+
130+
if ($node->name === 'class') {
131+
return stringT($class);
132+
}
133+
134+
if (str_contains($node->name, '*')) {
135+
return classConstantMaskT($class, $node->name);
136+
}
137+
138+
return classConstantT($class, $node->name);
139+
}
140+
104141
/**
105142
* @param non-empty-string $name
106143
* @param list<TypeNode> $genericNodes
107144
*/
108-
private function parseIdentifier(string $name, array $genericNodes = []): Type
145+
private function identifier(string $name, array $genericNodes = []): Type
109146
{
110-
$atomic = match ($name) {
147+
$singleton = match ($name) {
111148
'never' => neverT,
112149
'void' => voidT,
113150
'null' => nullT,
@@ -130,46 +167,55 @@ private function parseIdentifier(string $name, array $genericNodes = []): Type
130167
'numeric' => numericT,
131168
'scalar' => scalarT,
132169
'object' => objectT,
170+
'Closure' => closureT,
171+
'callable' => callableT,
133172
'mixed' => mixedT,
134173
default => null,
135174
};
136175

137-
if ($atomic !== null) {
176+
if ($singleton !== null) {
138177
if ($genericNodes !== []) {
139178
throw new \LogicException();
140179
}
141180

142-
return $atomic;
181+
return $singleton;
143182
}
144183

145184
if ($name === 'int' || $name === 'integer') {
146-
return $this->parseInt($genericNodes);
185+
return $this->int($genericNodes);
147186
}
148187

149-
if ($name === 'float') {
150-
return $this->parseFloat($genericNodes);
188+
if ($name === 'float' || $name === 'double') {
189+
return $this->float($genericNodes);
151190
}
152191

153-
$templateArguments = array_map($this->parseTypeNode(...), $genericNodes);
192+
$templateArguments = array_map($this->parse(...), $genericNodes);
193+
194+
if ($name === 'list' || $name === 'non-empty-list') {
195+
return $this->list($templateArguments, isNonEmpty: $name === 'non-empty-list');
196+
}
154197

155198
if ($name === 'array' || $name === 'non-empty-array') {
156-
return $this->parseArray($templateArguments, isNonEmpty: $name === 'non-empty-array');
199+
return $this->array($templateArguments, isNonEmpty: $name === 'non-empty-array');
200+
}
201+
202+
if ($name === 'iterable') {
203+
return $this->iterable($templateArguments);
157204
}
158205

159-
return $this->customTypeParser->parseCustomType($name, $templateArguments, $this->context)
160-
?? $this->context->resolveNameAsType($name, $templateArguments);
206+
return $this->context->resolveNameAsType($name, $templateArguments);
161207
}
162208

163209
/**
164210
* @param list<TypeNode> $genericNodes
165211
*/
166-
private function parseInt(array $genericNodes): Type
212+
private function int(array $genericNodes): Type
167213
{
168214
return match (\count($genericNodes)) {
169215
0 => intT,
170216
2 => intRangeT(
171-
min: self::parseIntRangeLimit($genericNodes[0], 'min'),
172-
max: self::parseIntRangeLimit($genericNodes[1], 'max'),
217+
min: self::intRangeLimit($genericNodes[0], 'min'),
218+
max: self::intRangeLimit($genericNodes[1], 'max'),
173219
),
174220
default => throw new \LogicException(\sprintf(
175221
'Int range type should have 2 type arguments, got %d',
@@ -181,24 +227,16 @@ private function parseInt(array $genericNodes): Type
181227
/**
182228
* @param 'min'|'max' $name
183229
*/
184-
private function parseIntRangeLimit(TypeNode $type, string $name): ?int
230+
private function intRangeLimit(TypeNode $type, string $name): ?int
185231
{
186-
if ($type instanceof IdentifierTypeNode) {
187-
if ($type->name === $name) {
188-
return null;
189-
}
232+
$string = (string) $type;
190233

191-
throw new \LogicException();
234+
if ($string === $name) {
235+
return null;
192236
}
193237

194-
if (!$type instanceof ConstTypeNode) {
195-
throw new \LogicException();
196-
}
197-
198-
$expr = $type->constExpr;
199-
200-
if ($expr instanceof ConstExprIntegerNode && is_numeric($expr->value)) {
201-
return (int) $expr->value;
238+
if (is_numeric($string) && !str_contains($string, '.')) {
239+
return (int) $string;
202240
}
203241

204242
throw new \LogicException();
@@ -207,13 +245,13 @@ private function parseIntRangeLimit(TypeNode $type, string $name): ?int
207245
/**
208246
* @param list<TypeNode> $genericNodes
209247
*/
210-
private function parseFloat(array $genericNodes): Type
248+
private function float(array $genericNodes): Type
211249
{
212250
return match (\count($genericNodes)) {
213251
0 => floatT,
214252
2 => floatRangeT(
215-
min: self::parseFloatRangeLimit($genericNodes[0], 'min'),
216-
max: self::parseFloatRangeLimit($genericNodes[1], 'max'),
253+
min: self::floatRangeLimit($genericNodes[0], 'min'),
254+
max: self::floatRangeLimit($genericNodes[1], 'max'),
217255
),
218256
default => throw new \LogicException(\sprintf(
219257
'Float range type should have 2 type arguments, got %d',
@@ -226,24 +264,16 @@ private function parseFloat(array $genericNodes): Type
226264
* @param 'min'|'max' $name
227265
* @return ?numeric-string
228266
*/
229-
private function parseFloatRangeLimit(TypeNode $type, string $name): ?string
267+
private function floatRangeLimit(TypeNode $type, string $name): ?string
230268
{
231-
if ($type instanceof IdentifierTypeNode) {
232-
if ($type->name === $name) {
233-
return null;
234-
}
269+
$string = (string) $type;
235270

236-
throw new \LogicException();
271+
if ($string === $name) {
272+
return null;
237273
}
238274

239-
if (!$type instanceof ConstTypeNode) {
240-
throw new \LogicException();
241-
}
242-
243-
$expr = $type->constExpr;
244-
245-
if (($expr instanceof ConstExprFloatNode || $expr instanceof ConstExprIntegerNode) && is_numeric($expr->value)) {
246-
return $expr->value;
275+
if (is_numeric($string)) {
276+
return $string;
247277
}
248278

249279
throw new \LogicException();
@@ -252,7 +282,19 @@ private function parseFloatRangeLimit(TypeNode $type, string $name): ?string
252282
/**
253283
* @param list<Type> $templateArguments
254284
*/
255-
private function parseArray(array $templateArguments, bool $isNonEmpty = false): ArrayDefaultT|ArrayT
285+
private function list(array $templateArguments, bool $isNonEmpty = false): ListT
286+
{
287+
return match ($number = \count($templateArguments)) {
288+
0 => new ListT(isNonEmpty: $isNonEmpty),
289+
1 => new ListT(valueType: $templateArguments[0], isNonEmpty: $isNonEmpty),
290+
default => throw new \LogicException(\sprintf('list type should have at most 1 type arguments, got %d', $number)),
291+
};
292+
}
293+
294+
/**
295+
* @param list<Type> $templateArguments
296+
*/
297+
private function array(array $templateArguments, bool $isNonEmpty = false): ArrayDefaultT|ArrayT
256298
{
257299
return match ($number = \count($templateArguments)) {
258300
0 => $isNonEmpty ? new ArrayT(isNonEmpty: true) : arrayT,
@@ -261,4 +303,17 @@ private function parseArray(array $templateArguments, bool $isNonEmpty = false):
261303
default => throw new \LogicException(\sprintf('array type should have at most 2 type arguments, got %d', $number)),
262304
};
263305
}
306+
307+
/**
308+
* @param list<Type> $templateArguments
309+
*/
310+
private function iterable(array $templateArguments): IterableDefaultT|IterableT
311+
{
312+
return match ($number = \count($templateArguments)) {
313+
0 => iterableT,
314+
1 => new IterableT(valueType: $templateArguments[0]),
315+
2 => new IterableT(keyType: $templateArguments[0], valueType: $templateArguments[1]),
316+
default => throw new \LogicException(\sprintf('iterable type should have at most 2 type arguments, got %d', $number)),
317+
};
318+
}
264319
}

0 commit comments

Comments
 (0)