Skip to content

[TypeInfo] Add PhpDocAwareReflectionTypeResolver #57618

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
Jul 25, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/TypeInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Add `PhpDocAwareReflectionTypeResolver` resolver

7.1
---

Expand Down
21 changes: 21 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithPhpDoc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Symfony\Component\TypeInfo\Tests\Fixtures;

final class DummyWithPhpDoc
{
/**
* @var array<Dummy>
*/
public mixed $arrayOfDummies = [];

/**
* @param Dummy $dummy
*
* @return Dummy
*/
public function getNextDummy(mixed $dummy): mixed
{
throw new \BadMethodCallException(sprintf('"%s" is not implemented.', __METHOD__));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <mathias.arlaud@gmail.com>
*
* @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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 28 additions & 22 deletions src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TypeResolverInterface>|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
Expand All @@ -97,6 +103,6 @@ public function get(string $id): TypeResolverInterface
}
};

return new self($resolvers);
return new self($resolversContainer);
}
}
Loading