Completed
Push — master ( 55ea92...6e9036 )
by Alexander
03:03
created

ReflectionEngine::parseFile()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0187

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 20
ccs 10
cts 11
cp 0.9091
rs 8.8571
cc 5
eloc 11
nc 5
nop 2
crap 5.0187
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
    }
85
86
    /**
87
     * Locates a file name for class
88
     *
89
     * @param string $fullClassName Full name of the class
90
     *
91
     * @return string
92
     */
93 12
    public static function locateClassFile($fullClassName)
94
    {
95 12
        if (class_exists($fullClassName, false)
96 1
            || interface_exists($fullClassName, false)
97 12
            || trait_exists($fullClassName, false)
98
        ) {
99 12
            $refClass      = new \ReflectionClass($fullClassName);
100 12
            $classFileName = $refClass->getFileName();
101
        } else {
102
            $classFileName = self::$locator->locateClass($fullClassName);
103
        }
104
105 12
        if (!$classFileName) {
106
            throw new \InvalidArgumentException("Class $fullClassName was not found by locator");
107
        }
108
109 12
        return $classFileName;
110
    }
111
112
    /**
113
     * Tries to parse a class by name using LocatorInterface
114
     *
115
     * @param string $fullClassName Class name to load
116
     *
117
     * @return ClassLike
118
     */
119 12
    public static function parseClass($fullClassName)
120
    {
121 12
        $classFileName  = self::locateClassFile($fullClassName);
122 12
        $namespaceParts = explode('\\', $fullClassName);
123 12
        $className      = array_pop($namespaceParts);
124 12
        $namespaceName  = join('\\', $namespaceParts);
125
126 12
        if ($namespaceName) {
127
            // we have a namespace nodes somewhere
128 12
            $namespace      = self::parseFileNamespace($classFileName, $namespaceName);
129 12
            $namespaceNodes = $namespace->stmts;
130
        } else {
131
            // global namespace
132
            $namespaceNodes = self::parseFile($classFileName);
133
        }
134
135 12
        foreach ($namespaceNodes as $namespaceLevelNode) {
136 12
            if ($namespaceLevelNode instanceof ClassLike && $namespaceLevelNode->name == $className) {
137 12
                $namespaceLevelNode->setAttribute('fileName', $classFileName);
138
139 12
                return $namespaceLevelNode;
140
            }
141
        }
142
143
        throw new \InvalidArgumentException("Class $fullClassName was not found in the $classFileName");
144
    }
145
146
    /**
147
     * Parses class method
148
     *
149
     * @param string $fullClassName Name of the class
150
     * @param string $methodName Name of the method
151
     *
152
     * @return ClassMethod
153
     */
154 1
    public static function parseClassMethod($fullClassName, $methodName)
155
    {
156 1
        $class      = self::parseClass($fullClassName);
157 1
        $classNodes = $class->stmts;
158
159 1
        foreach ($classNodes as $classLevelNode) {
160 1
            if ($classLevelNode instanceof ClassMethod && $classLevelNode->name == $methodName) {
161 1
                return $classLevelNode;
162
            }
163
        }
164
165
        throw new \InvalidArgumentException("Method $methodName was not found in the $fullClassName");
166
    }
167
168
    /**
169
     * Parses class property
170
     *
171
     * @param string $fullClassName Name of the class
172
     * @param string $propertyName Name of the property
173
     *
174
     * @return array Pair of [Property and PropertyProperty] nodes
175
     */
176 2
    public static function parseClassProperty($fullClassName, $propertyName)
177
    {
178 2
        $class      = self::parseClass($fullClassName);
179 2
        $classNodes = $class->stmts;
180
181 2
        foreach ($classNodes as $classLevelNode) {
182 2
            if ($classLevelNode instanceof Property) {
183 2
                foreach ($classLevelNode->props as $classProperty) {
184 2
                    if ($classProperty->name == $propertyName) {
185 2
                        return [$classLevelNode, $classProperty];
186
                    }
187
                }
188
            }
189
        }
190
191
        throw new \InvalidArgumentException("Property $propertyName was not found in the $fullClassName");
192
    }
193
194
    /**
195
     * Parses a file and returns an AST for it
196
     *
197
     * @param string      $fileName Name of the file
198
     * @param string|null $fileContent Optional content of the file
199
     *
200
     * @return \PhpParser\Node[]
201
     */
202 142
    public static function parseFile($fileName, $fileContent = null)
203
    {
204 142
        if (isset(self::$parsedFiles[$fileName])) {
205 138
            return self::$parsedFiles[$fileName];
206
        }
207
208 9
        if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) {
209
            array_shift(self::$parsedFiles);
210
        }
211
212 9
        if (!isset($fileContent)) {
213 9
            $fileContent = file_get_contents($fileName);
214
        }
215 9
        $treeNode = self::$parser->parse($fileContent);
216 9
        $treeNode = self::$traverser->traverse($treeNode);
217
218 9
        self::$parsedFiles[$fileName] = $treeNode;
219
220 9
        return $treeNode;
221
    }
222
223
    /**
224
     * Parses a file namespace and returns an AST for it
225
     *
226
     * @param string $fileName Name of the file
227
     * @param string $namespaceName Namespace name
228
     *
229
     * @return Namespace_
230
     */
231 24
    public static function parseFileNamespace($fileName, $namespaceName)
232
    {
233 24
        $topLevelNodes = self::parseFile($fileName);
234
        // namespaces can be only top-level nodes, so we can scan them directly
235 24
        foreach ($topLevelNodes as $topLevelNode) {
236 24
            if ($topLevelNode instanceof Namespace_ && ($topLevelNode->name->toString() == $namespaceName)) {
237 24
                return $topLevelNode;
238
            }
239
        }
240
241
        throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName");
242
    }
243
244
}
245