These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Parser Reflection API |
||
4 | * |
||
5 | * @copyright Copyright 2015, Lisachenko Alexander <[email protected]> |
||
6 | * |
||
7 | * This source file is subject to the license that is bundled |
||
8 | * with this source code in the file LICENSE. |
||
9 | */ |
||
10 | |||
11 | namespace Go\ParserReflection; |
||
12 | |||
13 | use Go\ParserReflection\Instrument\PathResolver; |
||
14 | use Go\ParserReflection\NodeVisitor\RootNamespaceNormalizer; |
||
15 | use PhpParser\Lexer; |
||
16 | use PhpParser\Node; |
||
17 | use PhpParser\Node\Stmt\ClassLike; |
||
18 | use PhpParser\Node\Stmt\ClassMethod; |
||
19 | use PhpParser\Node\Stmt\Namespace_; |
||
20 | use PhpParser\Node\Stmt\Property; |
||
21 | use PhpParser\NodeTraverser; |
||
22 | use PhpParser\NodeVisitor\NameResolver; |
||
23 | use PhpParser\Parser; |
||
24 | use PhpParser\ParserFactory; |
||
25 | |||
26 | /** |
||
27 | * AST-based reflection engine, powered by PHP-Parser |
||
28 | */ |
||
29 | class ReflectionEngine |
||
30 | { |
||
31 | /** |
||
32 | * @var null|LocatorInterface |
||
33 | */ |
||
34 | protected static $locator = null; |
||
35 | |||
36 | /** |
||
37 | * @var array|Node[] |
||
38 | */ |
||
39 | protected static $parsedFiles = array(); |
||
40 | |||
41 | /** |
||
42 | * @var null|integer |
||
43 | */ |
||
44 | protected static $maximumCachedFiles; |
||
45 | |||
46 | /** |
||
47 | * @var null|Parser |
||
48 | */ |
||
49 | protected static $parser = null; |
||
50 | |||
51 | /** |
||
52 | * @var null|NodeTraverser |
||
53 | */ |
||
54 | protected static $traverser = null; |
||
55 | |||
56 | private function __construct() {} |
||
57 | |||
58 | public static function init(LocatorInterface $locator) |
||
59 | { |
||
60 | $refParser = new \ReflectionClass(Parser::class); |
||
61 | $isNewParser = $refParser->isInterface(); |
||
62 | if (!$isNewParser) { |
||
63 | self::$parser = new Parser(new Lexer(['usedAttributes' => [ |
||
0 ignored issues
–
show
|
|||
64 | 'comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos', 'startFilePos', 'endFilePos' |
||
65 | ]])); |
||
66 | } else { |
||
67 | self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); |
||
68 | } |
||
69 | |||
70 | self::$traverser = $traverser = new NodeTraverser(); |
||
71 | $traverser->addVisitor(new NameResolver()); |
||
72 | $traverser->addVisitor(new RootNamespaceNormalizer()); |
||
73 | |||
74 | self::$locator = $locator; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * Limits number of files, that can be cached at any given moment |
||
79 | * |
||
80 | * @param integer $newLimit New limit |
||
81 | * |
||
82 | * @return void |
||
83 | */ |
||
84 | public static function setMaximumCachedFiles($newLimit) |
||
85 | { |
||
86 | self::$maximumCachedFiles = $newLimit; |
||
87 | if (count(self::$parsedFiles) > $newLimit) { |
||
88 | self::$parsedFiles = array_slice(self::$parsedFiles, 0, $newLimit); |
||
89 | } |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Locates a file name for class |
||
94 | * |
||
95 | * @param string $fullClassName Full name of the class |
||
96 | * |
||
97 | * @return string |
||
98 | */ |
||
99 | 18 | public static function locateClassFile($fullClassName) |
|
100 | { |
||
101 | 18 | if (class_exists($fullClassName, false) |
|
102 | 2 | || interface_exists($fullClassName, false) |
|
103 | 18 | || trait_exists($fullClassName, false) |
|
104 | ) { |
||
105 | 18 | $refClass = new \ReflectionClass($fullClassName); |
|
106 | 18 | $classFileName = $refClass->getFileName(); |
|
107 | } else { |
||
108 | $classFileName = self::$locator->locateClass($fullClassName); |
||
109 | } |
||
110 | |||
111 | 18 | if (!$classFileName) { |
|
112 | throw new \InvalidArgumentException("Class $fullClassName was not found by locator"); |
||
113 | } |
||
114 | |||
115 | 18 | return $classFileName; |
|
116 | } |
||
117 | |||
118 | /** |
||
119 | * Tries to parse a class by name using LocatorInterface |
||
120 | * |
||
121 | * @param string $fullClassName Class name to load |
||
122 | * |
||
123 | * @return ClassLike |
||
124 | */ |
||
125 | 18 | public static function parseClass($fullClassName) |
|
126 | { |
||
127 | 18 | $classFileName = self::locateClassFile($fullClassName); |
|
128 | 18 | $namespaceParts = explode('\\', $fullClassName); |
|
129 | 18 | $className = array_pop($namespaceParts); |
|
130 | 18 | $namespaceName = join('\\', $namespaceParts); |
|
131 | |||
132 | // we have a namespace node somewhere |
||
133 | 18 | $namespace = self::parseFileNamespace($classFileName, $namespaceName); |
|
134 | 18 | $namespaceNodes = $namespace->stmts; |
|
135 | |||
136 | 18 | foreach ($namespaceNodes as $namespaceLevelNode) { |
|
137 | 18 | if ($namespaceLevelNode instanceof ClassLike && $namespaceLevelNode->name == $className) { |
|
138 | 18 | $namespaceLevelNode->setAttribute('fileName', $classFileName); |
|
139 | |||
140 | 18 | return $namespaceLevelNode; |
|
141 | } |
||
142 | } |
||
143 | |||
144 | throw new \InvalidArgumentException("Class $fullClassName was not found in the $classFileName"); |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Parses class method |
||
149 | * |
||
150 | * @param string $fullClassName Name of the class |
||
151 | * @param string $methodName Name of the method |
||
152 | * |
||
153 | * @return ClassMethod |
||
154 | */ |
||
155 | public static function parseClassMethod($fullClassName, $methodName) |
||
156 | { |
||
157 | $class = self::parseClass($fullClassName); |
||
158 | $classNodes = $class->stmts; |
||
159 | |||
160 | foreach ($classNodes as $classLevelNode) { |
||
161 | if ($classLevelNode instanceof ClassMethod && $classLevelNode->name == $methodName) { |
||
162 | return $classLevelNode; |
||
163 | } |
||
164 | } |
||
165 | |||
166 | throw new \InvalidArgumentException("Method $methodName was not found in the $fullClassName"); |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Parses class property |
||
171 | * |
||
172 | * @param string $fullClassName Name of the class |
||
173 | * @param string $propertyName Name of the property |
||
174 | * |
||
175 | * @return array Pair of [Property and PropertyProperty] nodes |
||
176 | */ |
||
177 | 1 | public static function parseClassProperty($fullClassName, $propertyName) |
|
178 | { |
||
179 | 1 | $class = self::parseClass($fullClassName); |
|
180 | 1 | $classNodes = $class->stmts; |
|
181 | |||
182 | 1 | foreach ($classNodes as $classLevelNode) { |
|
183 | 1 | if ($classLevelNode instanceof Property) { |
|
184 | 1 | foreach ($classLevelNode->props as $classProperty) { |
|
185 | 1 | if ($classProperty->name == $propertyName) { |
|
186 | 1 | return [$classLevelNode, $classProperty]; |
|
187 | } |
||
188 | } |
||
189 | } |
||
190 | } |
||
191 | |||
192 | throw new \InvalidArgumentException("Property $propertyName was not found in the $fullClassName"); |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Parses a file and returns an AST for it |
||
197 | * |
||
198 | * @param string $fileName Name of the file |
||
199 | * @param string|null $fileContent Optional content of the file |
||
200 | * |
||
201 | * @return \PhpParser\Node[] |
||
202 | */ |
||
203 | 3006 | public static function parseFile($fileName, $fileContent = null) |
|
204 | { |
||
205 | 3006 | $fileName = PathResolver::realpath($fileName); |
|
206 | 3006 | if (isset(self::$parsedFiles[$fileName])) { |
|
207 | 3004 | return self::$parsedFiles[$fileName]; |
|
208 | } |
||
209 | |||
210 | 8 | if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) { |
|
211 | array_shift(self::$parsedFiles); |
||
212 | } |
||
213 | |||
214 | 8 | if (!isset($fileContent)) { |
|
215 | 8 | $fileContent = file_get_contents($fileName); |
|
216 | } |
||
217 | 8 | $treeNode = self::$parser->parse($fileContent); |
|
218 | 8 | $treeNode = self::$traverser->traverse($treeNode); |
|
219 | |||
220 | 8 | self::$parsedFiles[$fileName] = $treeNode; |
|
221 | |||
222 | 8 | return $treeNode; |
|
223 | } |
||
224 | |||
225 | /** |
||
226 | * Parses a file namespace and returns an AST for it |
||
227 | * |
||
228 | * @param string $fileName Name of the file |
||
229 | * @param string $namespaceName Namespace name |
||
230 | * |
||
231 | * @return Namespace_ |
||
232 | */ |
||
233 | 33 | public static function parseFileNamespace($fileName, $namespaceName) |
|
234 | { |
||
235 | 33 | $topLevelNodes = self::parseFile($fileName); |
|
236 | // namespaces can be only top-level nodes, so we can scan them directly |
||
237 | 33 | foreach ($topLevelNodes as $topLevelNode) { |
|
238 | 33 | if (!$topLevelNode instanceof Namespace_) { |
|
239 | continue; |
||
240 | } |
||
241 | 33 | $topLevelNodeName = $topLevelNode->name ? $topLevelNode->name->toString() : ''; |
|
242 | 33 | if ($topLevelNodeName === $namespaceName) { |
|
243 | 33 | return $topLevelNode; |
|
244 | } |
||
245 | } |
||
246 | |||
247 | throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName"); |
||
248 | } |
||
249 | |||
250 | } |
||
251 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.