8000 Add PhpStanExtractor · symfony/symfony@aeb982f · GitHub
[go: up one dir, main page]

Skip to content

Commit aeb982f

Browse files
committed
Add PhpStanExtractor
1 parent 6c0102c commit aeb982f

File tree

6 files changed

+762
-2
lines changed

6 files changed

+762
-2
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@
148148
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
149149
"twig/cssinliner-extra": "^2.12|^3",
150150
"twig/inky-extra": "^2.12|^3",
151-
"twig/markdown-extra": "^2.12|^3"
151+
"twig/markdown-extra": "^2.12|^3",
152+
"phpstan/phpdoc-parser": "^0.4"
152153
},
153154
"conflict": {
154155
"async-aws/core": "<1.5",
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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\Lexer\Lexer;
18+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
19+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
20+
use PHPStan\PhpDocParser\Parser\TokenIterator;
21+
use PHPStan\PhpDocParser\Parser\TypeParser;
22+
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
23+
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
24+
use Symfony\Component\PropertyInfo\Type;
25+
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
26+
27+
/**
28+
* Extracts data using PhpStan parser.
29+
*
30+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
31+
*/
32+
final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
33+
{
34+
public const PROPERTY = 0;
35+
public const ACCESSOR = 1;
36+
public const MUTATOR = 2;
37+
38+
/** @var PhpDocParser */
39+
private $phpDocParser;
40+
41+
/** @var Lexer */
42+
private $lexer;
43+
44+
private $docBlocks = [];
45+
private $phpStanTypeHelper;
46+
private $mutatorPrefixes;
47+
private $accessorPrefixes;
48+
private $arrayMutatorPrefixes;
49+
50+
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null)
51+
{
52+
$this->phpStanTypeHelper = new PhpStanTypeHelper();
53+
$this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : ReflectionExtractor::$defaultMutatorPrefixes;
54+
$this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : ReflectionExtractor::$defaultAccessorPrefixes;
55+
$this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : ReflectionExtractor::$defaultArrayMutatorPrefixes;
56+
57+
$constExprParser = new ConstExprParser();
58+
$this->phpDocParser = new PhpDocParser(new TypeParser($constExprParser), $constExprParser);
59+
$this->lexer = new Lexer();
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
public function getTypes(string $class, string $property, array $context = [])
66+
{
67+
/** @var $docNode PhpDocNode */
68+
[$docNode, $source, $prefix] = $this->getDocBlock($class, $property);
69+
if (null === $docNode) {
70+
return null;
71+
}
72+
73+
switch ($source) {
74+
case self::PROPERTY:
75+
$tag = '@var';
76+
break;
77+
78+
case self::ACCESSOR:
79+
$tag = '@return';
80+
break;
81+
82+
case self::MUTATOR:
83+
$tag = '@param';
84+
break;
85+
}
86+
87+
$parentClass = null;
88+
$types = [];
89+
foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
90+
if (!$tagDocNode->value instanceof InvalidTagValueNode) {
91+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value) as $type) {
92+
switch ($type->getClassName()) {
93+
case 'self':
94+
case 'static':
95+
$resolvedClass = $class;
96+
break;
97+
98+
case 'parent':
99+
if (false !== $resolvedClass = $parentClass ?? $parentClass = get_parent_class($class)) {
100+
break;
101+
}
102+
// no break
103+
104+
default:
105+
$types[] = $type;
106+
continue 2;
107+
}
108+
109+
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
110+
}
111+
}
112+
}
113+
114+
if (!isset($types[0])) {
115+
return null;
116+
}
117+
118+
if (!\in_array($prefix, $this->arrayMutatorPrefixes)) {
119+
return $types;
120+
}
121+
122+
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
123+
}
124+
125+
/**
126+
* {@inheritdoc}
127+
*/
128+
public function getTypesFromConstructor(string $class, string $property): ?array
129+
{
130+
$tagDocNode = $this->getDocBlockFromConstructor($class, $property);
131+
if (null === $tagDocNode) {
132+
return null;
133+
}
134+
135+
$types = [];
136+
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode) as $type) {
137+
$types = array_merge($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+
$reflectionConstructor = $reflectionClass->getConstructor();
156+
if (null === $reflectionConstructor) {
157+
return null;
158+
}
159+
160+
$rawDocNode = $reflectionConstructor->getDocComment();
161+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
162+
163+
return $this->filterDocBlockParams($this->phpDocParser->parse($tokens), $property);
164+
}
165+
166+
private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam): ?ParamTagValueNode
167+
{
168+
$tags = array_values(array_filter($docNode->getTagsByName('@param'), function ($tag) use ($allowedParam) {
169+
return $tag instanceof ParamTagValueNode && $allowedParam === $tag->parameterName;
170+
}));
171+
172+
if (\count($tags) <= 0) {
173+
return null;
174+
}
175+
176+
return $tags[0];
177+
}
178+
179+
private function getDocBlock(string $class, string $property): array
180+
{
181+
$propertyHash = sprintf('%s::%s', $class, $property);
182+
183+
if (isset($this->docBlocks[$propertyHash])) {
184+
return $this->docBlocks[$propertyHash];
185+
}
186+
187+
$ucFirstProperty = ucfirst($property);
188+
189+
switch (true) {
190+
case $docBlock = $this->getDocBlockFromProperty($class, $property):
191+
$data = [$docBlock, self::PROPERTY, null];
192+
break;
193+
194+
case [$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
195+
$data = [$docBlock, self::ACCESSOR, null];
196+
break;
197+
198+
case [$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
199+
$data = [$docBlock, self::MUTATOR, $prefix];
200+
break;
201+
202+
default:
203+
$data = [null, null, null];
204+
}
205+
206+
return $this->docBlocks[$propertyHash] = $data;
207+
}
208+
209+
private function getDocBlockFromProperty(string $class, string $property): ?PhpDocNode
210+
{
211+
// Use a ReflectionProperty instead of $class to get the parent class if applicable
212+
try {
213+
$reflectionProperty = new \ReflectionProperty($class, $property);
214+
} catch (\ReflectionException $e) {
215+
return null;
216+
}
217+
218+
$rawDocNode = $reflectionProperty->getDocComment() ?? null;
219+
if (null === $rawDocNode) {
220+
return null;
221+
}
222+
223+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
224+
225+
return $this->phpDocParser->parse($tokens);
226+
}
227+
228+
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
229+
{
230+
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
231+
$prefix = null;
232+
233+
foreach ($prefixes as $prefix) {
234+
$methodName = $prefix.$ucFirstProperty;
235+
236+
try {
237+
$reflectionMethod = new \ReflectionMethod($class, $methodName);
238+
if ($reflectionMethod->isStatic()) {
239+
continue;
240+
}
241+
242+
if (
243+
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) ||
244+
(self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
245+
) {
246+
break;
247+
}
248+
} catch (\ReflectionException $e) {
249+
// Try the next prefix if the method doesn't exist
250+
}
251+
}
252+
253+
if (!isset($reflectionMethod)) {
254+
return null;
255+
}
256+
257+
$rawDocNode = $reflectionMethod->getDocComment() ?? null;
258+
if (null === $rawDocNode) {
259+
return null;
260+
}
261+
262+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
263+
264+
return [$this->phpDocParser->parse($tokens), $prefix];
265+
}
266+
}

0 commit comments

Comments
 (0)
0