10000 feature #40457 [PropertyInfo] Add `PhpStanExtractor` (Korbeil) · symfony/symfony@5745b43 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5745b43

Browse files
committed
feature #40457 [PropertyInfo] Add PhpStanExtractor (Korbeil)
This PR was merged into the 5.4 branch. Discussion ---------- [PropertyInfo] Add `PhpStanExtractor` | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #38093 | License | MIT | Doc PR | symfony/symfony-docs#... This PR will add a PhpStanExtractor that is based on `phpstan/phpdoc-parser` library. The PhpStan library allows us to manage union types in collection key values that we don't manage today. ### Todo - [x] PhpStanExtractor - [x] Add tests for unions types - [x] Add FrameworkBundle glue (use this extractor if `phpstan/phpdoc-parser` is present) - [x] Update CHANGELOG Related PR: - symfony/serializer-pack#3 put the PhpStanExtractor as default extractor to use on the `serializer-pack` package. Commits ------- 9931c37 Add PhpStanExtractor
2 parents ff83e85 + 9931c37 commit 5745b43

File tree

12 files changed

+1015
-0
lines changed

12 files changed

+1015
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"paragonie/sodium_compat": "^1.8",
139139
"pda/pheanstalk": "^4.0",
140140
"php-http/httplug": "^1.0|^2.0",
141+
"phpstan/phpdoc-parser": "^0.4",
141142
"predis/predis": "~1.1",
142143
"psr/http-client": "^1.0",
143144
"psr/simple-cache": "^1.0|^2.0",

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ CHANGELOG
1919
* Bind the `default_context` parameter onto serializer's encoders and normalizers
2020
* Add support for `statusCode` default parameter when loading a template directly from route using the `Symfony\Bundle\FrameworkBundle\Controller\TemplateController` controller
2121
* Deprecate `translation:update` command, use `translation:extract` instead
22+
* Add `PhpStanExtractor` support for the PropertyInfo component
2223

2324
5.3
2425
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Doctrine\Common\Annotations\Reader;
1717
use Http\Client\HttpClient;
1818
use phpDocumentor\Reflection\DocBlockFactoryInterface;
19+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1920
use Psr\Cache\CacheItemPoolInterface;
2021
use Psr\Container\ContainerInterface as PsrContainerInterface;
2122
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
@@ -160,6 +161,7 @@
160161
use Symfony\Component\Notifier\Recipient\Recipient;
161162
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
162163
use Symfony\Component\PropertyAccess\PropertyAccessor;
164+
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
163165
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
164166
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
165167
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
@@ -1833,6 +1835,14 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container,
18331835

18341836
$loader->load('property_info.php');
18351837

1838+
if (
1839+
ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info'], true)
1840+
&& ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info'], true)
1841+
) {
1842+
$definition = $container->register('property_info.phpstan_extractor', PhpStanExtractor::class);
1843+
$definition->addTag('property_info.type_extractor', ['priority' => -1000]);
1844+
}
1845+
18361846
if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'], true)) {
18371847
$definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor');
18381848
$definition->addTag('property_info.description_extractor', ['priority' => -1000]);

