From c6698ced09455c68298a7da5137e589a4727f509 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 1 Jul 2024 16:00:19 +0200 Subject: [PATCH] [TypeInfo] Add PhpDocAwareReflectionTypeResolver --- .../FrameworkExtension.php | 15 +++- .../Extractor/ReflectionExtractor.php | 14 ++- src/Symfony/Component/TypeInfo/CHANGELOG.md | 5 ++ .../Tests/Fixtures/DummyWithPhpDoc.php | 21 +++++ .../PhpDocAwareReflectionTypeResolverTest.php | 44 ++++++++++ .../Tests/TypeResolver/TypeResolverTest.php | 17 ++-- .../PhpDocAwareReflectionTypeResolver.php | 87 +++++++++++++++++++ .../TypeResolver/StringTypeResolver.php | 11 +-- .../TypeInfo/TypeResolver/TypeResolver.php | 50 ++++++----- 9 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php create mode 100644 src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php create mode 100644 src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7cc67725ec461..04d45f59f9115 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -14,6 +14,7 @@ use Composer\InstalledVersions; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; +use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver; use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Types\ContextFactory; use PhpParser\Parser; @@ -1974,11 +1975,21 @@ private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpF if (ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/type-info'])) { $container->register('type_info.resolver.string', StringTypeResolver::class); + $container->register('type_info.resolver.reflection_parameter.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class) + ->setArguments([new Reference('type_info.resolver.reflection_parameter'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]); + $container->register('type_info.resolver.reflection_property.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class) + ->setArguments([new Reference('type_info.resolver.reflection_property'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]); + $container->register('type_info.resolver.reflection_return.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class) + ->setArguments([new Reference('type_info.resolver.reflection_return'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]); + /** @var ServiceLocatorArgument $resolversLocator */ $resolversLocator = $container->getDefinition('type_info.resolver')->getArgument(0); - $resolversLocator->setValues($resolversLocator->getValues() + [ + $resolversLocator->setValues([ 'string' => new Reference('type_info.resolver.string'), - ]); + \ReflectionParameter::class => new Reference('type_info.resolver.reflection_parameter.phpdoc_aware'), + \ReflectionProperty::class => new Reference('type_info.resolver.reflection_property.phpdoc_aware'), + \ReflectionFunctionAbstract::class => new Reference('type_info.resolver.reflection_return.phpdoc_aware'), + ] + $resolversLocator->getValues()); } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 953e33f04f27c..b97d846aa570a 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -24,6 +24,11 @@ use Symfony\Component\String\Inflector\InflectorInterface; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; @@ -102,7 +107,14 @@ public function __construct( $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags); $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags); $this->inflector = $inflector ?? new EnglishInflector(); - $this->typeResolver = TypeResolver::create(); + + $typeContextFactory = new TypeContextFactory(); + $this->typeResolver = TypeResolver::create([ + \ReflectionType::class => $reflectionTypeResolver = new ReflectionTypeResolver(), + \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory), + \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory), + \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory), + ]); $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes)); $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst); diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 6eb821cdebc51..c98ffeb4ac107 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add `PhpDocAwareReflectionTypeResolver` resolver + 7.1 --- diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php new file mode 100644 index 0000000000000..479ccfa2afc01 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php @@ -0,0 +1,21 @@ + + */ + public mixed $arrayOfDummies = []; + + /** + * @param Dummy $dummy + * + * @return Dummy + */ + public function getNextDummy(mixed $dummy): mixed + { + throw new \BadMethodCallException(sprintf('"%s" is not implemented.', __METHOD__)); + } +} diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php new file mode 100644 index 0000000000000..261fd19f18e96 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/PhpDocAwareReflectionTypeResolverTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\TypeResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithPhpDoc; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; +use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; + +class PhpDocAwareReflectionTypeResolverTest extends TestCase +{ + public function testReadPhpDoc() + { + $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::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy'))); + $this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0])); + } + + public function testFallbackWhenNoPhpDoc() + { + $resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory()); + $reflection = new \ReflectionClass(Dummy::class); + + $this->assertEquals(Type::int(), $resolver->resolve($reflection->getProperty('id'))); + $this->assertEquals(Type::int(), $resolver->resolve($reflection->getMethod('getId'))); + $this->assertEquals(Type::int(), $resolver->resolve($reflection->getMethod('setId')->getParameters()[0])); + } +} diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php index 3b778ab71c88d..1757a0ae0a685 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\TypeInfo\Tests\TypeResolver; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy; use Symfony\Component\TypeInfo\Type; @@ -38,7 +37,7 @@ public function testCannotFindResolver() $this->expectException(UnsupportedException::class); $this->expectExceptionMessage('Cannot find any resolver for "int" type.'); - $resolver = new TypeResolver(new ServiceLocator([])); + $resolver = TypeResolver::create([]); $resolver->resolve(1); } @@ -59,13 +58,13 @@ public function testUseProperResolver() $reflectionReturnTypeResolver = $this->createMock(TypeResolverInterface::class); $reflectionReturnTypeResolver->method('resolve')->willReturn(Type::template('REFLECTION_RETURN_TYPE')); - $resolver = new TypeResolver(new ServiceLocator([ - 'string' => fn () => $stringResolver, - \ReflectionType::class => fn () => $reflectionTypeResolver, - \ReflectionParameter::class => fn () => $reflectionParameterResolver, - \ReflectionProperty::class => fn () => $reflectionPropertyResolver, - \ReflectionFunctionAbstract::class => fn () => $reflectionReturnTypeResolver, - ])); + $resolver = TypeResolver::create([ + 'string' => $stringResolver, + \ReflectionType::class => $reflectionTypeResolver, + \ReflectionParameter::class => $reflectionParameterResolver, + \ReflectionProperty::class => $reflectionPropertyResolver, + \ReflectionFunctionAbstract::class => $reflectionReturnTypeResolver, + ]); $this->assertEquals(Type::template('STRING'), $resolver->resolve('foo')); $this->assertEquals( diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php new file mode 100644 index 0000000000000..5c6104afbb2e5 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/TypeResolver/PhpDocAwareReflectionTypeResolver.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\TypeResolver; + +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; +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 Symfony\Component\TypeInfo\Exception\UnsupportedException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeContext\TypeContext; +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; + +/** + * Resolves type on reflection prioriziting PHP documentation. + * + * @author Mathias Arlaud + * + * @internal + */ +final readonly class PhpDocAwareReflectionTypeResolver implements TypeResolverInterface +{ + public function __construct( + private TypeResolverInterface $reflectionTypeResolver, + private TypeResolverInterface $stringTypeResolver, + private TypeContextFactory $typeContextFactory, + private PhpDocParser $phpDocParser = new PhpDocParser(new TypeParser(), new ConstExprParser()), + private Lexer $lexer = new Lexer(), + ) { + } + + public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type + { + if (!$subject instanceof \ReflectionProperty && !$subject instanceof \ReflectionParameter && !$subject instanceof \ReflectionFunctionAbstract) { + throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionProperty", a "ReflectionParameter" or a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject); + } + + $docComment = match (true) { + $subject instanceof \ReflectionProperty => $subject->getDocComment(), + $subject instanceof \ReflectionParameter => $subject->getDeclaringFunction()->getDocComment(), + $subject instanceof \ReflectionFunctionAbstract => $subject->getDocComment(), + }; + + if (!$docComment) { + return $this->reflectionTypeResolver->resolve($subject); + } + + $typeContext ??= $this->typeContextFactory->createFromReflection($subject); + + $tagName = match (true) { + $subject instanceof \ReflectionProperty => '@var', + $subject instanceof \ReflectionParameter => '@param', + $subject instanceof \ReflectionFunctionAbstract => '@return', + }; + + $tokens = new TokenIterator($this->lexer->tokenize($docComment)); + $docNode = $this->phpDocParser->parse($tokens); + + foreach ($docNode->getTagsByName($tagName) as $tag) { + $tagValue = $tag->value; + + if ( + $tagValue instanceof VarTagValueNode + || $tagValue instanceof ParamTagValueNode && $tagName && '$'.$subject->getName() === $tagValue->parameterName + || $tagValue instanceof ReturnTagValueNode + ) { + return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); + } + } + + return $this->reflectionTypeResolver->resolve($subject); + } +} diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 3c97e397bb060..793eb394e9df0 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -58,13 +58,10 @@ final class StringTypeResolver implements TypeResolverInterface */ private static array $classExistCache = []; - private readonly Lexer $lexer; - private readonly TypeParser $parser; - - public function __construct() - { - $this->lexer = new Lexer(); - $this->parser = new TypeParser(new ConstExprParser()); + public function __construct( + private Lexer $lexer = new Lexer(), + private TypeParser $parser = new TypeParser(new ConstExprParser()), + ) { } public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php index baf011575a1f2..373249c479e24 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php @@ -61,29 +61,35 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type return $resolver->resolve($subject, $typeContext); } - public static function create(): self + /** + * @param array|null $resolvers + */ + public static function create(?array $resolvers = null): self { - $resolvers = new class() implements ContainerInterface { - private readonly array $resolvers; + if (null === $resolvers) { + $stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null; + $typeContextFactory = new TypeContextFactory($stringTypeResolver); + $reflectionTypeResolver = new ReflectionTypeResolver(); + + $resolvers = [ + \ReflectionType::class => $reflectionTypeResolver, + \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory), + \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory), + \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory), + ]; + + if (null !== $stringTypeResolver) { + $resolvers['string'] = $stringTypeResolver; + $resolvers[\ReflectionParameter::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionParameter::class], $stringTypeResolver, $typeContextFactory); + $resolvers[\ReflectionProperty::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionProperty::class], $stringTypeResolver, $typeContextFactory); + $resolvers[\ReflectionFunctionAbstract::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionFunctionAbstract::class], $stringTypeResolver, $typeContextFactory); + } + } - public function __construct() - { - $stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null; - $typeContextFactory = new TypeContextFactory($stringTypeResolver); - $reflectionTypeResolver = new ReflectionTypeResolver(); - - $resolvers = [ - \ReflectionType::class => $reflectionTypeResolver, - \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory), - \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory), - \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory), - ]; - - if (null !== $stringTypeResolver) { - $resolvers['string'] = $stringTypeResolver; - } - - $this->resolvers = $resolvers; + $resolversContainer = new class($resolvers) implements ContainerInterface { + public function __construct( + private readonly array $resolvers, + ) { } public function has(string $id): bool @@ -97,6 +103,6 @@ public function get(string $id): TypeResolverInterface } }; - return new self($resolvers); + return new self($resolversContainer); } } 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