Completed
Push — master ( 34991a...adfc38 )
by Alexander
13s
created

src/ReflectionEngine.php (1 issue)

Upgrade to new PHP Analysis Engine

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
    /**
57
     * @var null|Lexer
58
     */
59
    protected static $lexer = null;
60
61
    private function __construct() {}
62
63 1
    public static function init(LocatorInterface $locator)
64
    {
65 1
        self::$lexer = new Lexer(['usedAttributes' => [
66
            'comments',
67
            'startLine',
68
            'endLine',
69
            'startTokenPos',
70
            'endTokenPos',
71
            'startFilePos',
72
            'endFilePos'
73
        ]]);
74
75 1
        $refParser   = new \ReflectionClass(Parser::class);
76 1
        $isNewParser = $refParser->isInterface();
77 1
        if (!$isNewParser) {
78
            self::$parser = new Parser(self::$lexer);
1 ignored issue
show
The call to Parser::__construct() has too many arguments starting with self::$lexer.

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.

Loading history...
79
        } else {
80 1
            self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, self::$lexer);
81
        }
82
83 1
        self::$traverser = $traverser = new NodeTraverser();
84 1
        $traverser->addVisitor(new NameResolver());
85 1
        $traverser->addVisitor(new RootNamespaceNormalizer());
86
87 1
        self::$locator = $locator;
88 1
    }
89
90
    /**
91
     * Limits number of files, that can be cached at any given moment
92
     *
93
     * @param integer $newLimit New limit
94
     *
95
     * @return void
96
     */
97
    public static function setMaximumCachedFiles($newLimit)
98
    {
99
        self::$maximumCachedFiles = $newLimit;
100
        if (count(self::$parsedFiles) > $newLimit) {
101
            self::$parsedFiles = array_slice(self::$parsedFiles, 0, $newLimit);
102
        }
103
    }
104
105
    /**
106
     * Locates a file name for class
107
     *
108
     * @param string $fullClassName Full name of the class
109
     *
110
     * @return string
111
     */
112 19
    public static function locateClassFile($fullClassName)
113
    {
114 19
        if (class_exists($fullClassName, false)
115 3
            || interface_exists($fullClassName, false)
116 19
            || trait_exists($fullClassName, false)
117
        ) {
118 18
            $refClass      = new \ReflectionClass($fullClassName);
119 18
            $classFileName = $refClass->getFileName();
120
        } else {
121 1
            $classFileName = self::$locator->locateClass($fullClassName);
122
        }
123
124 19
        if (!$classFileName) {
125
            throw new \InvalidArgumentException("Class $fullClassName was not found by locator");
126
        }
127
128 19
        return $classFileName;
129
    }
130
131
    /**
132
     * Tries to parse a class by name using LocatorInterface
133
     *
134
     * @param string $fullClassName Class name to load
135
     *
136
     * @return ClassLike
137
     */
138 19
    public static function parseClass($fullClassName)
139
    {
140 19
        $classFileName  = self::locateClassFile($fullClassName);
141 19
        $namespaceParts = explode('\\', $fullClassName);
142 19
        $className      = array_pop($namespaceParts);
143 19
        $namespaceName  = join('\\', $namespaceParts);
144
145
        // we have a namespace node somewhere
146 19
        $namespace      = self::parseFileNamespace($classFileName, $namespaceName);
147 19
        $namespaceNodes = $namespace->stmts;
148
149 19
        foreach ($namespaceNodes as $namespaceLevelNode) {
150 19
            if ($namespaceLevelNode instanceof ClassLike && $namespaceLevelNode->name == $className) {
151 19
                $namespaceLevelNode->setAttribute('fileName', $classFileName);
152
153 19
                return $namespaceLevelNode;
154
            }
155
        }
156
157
        throw new \InvalidArgumentException("Class $fullClassName was not found in the $classFileName");
158
    }
159
160
    /**
161
     * Parses class method
162
     *
163
     * @param string $fullClassName Name of the class
164
     * @param string $methodName Name of the method
165
     *
166
     * @return ClassMethod
167
     */
168
    public static function parseClassMethod($fullClassName, $methodName)
169
    {
170
        $class      = self::parseClass($fullClassName);
171
        $classNodes = $class->stmts;
172
173
        foreach ($classNodes as $classLevelNode) {
174
            if ($classLevelNode instanceof ClassMethod && $classLevelNode->name == $methodName) {
175
                return $classLevelNode;
176
            }
177
        }
178
179
        throw new \InvalidArgumentException("Method $methodName was not found in the $fullClassName");
180
    }
181
182
    /**
183
     * Parses class property
184
     *
185
     * @param string $fullClassName Name of the class
186
     * @param string $propertyName Name of the property
187
     *
188
     * @return array Pair of [Property and PropertyProperty] nodes
189
     */
190 1
    public static function parseClassProperty($fullClassName, $propertyName)
191
    {
192 1
        $class      = self::parseClass($fullClassName);
193 1
        $classNodes = $class->stmts;
194
195 1
        foreach ($classNodes as $classLevelNode) {
196 1
            if ($classLevelNode instanceof Property) {
197 1
                foreach ($classLevelNode->props as $classProperty) {
198 1
                    if ($classProperty->name == $propertyName) {
199 1
                        return [$classLevelNode, $classProperty];
200
                    }
201
                }
202
            }
203
        }
204
205
        throw new \InvalidArgumentException("Property $propertyName was not found in the $fullClassName");
206
    }
207
208
    /**
209
     * Parses a file and returns an AST for it
210
     *
211
     * @param string      $fileName Name of the file
212
     * @param string|null $fileContent Optional content of the file
213
     *
214
     * @return \PhpParser\Node[]
215
     */
216 3046
    public static function parseFile($fileName, $fileContent = null)
217
    {
218 3046
        $fileName = PathResolver::realpath($fileName);
219 3046
        if (isset(self::$parsedFiles[$fileName]) && !isset($fileContent)) {
220 3044
            return self::$parsedFiles[$fileName];
221
        }
222
223 9
        if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) {
224
            array_shift(self::$parsedFiles);
225
        }
226
227 9
        if (!isset($fileContent)) {
228 9
            $fileContent = file_get_contents($fileName);
229
        }
230 9
        $treeNode = self::$parser->parse($fileContent);
231 9
        $treeNode = self::$traverser->traverse($treeNode);
232
233 9
        self::$parsedFiles[$fileName] = $treeNode;
234
235 9
        return $treeNode;
236
    }
237
238
    /**
239
     * Parses a file namespace and returns an AST for it
240
     *
241
     * @param string $fileName Name of the file
242
     * @param string $namespaceName Namespace name
243
     *
244
     * @return Namespace_
245
     * @throws ReflectionException
246
     */
247 35
    public static function parseFileNamespace($fileName, $namespaceName)
248
    {
249 35
        $topLevelNodes = self::parseFile($fileName);
250
        // namespaces can be only top-level nodes, so we can scan them directly
251 35
        foreach ($topLevelNodes as $topLevelNode) {
252 35
            if (!$topLevelNode instanceof Namespace_) {
253 2
                continue;
254
            }
255 35
            $topLevelNodeName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
256 35
            if (ltrim($topLevelNodeName, '\\') === trim($namespaceName, '\\')) {
257 35
                return $topLevelNode;
258
            }
259
        }
260
261
        throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName");
262
    }
263
264
}
265