8000 [TypeInfo] Fix type alias resolving · symfony/symfony@cde4768 · GitHub
[go: up one dir, main page]

Skip to content

Commit cde4768

Browse files
committed
[TypeInfo] Fix type alias resolving
1 parent b2a1379 commit cde4768

File tree

3 files changed

+108
-13
lines changed

3 files changed

+108
-13
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
1313

1414
/**
15+
* @phpstan-type CustomArray = array{0: CustomInt, 1: CustomString, 2: bool}
1516
* @phpstan-type CustomString = string
17+
*
1618
* @phpstan-import-type CustomInt from DummyWithPhpDoc
1719
* @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt
1820
*
21+
* @psalm-type PsalmCustomArray = array{0: PsalmCustomInt, 1: PsalmCustomString, 2: bool}
1922
* @psalm-type PsalmCustomString = string
23+
*
2024
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc
2125
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc as PsalmAliasedCustomInt
2226
*/
@@ -53,9 +57,23 @@ final class DummyWithTypeAliases
5357
public mixed $psalmOtherAliasedExternalAlias;
5458
}
5559

60+
/**
61+
* @phpstan-type Invalid = SomethingInvalid
62+
*/
63+
final class DummyWithInvalidTypeAlias
64+
{
65+
}
66+
5667
/**
5768
* @phpstan-import-type Invalid from DummyWithTypeAliases
5869
*/
5970
final class DummyWithInvalidTypeAliasImport
6071
{
6172
}
73+
74+
/**
75+
* @phpstan-import-type Invalid from int
76+
*/
77+
final class DummyWithTypeAliasImportedFromInvalidClassName
78+
{
79+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
use Symfony\Component\TypeInfo\Exception\LogicException;
1616
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
1717
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
18+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAlias;
1819
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAliasImport;
1920
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
2021
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
22+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliasImportedFromInvalidClassName;
2123
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
2224
use Symfony\Component\TypeInfo\Type;
2325
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
@@ -128,27 +130,33 @@ public function testCollectTypeAliases()
128130
$this->assertEquals([
129131
'CustomString' => Type::string(),
130132
'CustomInt' => Type::int(),
133+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
131134
'AliasedCustomInt' => Type::int(),
132135
'PsalmCustomString' => Type::string(),
133136
'PsalmCustomInt' => Type::int(),
137+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
134138
'PsalmAliasedCustomInt' => Type::int(),
135139
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
136140

137141
$this->assertEquals([
138142
'CustomString' => Type::string(),
139143
'CustomInt' => Type::int(),
144+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
140145
'AliasedCustomInt' => Type::int(),
141146
'PsalmCustomString' => Type::string(),
142147
'PsalmCustomInt' => Type::int(),
148+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
143149
'PsalmAliasedCustomInt' => Type::int(),
144150
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);
145151

146152
$this->assertEquals([
147153
'CustomString' => Type::string(),
148154
'CustomInt' => Type::int(),
155+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
149156
'AliasedCustomInt' => Type::int(),
150157
'PsalmCustomString' => Type::string(),
151158
'PsalmCustomInt' => Type::int(),
159+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
152160
'PsalmAliasedCustomInt' => Type::int(),
153161
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
154162
}
@@ -167,4 +175,20 @@ public function testThrowWhenImportingInvalidAlias()
167175

168176
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
169177
}
178+
179+
public function testThrowWhenCannotResolveTypeAlias()
180+
{
181+
$this->expectException(LogicException::class);
182+
$this->expectExceptionMessage('Cannot resolve "Invalid" type alias.');
183+
184+
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAlias::class);
185+
}
186+
187+
public function testThrowWhenTypeAliasNotImportedFromValidClassName()
188+
{
189+
$this->expectException(LogicException::class);
190+
$this->expectExceptionMessage('Type alias "Invalid" is not imported from a valid class name.');
191+
192+
$this->typeContextFactory->createFromClassName(DummyWithTypeAliasImportedFromInvalidClassName::class);
193+
}
170194
}

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

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,32 +199,85 @@ private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $t
199199
}
200200

201201
$aliases = [];
202-
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
203-
if (!$tag->value instanceof TypeAliasTagValueNode) {
202+
$resolvedAliases = [];
203+
204+
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
205+
if (!$tag->value instanceof TypeAliasImportTagValueNode) {
204206
continue;
205207
}
206208

207-
$aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext);
209+
$importedFromType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
210+
if (!$importedFromType instanceof ObjectType) {
211+
throw new LogicException(\sprintf('Type alias "%s" is not imported from a valid class name.', $tag->value->importedAlias));
212+
}
213+
214+
$importedFromContext = $this->createFromClassName($importedFromType->getClassName());
215+
216+
$typeAlias = $importedFromContext->typeAliases[$tag->value->importedAlias] ?? null;
217+
if (!$typeAlias) {
218+
throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedFromType->getClassName()));
219+
}
220+
221+
$resolvedAliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
208222
}
209223

210-
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
211-
if (!$tag->value instanceof TypeAliasImportTagValueNode) {
224+
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
225+
if (!$tag->value instanceof TypeAliasTagValueNode) {
212226
continue;
213227
}
214228

215-
/** @var ObjectType $importedType */
216-
$importedType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
217-
$importedTypeContext = $this->createFromClassName($importedType->getClassName());
229+
$aliases[$tag->value->alias] = (string) $tag->value->type;
230+
}
218231

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()));
232+
return $this->resolveTypeAliases($aliases, $resolvedAliases, $typeContext);
233+
}
234+
235+
/**
236+
* @param array<string, string> $toResolve
237+
* @param array<string, Type> $resolved
238+
*
239+
* @return array<string, Type>
240+
*/
241+
private function resolveTypeAliases(array $toResolve, array $resolved, TypeContext $typeContext): array
242+
{
243+
if ([] === $toResolve) {
244+
return [];
245+
}
246+
247+
$typeContext = new TypeContext(
248+
$typeContext->calledClassName,
249+
$typeContext->declaringClassName,
250+
$typeContext->namespace,
251+
$typeContext->uses,
252+
$typeContext->templates,
253+
$typeContext->typeAliases + $resolved,
254+
);
255+
256+
$succeeded = false;
257+
$lastFailure = null;
258+
$lastFailingAlias = null;
259+
260+
foreach ($toResolve as $alias => $type) {
261+
try {
262+
$resolved[$alias] = $this->stringTypeResolver->resolve($type, $typeContext);
263+
unset($toResolve[$alias]);
264+
$succeeded = true;
265+
} catch (UnsupportedException $lastFailure) {
266+
$lastFailingAlias = $alias;
222267
}
268+
}
269+
270+
// nothing has succeeded, the result won't be different from the
271+
// previous one, we can stop here.
272+
if (!$succeeded) {
273+
throw new LogicException(\sprintf('Cannot resolve "%s" type alias.', $lastFailingAlias), 0, $lastFailure);
274+
}
223275

224-
$aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
276+
if ([] !== $toResolve) {
277+
return $this->resolveTypeAliases($toResolve, $resolved, $typeContext);
225278
}
226279

227-
return $aliases;
280+
return $resolved;
228281
}
229282

230283
private function getPhpDocNode(string $rawDocNode): PhpDocNode

0 commit comments

Comments
 (0)
0