diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 2dfd862c9f77d..5eaf445c6b8a0 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add `TypeFactoryTrait::fromValue()` method * Deprecate constructing a `CollectionType` instance as a list that is not an array * Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead + * Add type alias support in `TypeContext` and `StringTypeResolver` 7.2 --- diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php index 9dd5a2dc28b54..774ff2b5d0b89 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; abstract class AbstractDummy diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/Dummy.php index ba9209d942e5e..d634f37747a7f 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/Dummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; final class Dummy extends AbstractDummy diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php index 2348415910314..2b2482481d0b9 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; enum DummyBackedEnum: string diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyCollection.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyCollection.php index a12c76b3c6f73..acf832a337a99 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyCollection.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyCollection.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; final class DummyCollection implements \IteratorAggregate diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyEnum.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyEnum.php index 22bf513f484fd..11beb364f03e5 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyEnum.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyEnum.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; enum DummyEnum diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyExtendingStdClass.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyExtendingStdClass.php index 4940dc1fd04c6..411ee94ecf2e3 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyExtendingStdClass.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyExtendingStdClass.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; final class DummyExtendingStdClass extends \stdClass diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php index 30141f95c3d0f..0063dd1ba944e 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php @@ -1,7 +1,20 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; +/** + * @phpstan-type CustomInt = int + * @psalm-type PsalmCustomInt = int + */ final class DummyWithPhpDoc { /** @@ -9,6 +22,11 @@ final class DummyWithPhpDoc */ public mixed $arrayOfDummies = []; + /** + * @var CustomInt + */ + public mixed $aliasedInt; + /** * @param bool $promoted */ diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTemplates.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTemplates.php index 09d9666410248..2c26616ce6d6e 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTemplates.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTemplates.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; /** diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php new file mode 100644 index 0000000000000..0b65137e4cdda --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithTypeAliases.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\Fixtures; + +/** + * @phpstan-type CustomString = string + * @phpstan-import-type CustomInt from DummyWithPhpDoc + * @phpstan-import-type CustomInt from DummyWithPhpDoc as AliasedCustomInt + * + * @psalm-type PsalmCustomString = string + * @psalm-import-type PsalmCustomInt from DummyWithPhpDoc + * @psalm-import-type PsalmCustomInt from DummyWithPhpDoc as PsalmAliasedCustomInt + */ +final class DummyWithTypeAliases +{ + /** + * @var CustomString + */ + public mixed $localAlias; + + /** + * @var CustomInt + */ + public mixed $externalAlias; + + /** + * @var AliasedCustomInt + */ + public mixed $aliasedExternalAlias; + + /** + * @var PsalmCustomString + */ + public mixed $psalmLocalAlias; + + /** + * @var PsalmCustomInt + */ + public mixed $psalmExternalAlias; + + /** + * @var PsalmAliasedCustomInt + */ + public mixed $psalmOtherAliasedExternalAlias; +} + +/** + * @phpstan-import-type Invalid from DummyWithTypeAliases + */ +final class DummyWithInvalidTypeAliasImport +{ +} diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php index 58517a4bd0428..868ec8250e5c5 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; use Symfony\Component\TypeInfo\Type; diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php index 7e7fa271e4782..018fd36b6b9d2 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\TypeInfo\Tests\Fixtures; final class ReflectionExtractableDummy extends AbstractDummy diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php index 1de82676b0334..e7794e4f114b6 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php @@ -12,9 +12,12 @@ namespace Symfony\Component\TypeInfo\Tests\TypeContext; use PHPUnit\Framework\TestCase; +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\DummyWithInvalidTypeAliasImport; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; @@ -119,4 +122,49 @@ public function testDoNotCollectTemplatesWhenToStringTypeResolver() $this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTemplates::class)->templates); } + + public function testCollectTypeAliases() + { + $this->assertEquals([ + 'CustomString' => Type::string(), + 'CustomInt' => Type::int(), + 'AliasedCustomInt' => Type::int(), + 'PsalmCustomString' => Type::string(), + 'PsalmCustomInt' => Type::int(), + 'PsalmAliasedCustomInt' => Type::int(), + ], $this->typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases); + + $this->assertEquals([ + 'CustomString' => Type::string(), + 'CustomInt' => Type::int(), + 'AliasedCustomInt' => Type::int(), + 'PsalmCustomString' => Type::string(), + 'PsalmCustomInt' => Type::int(), + 'PsalmAliasedCustomInt' => Type::int(), + ], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTypeAliases::class))->typeAliases); + + $this->assertEquals([ + 'CustomString' => Type::string(), + 'CustomInt' => Type::int(), + 'AliasedCustomInt' => Type::int(), + 'PsalmCustomString' => Type::string(), + 'PsalmCustomInt' => Type::int(), + 'PsalmAliasedCustomInt' => Type::int(), + ], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTypeAliases::class, 'localAlias'))->typeAliases); + } + + public function testDoNotCollectTypeAliasesWhenToStringTypeResolver() + { + $typeContextFactory = new TypeContextFactory(); + + $this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)->typeAliases); + } + + public function testThrowWhenImportingInvalidAlias() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage(\sprintf('Cannot find any "Invalid" type alias in "%s".', DummyWithTypeAliases::class)); + + $this->typeContextFactory->createFromClassName(DummyWithInvalidTypeAliasImport::class); + } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php index 7e92638a9ce38..cae8b7044c52b 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php @@ -22,15 +22,28 @@ class PhpDocAwareReflectionTypeResolverTest extends TestCase { - public function testReadPhpDoc() + /** + * @dataProvider readPhpDocDataProvider + */ + public function testReadPhpDoc(Type $expected, \Reflector $reflector) + { + $resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory(new StringTypeResolver())); + + $this->assertEquals($expected, $resolver->resolve($reflector)); + } + + /** + * @return iterable + */ + public static function readPhpDocDataProvider(): iterable { - $resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory()); $reflection = new \ReflectionClass(DummyWithPhpDoc::class); - $this->assertEquals(Type::array(Type::object(Dummy::class)), $resolver->resolve($reflection->getProperty('arrayOfDummies'))); - $this->assertEquals(Type::bool(), $resolver->resolve($reflection->getProperty('promoted'))); - $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy'))); - $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0])); + yield [Type::array(Type::object(Dummy::class)), $reflection->getProperty('arrayOfDummies')]; + yield [Type::bool(), $reflection->getProperty('promoted')]; + yield [Type::object(Dummy::class), $reflection->getMethod('getNextDummy')]; + yield [Type::object(Dummy::class), $reflection->getMethod('getNextDummy')->getParameters()[0]]; + yield [Type::int(), $reflection->getProperty('aliasedInt')]; } public function testFallbackWhenNoPhpDoc() diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index 9320987c6baed..bbc1ffc93b738 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -20,6 +20,7 @@ use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContext; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; @@ -175,6 +176,10 @@ public static function resolveDataProvider(): iterable yield [Type::collection(Type::object(\IteratorAggregate::class), Type::string()), \IteratorAggregate::class.'']; yield [Type::collection(Type::object(\IteratorAggregate::class), Type::bool(), Type::string()), \IteratorAggregate::class.'']; yield [Type::collection(Type::object(DummyCollection::class), Type::bool(), Type::string()), DummyCollection::class.'']; + + // type aliases + yield [Type::int(), 'CustomInt', $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)]; + yield [Type::string(), 'CustomString', $typeContextFactory->createFromClassName(DummyWithTypeAliases::class)]; } public function testCannotResolveNonStringType() diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php index 594c17e6ac9a8..8175fd80fb306 100644 --- a/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php +++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php @@ -33,6 +33,7 @@ final class TypeContext /** * @param array $uses * @param array $templates + * @param array $typeAliases */ public function __construct( public readonly string $calledClassName, @@ -40,6 +41,7 @@ public function __construct( public readonly ?string $namespace = null, public readonly array $uses = [], public readonly array $templates = [], + public readonly array $typeAliases = [], ) { } diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php index 8cf405bd76696..d268c85fe49b0 100644 --- a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php +++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php @@ -11,16 +11,21 @@ namespace Symfony\Component\TypeInfo\TypeContext; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; +use Symfony\Component\TypeInfo\Exception\LogicException; use Symfony\Component\TypeInfo\Exception\RuntimeException; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; /** @@ -66,6 +71,7 @@ public function createFromClassName(string $calledClassName, ?string $declaringC $typeContext->namespace, $typeContext->uses, $this->collectTemplates($declaringClassReflection, $typeContext), + $this->collectTypeAliases($declaringClassReflection, $typeContext), ); } @@ -103,6 +109,7 @@ public function createFromReflection(\Reflector $reflection): ?TypeContext $typeContext->namespace, $typeContext->uses, $templates, + $this->collectTypeAliases($declaringClassReflection, $typeContext), ); } @@ -156,19 +163,8 @@ private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $ return []; } - if (class_exists(ParserConfig::class)) { - $config = new ParserConfig([]); - $this->phpstanLexer ??= new Lexer($config); - $this->phpstanParser ??= new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config)); - } else { - $this->phpstanLexer ??= new Lexer(); - $this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); - } - - $tokens = new TokenIterator($this->phpstanLexer->tokenize($rawDocNode)); - $templates = []; - foreach ($this->phpstanParser->parse($tokens)->getTagsByName('@template') as $tag) { + foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@template') as $tag) { if (!$tag->value instanceof TemplateTagValueNode) { continue; } @@ -188,4 +184,59 @@ private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $ return $templates; } + + /** + * @return array + */ + private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $typeContext): array + { + if (!$this->stringTypeResolver || !class_exists(PhpDocParser::class)) { + return []; + } + + if (!$rawDocNode = $reflection->getDocComment()) { + return []; + } + + $aliases = []; + foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) { + if (!$tag->value instanceof TypeAliasTagValueNode) { + continue; + } + + $aliases[$tag->value->alias] = $this->stringTypeResolver->resolve((string) $tag->value->type, $typeContext); + } + + foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) { + if (!$tag->value instanceof TypeAliasImportTagValueNode) { + continue; + } + + /** @var ObjectType $importedType */ + $importedType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext); + $importedTypeContext = $this->createFromClassName($importedType->getClassName()); + + $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())); + } + + $aliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias; + } + + return $aliases; + } + + private function getPhpDocNode(string $rawDocNode): PhpDocNode + { + if (class_exists(ParserConfig::class)) { + $this->phpstanLexer ??= new Lexer($config = new ParserConfig([])); + $this->phpstanParser ??= new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config)); + } else { + $this->phpstanLexer ??= new Lexer(); + $this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); + } + + return $this->phpstanParser->parse(new TokenIterator($this->phpstanLexer->tokenize($rawDocNode))); + } } diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index a172d388a8722..5cd0819bd8b76 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -275,6 +275,10 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC return Type::template($identifier, $typeContext->templates[$identifier]); } + if (isset($typeContext?->typeAliases[$identifier])) { + return $typeContext->typeAliases[$identifier]; + } + throw new \DomainException(\sprintf('Unhandled "%s" identifier.', $identifier)); } }