Completed
Push — master ( fdee56...483c4c )
by Alexander
02:07
created

ReflectionEngine::parseFileNamespace()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.2742

Importance

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