Completed
Pull Request — master (#38)
by Alexander
02:38
created

ReflectionEngine   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 67.57%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 12
dl 0
loc 222
ccs 50
cts 74
cp 0.6757
rs 9.3999
c 0
b 0
f 0

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