Completed
Pull Request — master (#82)
by Loren
03:39
created

ReflectionEngine::init()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2.0017

Importance

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