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

ReflectionEngine::parseFileNamespace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.074

Importance

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