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

Skip to content

Commit 32e2af3

Browse files
committed
Add PhpStanExtractor
1 parent 6c0102c commit 32e2af3

File tree

6 files changed

+761
-2
lines changed

6 files changed

+761
-2
lines changed

composer.json

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

0 commit comments

Comments
 (0)
0