Skip to content

Commit 34dbc2f

Browse files
committed
[TypeInfo] Add type alias support
1 parent ab6c611 commit 34dbc2f

File tree

9 files changed

+175
-18
lines changed

9 files changed

+175
-18
lines changed

src/Symfony/Component/TypeInfo/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `TypeFactoryTrait::fromValue()` method
99
* Deprecate constructing a `CollectionType` instance as a list that is not an array
1010
* Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead
11+
* Add type alias support in `TypeContext` and `StringTypeResolver`
1112

1213
7.2
1314
---

src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php

+8
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22

33
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
44

5+
/**
6+
* @phpstan-type CustomInt int
7+
*/
58
final class DummyWithPhpDoc
69
{
710
/**
811
* @var array<Dummy>
912
*/
1013
public mixed $arrayOfDummies = [];
1114

15+
/**
16+
* @var CustomInt
17+
*/
18+
public mixed $aliasedInt;
19+
1220
/**
1321
* @param bool $promoted
1422
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
4+
5+
/**
6+
* @phpstan-type CustomString string
7+
* @phpstan-import-type CustomInt from DummyWithPhpDoc
8+
* @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt
9+
*/
10+
final class DummyWithTypeAliases
11+
{
12+
/**
13+
* @var Local
14+
*/
15+
public mixed $localAlias;
16+
17+
/**
18+
* @var CustomInt
19+
*/
20+
public mixed $externalAlias;
21+
22+
/**
23+
* @var AliasedCustomInt
24+
*/
25+
public mixed $aliasedExternalAlias;
26+
}
27+
28+
/**
29+
* @phpstan-import-type Invalid from DummyWithTypeAliases
30+
*/
31+
final class DummyWithInvalidTypeAliasImport
32+
{
33+
34+
}

src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php

+39
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
namespace Symfony\Component\TypeInfo\Tests\TypeContext;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\TypeInfo\Exception\LogicException;
1516
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
1617
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
18+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAliasImport;
1719
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
20+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
1821
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
1922
use Symfony\Component\TypeInfo\Type;
2023
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
@@ -119,4 +122,40 @@ public function testDoNotCollectTemplatesWhenToStringTypeResolver()
119122

120123
$this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTemplates::class)->templates);
121124
}
125+
126+
public function testCollectTypeAliases()
127+
{
128+
$this->assertEquals([
129+
'CustomString' => Type::string(),
130+
'CustomInt' => Type::int(),
131+
'AliasedCustomInt' => Type::int(),
132+
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
133+
134+
$this->assertEquals([
135+
'CustomString' => Type::string(),
136+
'CustomInt' => Type::int(),
137+
'AliasedCustomInt' => Type::int(),
138+
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);
139+
140+
$this->assertEquals([
141+
'CustomString' => Type::string(),
142+
'CustomInt' => Type::int(),
143+
'AliasedCustomInt' => Type::int(),
144+
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
145+
}
146+
147+
public function testDoNotCollectTypeAliasesWhenToStringTypeResolver()
148+
{
149+
$typeContextFactory = new TypeContextFactory();
150+
151+
$this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
152+
}
153+
154+
public function testThrowWhenImportingInvalidAlias()
155+
{
156+
$this->expectException(LogicException::class);
157+
$this->expectExceptionMessage(\sprintf('Cannot find any "Invalid" type alias in "%s".', DummyWithTypeAliases::class));
158+
159+
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
160+
}
122161
}

src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php

+19-6
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,28 @@
2222

2323
class PhpDocAwareReflectionTypeResolverTest extends TestCase
2424
{
25-
public function testReadPhpDoc()
25+
/**
26+
* @dataProvider readPhpDocDataProvider
27+
*/
28+
public function testReadPhpDoc(Type $expected, \Reflector $reflector)
29+
{
30+
$resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory(new StringTypeResolver()));
31+
32+
$this->assertEquals($expected, $resolver->resolve($reflector));
33+
}
34+
35+
/**
36+
* @return iterable<array{0: Type, 1: \Reflector}>
37+
*/
38+
public static function readPhpDocDataProvider(): iterable
2639
{
27-
$resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory());
2840
$reflection = new \ReflectionClass(DummyWithPhpDoc::class);
2941

30-
$this->assertEquals(Type::array(Type::object(Dummy::class)), $resolver->resolve($reflection->getProperty('arrayOfDummies')));
31-
$this->assertEquals(Type::bool(), $resolver->resolve($reflection->getProperty('promoted')));
32-
$this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')));
33-
$this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0]));
42+
yield [Type::array(Type::object(Dummy::class)), $reflection->getProperty('arrayOfDummies')];
43+
yield [Type::bool(), $reflection->getProperty('promoted')];
44+
yield [Type::object(Dummy::class), $reflection->getMethod('getNextDummy')];
45+
yield [Type::object(Dummy::class), $reflection->getMethod('getNextDummy')->getParameters()[0]];
46+
yield [Type::int(), $reflection->getProperty('aliasedInt')];
3447
}
3548

3649
public function testFallbackWhenNoPhpDoc()

