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

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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
< 3BFC /tr>
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