src/Symfony/Component/PropertyInfo/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.4
5+
---
6+
7+
* Add PhpStanExtractor
8+
49
5.3
510
---
611

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyInfo\Extractor;
13+
14+
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
15+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
17+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
18+
use PHPStan\PhpDocParser\Lexer\Lexer;
19+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
20+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
21+
use PHPStan\PhpDocParser\Parser\TokenIterator;
22+
use PHPStan\PhpDocParser\Parser\TypeParser;
23+
use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
24+
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
25+
use Symfony\Component\PropertyInfo\Type;
26+
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
27+
28+
/**
29+
* Extracts data using PHPStan parser.
30+
*
31+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
32+
*/
33+
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
34+
{
35+
public const PROPERTY = 0;
36+
public const ACCESSOR = 1;
37+
public const MUTATOR = 2;
38+
39+
/** @var PhpDocParser */
40+
private $phpDocParser;
41+
42+
/** @var Lexer */
43+
private $lexer;
44+
45+
/** @var NameScopeFactory */
46+
private $nameScopeFactory;
47+
48+
private $docBlocks = [];
49+
private $phpStanTypeHelper;
50+
private $mutatorPrefixes;
51+
private $accessorPrefixes;
52+
private $arrayMutatorPrefixes;
53+
54+
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null)
55+
{
56+
$this->phpStanTypeHelper = new PhpStanTypeHelper();
57+
$this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
58+
$this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
59+
$this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
60+
61+
$this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
62+
$this->lexer = new Lexer();
63+
$this->nameScopeFactory = new NameScopeFactory();
64+
}
65+
66+
public function getTypes(string $class, string $property, array $context = []): ?array
67+
{
68+
/** @var $docNode PhpDocNode */
69+
[$docNode, $source, $prefix] = $this->getDocBlock($class, $property);
70+
$nameScope = $this->nameScopeFactory->create($class);
71+
if (null === $docNode) {
72+
return null;
73+
}
74+
75+
switch ($source) {
76+
case self::PROPERTY:
77+
$tag = '@var';
78+
break;
79+
80+
case self::ACCESSOR:
81+
$tag = '@return';
82+
break;
83+
84+
case self::MUTATOR:
85+
$tag = '@param';
86+
break;
87+
}
88+
89+
$parentClass = null;
90+
$types = [];
91+
foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
92+
if ($tagDocNode->value instanceof InvalidTagValueNode) {
93+
continue;
94+
}
95+
96+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope) as $type) {
97+
switch ($type->getClassName()) {
98+
case 'self':
99+
case 'static':
100+
$resolvedClass = $class;
101+
break;
102+
103+
case 'parent':
104+
if (false !== $resolvedClass = $parentClass ?? $parentClass = get_parent_class($class)) {
105+
break;
106+
}
107+
// no break
108+
109+
default:
110+
$types[] = $type;
111+
continue 2;
112+
}
113+
114+
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
115+
}
116+
}
117+
118+
if (!isset($types[0])) {
119+
return null;
120+
}
121+
122+
if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
123+
return $types;
124+
}
125+
126+
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
127+
}
128+
129+
public function getTypesFromConstructor(string $class, string $property): ?array
130+
{
131+
if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) {
132+
return null;
133+
}
134+
135+
$types = [];
136+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class)) as $type) {
137+
$types[] = $type;
138+
}
139+
140+
if (!isset($types[0])) {
141+
return null;
142+
}
143+
144+
return $types;
145+
}
146+
147+
private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
148+
{
149+
try {
150+
$reflectionClass = new \ReflectionClass($class);
151+
} catch (\ReflectionException $e) {
152+
return null;
153+
}
154+
155+
if (null === $reflectionConstructor = $reflectionClass->getConstructor()) {
156+
return null;
157+
}
158+
159+
$rawDocNode = $reflectionConstructor->getDocComment();
160+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
161+
$phpDocNode = $this->phpDocParser->parse($tokens);
162+
$tokens->consumeTokenType(Lexer::TOKEN_END);
163+
164+
return $this->filterDocBlockParams($phpDocNode, $property);
165+
}
166+
167+
private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam): ?ParamTagValueNode
168+
{
169+
$tags = array_values(array_filter($docNode->getTagsByName('@param'), function ($tagNode) use ($allowedParam) {
170+
return $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName;
171+
}));
172+
173+
if (!$tags) {
174+
return null;
175+
}
176+
177+
return $tags[0]->value;
178+
}
179+
180+
private function getDocBlock(string $class, string $property): array
181+
{
182+
$propertyHash = $class.'::'.$property;
183+
184+
if (isset($this->docBlocks[$propertyHash])) {
185+
return $this->docBlocks[$propertyHash];
186+
}
187+
188+
$ucFirstProperty = ucfirst($property);
189+
190+
if ($docBlock = $this->getDocBlockFromProperty($class, $property)) {
191+
$data = [$docBlock, self::PROPERTY, null];
192+
} elseif ([$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
193+
$data = [$docBlock, self::ACCESSOR, null];
194+
} elseif ([$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR)) {
195+
$data = [$docBlock, self::MUTATOR, $prefix];
196+
} else {
197+
$data = [null, null, null];
198+
}
199+
200+
return $this->docBlocks[$propertyHash] = $data;
201+
}
202+
203+
private function getDocBlockFromProperty(string $class, string $property): ?PhpDocNode
204+
{
205+
// Use a ReflectionProperty instead of $class to get the parent class if applicable
206+
try {
207+
$reflectionProperty = new \ReflectionProperty($class, $property);
208+
} catch (\ReflectionException $e) {
209+
return null;
210+
}
211+
212+
if (null === $rawDocNode = $reflectionProperty->getDocComment() ?: null) {
213+
return null;
214+
}
215+
216+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
217+
$phpDocNode = $this->phpDocParser->parse($tokens);
218+
$tokens->consumeTokenType(Lexer::TOKEN_END);
219+
220+
return $phpDocNode;
221+
}
222+
223+
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
224+
{
225+
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
226+
$prefix = null;
227+
228+
foreach ($prefixes as $prefix) {
229+
$methodName = $prefix.$ucFirstProperty;
230+
231+
try {
232+
$reflectionMethod = new \ReflectionMethod($class, $methodName);
233+
if ($reflectionMethod->isStatic()) {
234+
continue;
235+
}
236+
237+
if (
238+
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
239+
|| (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
240+
) {
241+
break;
242+
}
243+
} catch (\ReflectionException $e) {
244+
// Try the next prefix if the method doesn't exist
245+
}
246+
}
247+
248+
if (!isset($reflectionMethod)) {
249+
return null;
250+
}
251+
252+
if (null === $rawDocNode = $reflectionMethod->getDocComment() ?: null) {
253+
return null;
254+
}
255+
256+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
257+
$phpDocNode = $this->phpDocParser->parse($tokens);
258+
$tokens->consumeTokenType(Lexer::TOKEN_END);
259+
260+
return [$phpDocNode, $prefix];
261+
}
262+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyInfo\PhpStan;
13+
14+
/**
15+
* NameScope class adapted from PHPStan code.
16+
*
17+
* @copyright Copyright (c) 2016, PHPStan https://github.com/phpstan/phpstan-src
18+
* @copyright Copyright (c) 2016, Ondřej Mirtes
19+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
20+
*
21+
* @internal
22+
*/
23+
final class NameScope
24+
{
25+
private $className;
26+
private $namespace;
27+
/** @var array<string, string> alias(string) => fullName(string) */
28+
private $uses;
29+
30+
public function __construct(string $className, string $namespace, array $uses = [])
31+
{
32+
$this->className = $className;
33+
$this->namespace = $namespace;
34+
$this->uses = $uses;
35+
}
36+
37+
public function resolveStringName(string $name): string
38+
{
39+
if (0 === strpos($name, '\\')) {
40+
return ltrim($name, '\\');
41+
}
42+
43+
$nameParts = explode('\\', $name);
44+
if (isset($this->uses[$nameParts[0]])) {
45+
if (1 === \count($nameParts)) {
46+
return $this->uses[$nameParts[0]];
47+
}
48+
array_shift($nameParts);
49+
50+
return sprintf('%s\\%s', $this->uses[$nameParts[0]], implode('\\', $nameParts));
51+
}
52+
53+
if (null !== $this->namespace) {
54+
return sprintf('%s\\%s', $this->namespace, $name);
55+
}
56+
57+
return $name;
58+
}
59+
60+
public function resolveRootClass(): string
61+
{
62+
return $this->resolveStringName($this->className);
63+
}
64+
}

0 commit comments

Comments
 (0)
0