Skip to content

[TypeInfo] Fix type alias resolving #60641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[TypeInfo] Fix type alias resolving
  • Loading branch information
mtarld committed Jun 4, 2025
commit b778a80c81b2cc4f22fb3f4ff2cde17b9c2a9b63
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
namespace Symfony\Component\TypeInfo\Tests\Fixtures;

/**
* @phpstan-type CustomArray = array{0: CustomInt, 1: CustomString, 2: bool}
* @phpstan-type CustomString = string
*
* @phpstan-import-type CustomInt from DummyWithPhpDoc
* @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt
*
* @psalm-type PsalmCustomArray = array{0: PsalmCustomInt, 1: PsalmCustomString, 2: bool}
* @psalm-type PsalmCustomString = string
*
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc
* @psalm-import-type PsalmCustomInt from DummyWithPhpDoc as PsalmAliasedCustomInt
*/
Expand Down Expand Up @@ -53,9 +57,31 @@ final class DummyWithTypeAliases
public mixed $psalmOtherAliasedExternalAlias;
}

/**
* @phpstan-type Foo = array{0: Bar}
* @phpstan-type Bar = array{0: Foo}
*/
final class DummyWithRecursiveTypeAliases
{
}

/**
* @phpstan-type Invalid = SomethingInvalid
*/
final class DummyWithInvalidTypeAlias
{
}

/**
* @phpstan-import-type Invalid from DummyWithTypeAliases
*/
final class DummyWithInvalidTypeAliasImport
{
}

/**
* @phpstan-import-type Invalid from int
*/
final class DummyWithTypeAliasImportedFromInvalidClassName
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
use Symfony\Component\TypeInfo\Exception\LogicException;
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAlias;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithInvalidTypeAliasImport;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithRecursiveTypeAliases;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliasImportedFromInvalidClassName;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
Expand Down Expand Up @@ -128,27 +131,33 @@ public function testCollectTypeAliases()
$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases);

$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases);

$this->assertEquals([
'CustomString' => Type::string(),
'CustomInt' => Type::int(),
'CustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'AliasedCustomInt' => Type::int(),
'PsalmCustomString' => Type::string(),
'PsalmCustomInt' => Type::int(),
'PsalmCustomArray' => Type::arrayShape([0 => Type::int(), 1 => Type::string(), 2 => Type::bool()]),
'PsalmAliasedCustomInt' => Type::int(),
], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases);
}
Expand All @@ -167,4 +176,28 @@ public function testThrowWhenImportingInvalidAlias()

$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class);
}

public function testThrowWhenCannotResolveTypeAlias()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot resolve "Invalid" type alias.');

$this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAlias::class);
}

public function testThrowWhenTypeAliasNotImportedFromValidClassName()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Type alias "Invalid" is not imported from a valid class name.');

$this->typeContextFactory->createFromClassName(DummyWithTypeAliasImportedFromInvalidClassName::class);
}

public function testThrowWhenImportingRecursiveTypeAliases()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot resolve "Bar" type alias.');

$this->typeContextFactory->createFromClassName(DummyWithRecursiveTypeAliases::class)->typeAliases;
}
}
79 changes: 66 additions & 13 deletions src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,32 +199,85 @@ private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $t
}

$aliases = [];
foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) {
if (!$tag->value instanceof TypeAliasTagValueNode) {
$resolvedAliases = [];

foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) {
if (!$tag->value instanceof TypeAliasImportTagValueNode) {
continue;
}

$aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext);
$importedFromType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext);
if (!$importedFromType instanceof ObjectType) {
throw new LogicException(\sprintf('Type alias "%s" is not imported from a valid class name.', $tag->value->importedAlias));
}

$importedFromContext = $this->createFromClassName($importedFromType->getClassName());

$typeAlias = $importedFromContext->typeAliases[$tag->value->importedAlias] ?? null;
if (!$typeAlias) {
throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedFromType->getClassName()));
}

$resolvedAliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
}

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

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

$typeAlias = $importedTypeContext->typeAliases[$tag->value->importedAlias] ?? null;
if (!$typeAlias) {
throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedType->getClassName()));
return $this->resolveTypeAliases($aliases, $resolvedAliases, $typeContext);
}

/**
* @param array<string, string> $toResolve
* @param array<string, Type> $resolved
*
* @return array<string, Type>
*/
private function resolveTypeAliases(array $toResolve, array $resolved, TypeContext $typeContext): array
{
if (!$toResolve) {
return [];
}

$typeContext = new TypeContext(
$typeContext->calledClassName,
$typeContext->declaringClassName,
$typeContext->namespace,
$typeContext->uses,
$typeContext->templates,
$typeContext->typeAliases + $resolved,
);

$succeeded = false;
$lastFailure = null;
$lastFailingAlias = null;

foreach ($toResolve as $alias => $type) {
try {
$resolved[$alias] = $this->stringTypeResolver->resolve($type, $typeContext);
unset($toResolve[$alias]);
$succeeded = true;
} catch (UnsupportedException $lastFailure) {
$lastFailingAlias = $alias;
}
}

// nothing has succeeded, the result won't be different from the
// previous one, we can stop here.
if (!$succeeded) {
throw new LogicException(\sprintf('Cannot resolve "%s" type alias.', $lastFailingAlias), 0, $lastFailure);
}

$aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias;
if ($toResolve) {
return $this->resolveTypeAliases($toResolve, $resolved, $typeContext);
}

return $aliases;
return $resolved;
}

private function getPhpDocNode(string $rawDocNode): PhpDocNode
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy