10000 Add PhpStanExtractor · symfony/symfony@4a5236f · GitHub
[go: up one dir, main page]

Skip to content

Commit 4a5236f

Browse files
committed
Add PhpStanExtractor
1 parent d679ac5 commit 4a5236f

File tree

7 files changed

+859
-2
lines changed

7 files changed

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

0 commit comments

Comments
 (0)
0