8000 bug #60641 [TypeInfo] Fix type alias resolving (mtarld) · symfony/symfony@70ccb7e · GitHub
[go: up one dir, main page]

Skip to content

Commit 70ccb7e

Browse files
bug #60641 [TypeInfo] Fix type alias resolving (mtarld)
This PR was merged into the 7.3 branch. Discussion ---------- [TypeInfo] Fix type alias resolving | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fixes #60598, fixes #60657 | License | MIT Take other type aliases into account when resolving type aliases, no matter the order they're defined. Commits ------- b778a80 [TypeInfo] Fix type alias resolving
2 parents effb4a7 + b778a80 commit 70ccb7e

File tree

3 files changed

+125
-13
lines changed

3 files changed

+125
-13
lines changed

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

Lines changed: 26 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,31 @@ final class DummyWithTypeAliases
5357
public mixed $psalmOtherAliasedExternalAlias;
5458
}
5559

60+
/**
61+
* @phpstan-type Foo = array{0: Bar}
62+
* @phpstan-type Bar = array{0: Foo}
63+
*/
64+
final class DummyWithRecursiveTypeAliases
65+
{
66+
}
67+
68+
/**
69+
* @phpstan-type Invalid = SomethingInvalid
70+
*/
71+
final class DummyWithInvalidTypeAlias
72+
{
73+
}
74+
5675
/**
5776
* @phpstan-import-type Invalid from DummyWithTypeAliases
5877
*/
5978
final class DummyWithInvalidTypeAliasImport
6079
{
6180
}
81+
82+
/**
83+
* @phpstan-import-type Invalid from int
84+
*/
85+
final class DummyWithTypeAliasImportedFromInvalidClassName
86+
{
87+
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
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;
20+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithRecursiveTypeAliases;
1921
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
2022
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
23+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliasImportedFromInvalidClassName;
2124
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
2225
use Symfony\Component\TypeInfo\Type;
2326
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
@@ -128,27 +131,33 @@ public function testCollectTypeAliases()
128131
$this->assertEquals([
129132
'CustomString' => Type::string(),
130133
'CustomInt' => Type::int(),
134+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
131135
'AliasedCustomInt' => Type::int(),
132136
'PsalmCustomString' => Type::string(),
133137
'PsalmCustomInt' => Type::int(),
138+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
134139
'PsalmAliasedCustomInt' => Type::int(),
135140
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);
136141

137142
$this->assertEquals([
138143
'CustomString' => Type::string(),
139144
'CustomInt' => Type::int(),
145+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
140146
'AliasedCustomInt' => Type::int(),
141147
'PsalmCustomString' => Type::string(),
142148
'PsalmCustomInt' => Type::int(),
149+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
143150
'PsalmAliasedCustomInt' => Type::int(),
144151
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);
145152

146153
$this->assertEquals([
147154
'CustomString' => Type::string(),
148155
'CustomInt' => Type::int(),
156+
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
149157
'AliasedCustomInt' => Type::int(),
150158
'PsalmCustomString' => Type::string(),
151159
'PsalmCustomInt' => Type::int(),
160+
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
152161
'PsalmAliasedCustomInt' => Type::int(),
153162
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
154163
}
@@ -167,4 +176,28 @@ public function testThrowWhenImportingInvalidAlias()
167176

168177
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
169178
}
179+
180+
public function testThrowWhenCannotResolveTypeAlias()
181+
{
182+
$this->expectException(LogicException::class);
183+
$this->expectExceptionMessage('Cannot resolve "Invalid" type alias.');
184+
185+
$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAlias::class);
186+
}
187+
188+
public function testThrowWhenTypeAliasNotImportedFromValidClassName()
189+
{
190+
$this->expectException(LogicException::class);
191+
$this->expectExceptionMessage('Type alias "Invalid" is not imported from a valid class name.');
192+
193+
$this->typeContextFactory->createFromClassName(DummyWithTypeAliasImportedFromInvalidClassName::class);
194+
}
195+
196+
public function testThrowWhenImportingRecursiveTypeAliases()
197+
{
198+
$this->expectException(LogicException::class);
199+
$this->expectExceptionMessage('Cannot resolve "Bar" type alias.');
200+
201+
$this->typeContextFactory->createFromClassName(DummyWithRecursiveTypeAliases::class)->typeAliases;
202+
}
170203
}

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