Completed
Push — master ( 04e107...80b1e1 )
by Alexander
03:59 queued 59s
created

ReflectionEngine   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 67.12%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 33
c 5
b 0
f 0
lcom 1
cbo 11
dl 0
loc 221
ccs 49
cts 73
cp 0.6712
rs 9.3999

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 1 1
A init() 0 17 2
A setMaximumCachedFiles() 0 7 2
B locateClassFile() 0 18 5
B parseClass() 0 26 5
A parseClassMethod() 0 13 4
B parseClassProperty() 0 17 5
B parseFile() 0 20 5
A parseFileNamespace() 0 12 4
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 PhpParser\Lexer;
14
use PhpParser\Node;
15
use PhpParser\Node\Stmt\ClassLike;
16
use PhpParser\Node\Stmt\ClassMethod;
17
use PhpParser\Node\Stmt\Namespace_;
18
use PhpParser\Node\Stmt\Property;
19
use PhpParser\NodeTraverser;
20
use PhpParser\NodeVisitor\NameResolver;
21
use PhpParser\Parser;
22
use PhpParser\ParserFactory;
23
24
/**
25
 * AST-based reflection engine, powered by PHP-Parser
26
 */
27
class ReflectionEngine
28
{
29
    /**
30
     * @var null|LocatorInterface
31
     */
32
    protected static $locator = null;
33
34
    /**
35
     * @var array|Node[]
36
     */
37
    protected static $parsedFiles = array();
38
39
    /**
40
     * @var null|integer
41
     */
42
    protected static $maximumCachedFiles;
43
44
    /**
45
     * @var null|Parser
46
     */
47
    protected static $parser = null;
48
49
    /**
50
     * @var null|NodeTraverser
51
     */
52
    protected static $traverser = null;
53
54
    private function __construct() {}
55
56
    public static function init(LocatorInterface $locator)
57
    {
58
        $refParser   = new \ReflectionClass(Parser::class);
59
        $isNewParser = $refParser->isInterface();
60
        if (!$isNewParser) {
61
            self::$parser = new Parser(new Lexer(['usedAttributes' => [
0 ignored issues
show
Unused Code introduced by
The call to Parser::__construct() has too many arguments starting with new \PhpParser\Lexer(arr...lePos', 'endFilePos'))).

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