src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
2121
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
2222
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
23+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
2324
use Symfony\Component\TypeInfo\Type;
2425
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
2526
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
@@ -175,6 +176,10 @@ public static function resolveDataProvider(): iterable
175176
yield [Type::collection(Type::object(\IteratorAggregate::class), Type::string()), \IteratorAggregate::class.'<string>'];
176177
yield [Type::collection(Type::object(\IteratorAggregate::class), Type::bool(), Type::string()), \IteratorAggregate::class.'<string, bool>'];
177178
yield [Type::collection(Type::object(DummyCollection::class), Type::bool(), Type::string()), DummyCollection::class.'<string, bool>'];
179+
180+
// type aliases
181+
yield [Type::int(), 'CustomInt', $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)];
182+
yield [Type::string(), 'CustomString', $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)];
178183
}
179184

180185
public function testCannotResolveNonStringType()

src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php

+2
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ final class TypeContext
3333
/**
3434
* @param array<string, string> $uses
3535
* @param array<string, Type> $templates
36+
* @param array<string, Type> $typeAliases
3637
*/
3738
public function __construct(
3839
public readonly string $calledClassName,
3940
public readonly string $declaringClassName,
4041
public readonly ?string $namespace = null,
4142
public readonly array $uses = [],
4243
public readonly array $templates = [],
44+
public readonly array $typeAliases = [],
4345
) {
4446
}
4547

src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php

+63-12
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@
1111

1212
namespace Symfony\Component\TypeInfo\TypeContext;
1313

14+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1415
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
17+
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
1518
use PHPStan\PhpDocParser\Lexer\Lexer;
1619
use PHPStan\PhpDocParser\Parser\ConstExprParser;
1720
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1821
use PHPStan\PhpDocParser\Parser\TokenIterator;
1922
use PHPStan\PhpDocParser\Parser\TypeParser;
2023
use PHPStan\PhpDocParser\ParserConfig;
24+
use Symfony\Component\TypeInfo\Exception\LogicException;
2125
use Symfony\Component\TypeInfo\Exception\RuntimeException;
2226
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
2327
use Symfony\Component\TypeInfo\Type;
28+
use Symfony\Component\TypeInfo\Type\ObjectType;
2429
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
2530

2631
/**
@@ -66,6 +71,7 @@ public function createFromClassName(string $calledClassName, ?string $declaringC
6671
$typeContext->namespace,
6772
$typeContext->uses,
6873
$this->collectTemplates($declaringClassReflection, $typeContext),
74+
$this->collectTypeAliases($declaringClassReflection, $typeContext),
6975
);
7076
}
7177

@@ -103,6 +109,7 @@ public function createFromReflection(\Reflector $reflection): ?TypeContext
103109
$typeContext->namespace,
104110
$typeContext->uses,
105111
$templates,
112+
$this->collectTypeAliases($declaringClassReflection, $typeContext),
106113
);
107114
}
108115

@@ -156,19 +163,8 @@ private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $
156163
return [];
157164
}
158165

159-
if (class_exists(ParserConfig::class)) {
160-
$config = new ParserConfig([]);
161-
$this->phpstanLexer ??= new Lexer($config);
162-
$this->phpstanParser ??= new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config));
163-
} else {
164-
$this->phpstanLexer ??= new Lexer();
165-
$this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
166-
}
167-
168-
$tokens = new TokenIterator($this->phpstanLexer->tokenize($rawDocNode));
169-
170166
$templates = [];
171-
foreach ($this->phpstanParser->parse($tokens)->getTagsByName('@template') as $tag) {
167+
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@template') as $tag) {
172168
if (!$tag->value instanceof TemplateTagValueNode) {
173169
continue;
174170
}
@@ -188,4 +184,59 @@ private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $
188184

189185
return $templates;
190186
}
187+
188+
/**
189+
* @return array<string, Type>
190+
*/
191+
private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $typeContext): array
192+
{
193+
if (!$this->stringTypeResolver || !class_exists(PhpDocParser::class)) {
194+
return [];
195+
}
196+
197+
if (!$rawDocNode = $reflection->getDocComment()) {
198+
return [];
199+
}
200+
201+
$aliases = [];
202+
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
203+
if (!$tag->value instanceof TypeAliasTagValueNode) {
204+
continue;
205+
}
206+
207+
$aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext);
208+
}
209+
210+
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
211+
if (!$tag->value instanceof TypeAliasImportTagValueNode) {
212+
continue;
213+
}
214+
215+
/** @var ObjectType $importedType */
216+
$importedType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
217+
$importedTypeContext = $this->createFromClassName($importedType->getClassName());
218+
219+
$typeAlias = $importedTypeContext->typeAliases[$tag->value->importedAlias] ?? null;
220+
if (!$typeAlias) {
221+
throw new LogicException(sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedType->getClassName()));
222+
}
223+
224+
$aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
225+
}
226+
227+
return $aliases;
228+
}
229+
230+
private function getPhpDocNode(string $rawDocNode): PhpDocNode
231+
{
232+
if (class_exists(ParserConfig::class)) {
233+
$this->phpstanLexer ??= new Lexer($config = new ParserConfig([]));
234+
$this->phpstanParser ??= new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config));
235+
} else {
236+
$this->phpstanLexer ??= new Lexer();
237+
$this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
238+
}
239+
240+
return $this->phpstanParser->parse(new TokenIterator($this->phpstanLexer->tokenize($rawDocNode)));
241+
}
191242
}

src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php

+4
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC
275275
return Type::template($identifier, $typeContext->templates[$identifier]);
276276
}
277277

278+
if (isset($typeContext?->typeAliases[$identifier])) {
279+
return $typeContext->typeAliases[$identifier];
280+
}
281+
278282
throw new \DomainException(\sprintf('Unhandled "%s" identifier.', $identifier));
279283
}
280284
}

0 commit comments

Comments
 (0)