Completed
Push — master ( fa2155...ac7a3a )
by Alexander
126:27 queued 101:26
created

ReflectionEngine::findClassLikeNodeByClassName()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 7
cp 0
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 2
crap 90
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Parser Reflection API
5
 *
6
 * @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
7
 *
8
 * This source file is subject to the license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Go\ParserReflection;
13
14
use Go\ParserReflection\Instrument\PathResolver;
15
use Go\ParserReflection\NodeVisitor\RootNamespaceNormalizer;
16
use InvalidArgumentException;
17
use PhpParser\Lexer;
18
use PhpParser\Node;
19
use PhpParser\Node\Stmt\ClassConst;
20
use PhpParser\Node\Stmt\ClassLike;
21
use PhpParser\Node\Stmt\ClassMethod;
22
use PhpParser\Node\Stmt\Namespace_;
23
use PhpParser\Node\Stmt\Property;
24
use PhpParser\NodeTraverser;
25
use PhpParser\NodeVisitor\NameResolver;
26
use PhpParser\Parser;
27
use PhpParser\ParserFactory;
28
29
/**
30
 * AST-based reflection engine, powered by PHP-Parser
31
 */
32
class ReflectionEngine
33
{
34
    /**
35
     * @var null|LocatorInterface
36
     */
37
    protected static $locator;
38
39
    /**
40
     * @var array|Node[]
41
     */
42
    protected static $parsedFiles = [];
43
44
    /**
45
     * @var null|int
46
     */
47
    protected static $maximumCachedFiles;
48
49
    /**
50
     * @var null|Parser
51
     */
52
    protected static $parser;
53
54
    /**
55
     * @var null|NodeTraverser
56
     */
57
    protected static $traverser;
58
59
    /**
60
     * @var null|Lexer
61
     */
62
    protected static $lexer;
63
64 1
    private function __construct() {}
65
66 1
    public static function init(LocatorInterface $locator): void
67 1
    {
68
        self::$lexer = new Lexer(['usedAttributes' => [
69
            'comments',
70
            'startLine',
71
            'endLine',
72
            'startTokenPos',
73
            'endTokenPos',
74
            'startFilePos',
75
            'endFilePos'
76 1
        ]]);
77
78 1
        self::$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, self::$lexer);
79 1
80 1
        self::$traverser = $traverser = new NodeTraverser();
81
        $traverser->addVisitor(new NameResolver());
82 1
        $traverser->addVisitor(new RootNamespaceNormalizer());
83 1
84
        self::$locator = $locator;
85
    }
86
87
    /**
88
     * Limits number of files, that can be cached at any given moment
89
     */
90
    public static function setMaximumCachedFiles(int $newLimit): void
91
    {
92
        self::$maximumCachedFiles = $newLimit;
93
        if (count(self::$parsedFiles) > $newLimit) {
94
            self::$parsedFiles = array_slice(self::$parsedFiles, 0, $newLimit);
95
        }
96
    }
97
98
    /**
99
     * Locates a file name for class
100
     */
101
    public static function locateClassFile(string $fullClassName): string
102
    {
103
        if (class_exists($fullClassName, false)
104
            || interface_exists($fullClassName, false)
105
            || trait_exists($fullClassName, false)
106
        ) {
107 22
            $refClass      = new \ReflectionClass($fullClassName);
108
            $classFileName = $refClass->getFileName();
109 22
        } else {
110 3
            $classFileName = self::$locator->locateClass($fullClassName);
111 22
        }
112
113 21
        if (!$classFileName) {
114 21
            throw new InvalidArgumentException("Class $fullClassName was not found by locator");
115
        }
116 1
117
        return $classFileName;
118
    }
119 22
120
    /**
121
     * Tries to parse a class by name using LocatorInterface
122
     */
123 22
    public static function parseClass(string $fullClassName): ClassLike
124
    {
125
        $classFileName  = self::locateClassFile($fullClassName);
126
        $namespaceParts = explode('\\', $fullClassName);
127
        $className      = array_pop($namespaceParts);
128
        $namespaceName  = implode('\\', $namespaceParts);
129
130
        // we have a namespace node somewhere
131
        $namespace      = self::parseFileNamespace($classFileName, $namespaceName);
132
        $namespaceNodes = $namespace->stmts;
133 22
134
        $namespaceNode = self::findClassLikeNodeByClassName($namespaceNodes, $className);
135 22
        if ($namespaceNode instanceof ClassLike) {
136 22
            $namespaceNode->setAttribute('fileName', $classFileName);
137 22
138 22
            return $namespaceNode;
139
        }
140
141 22
        throw new InvalidArgumentException("Class $fullClassName was not found in the $classFileName");
142 22
    }
143
144 22
    /**
145 22
     * Loop through an array and find a ClassLike statement by the given class name.
146 22
     *
147
     * If an if statement like `if (false) {` is found, the class will also be search inside that if statement.
148 22
     * This relies on the guide of greg0ire on how to deprecate a type.
149
     *
150
     * @see https://dev.to/greg0ire/how-to-deprecate-a-type-in-php-48cf
151
     */
152
    protected static function findClassLikeNodeByClassName(array $nodes, string $className): ?ClassLike
153
    {
154
        foreach ($nodes as $node) {
155
            if ($node instanceof ClassLike && $node->name->toString() == $className) {
156
                return $node;
157
            }
158
            if ($node instanceof Node\Stmt\If_
159
                && $node->cond instanceof Node\Expr\ConstFetch
160
                && isset($node->cond->name->parts[0])
161
                && $node->cond->name->parts[0] === 'false'
162
            ) {
163
                $result = self::findClassLikeNodeByClassName($node->stmts, $className);
164
165
                if ($result instanceof ClassLike) {
166
                    return $result;
167
                }
168
            }
169
        }
170
171
        return null;
172
    }
173
174
    /**
175
     * Parses class method
176
     */
177
    public static function parseClassMethod(string $fullClassName, string $methodName): ClassMethod
178
    {
179
        $class      = self::parseClass($fullClassName);
180
        $classNodes = $class->stmts;
181
182
        foreach ($classNodes as $classLevelNode) {
183
            if ($classLevelNode instanceof ClassMethod && $classLevelNode->name->toString() === $methodName) {
184
                return $classLevelNode;
185 1
            }
186
        }
187 1
188 1
        throw new InvalidArgumentException("Method $methodName was not found in the $fullClassName");
189
    }
190 1
191 1
    /**
192 1
     * Parses class property
193 1
     *
194 1
     * @return array Pair of [Property and PropertyProperty] nodes
195
     */
196
    public static function parseClassProperty(string $fullClassName, string $propertyName): array
197
    {
198
        $class      = self::parseClass($fullClassName);
199
        $classNodes = $class->stmts;
200
201
        foreach ($classNodes as $classLevelNode) {
202
            if ($classLevelNode instanceof Property) {
203
                foreach ($classLevelNode->props as $classProperty) {
204
                    if ($classProperty->name->toString() === $propertyName) {
205
                        return [$classLevelNode, $classProperty];
206
                    }
207
                }
208
            }
209
        }
210
211
        throw new InvalidArgumentException("Property $propertyName was not found in the $fullClassName");
212
    }
213
214
    /**
215
     * Parses class constants
216
     *
217
     * @param string $fullClassName
218
     * @param string $constantName
219
     * @return array Pair of [ClassConst and Const_] nodes
220
     */
221
    public static function parseClassConstant(string $fullClassName, string $constantName): array
222
    {
223
        $class      = self::parseClass($fullClassName);
224
        $classNodes = $class->stmts;
225
226
        foreach ($classNodes as $classLevelNode) {
227
            if ($classLevelNode instanceof ClassConst) {
228
                foreach ($classLevelNode->consts as $classConst) {
229
                    if ($classConst->name->toString() === $constantName) {
230
                        return [$classLevelNode, $classConst];
231
                    }
232
                }
233
            }
234
        }
235
236 3051
        throw new InvalidArgumentException("ClassConstant $constantName was not found in the $fullClassName");
237
    }
238 3051
239 3051
    /**
240 3049
     * Parses a file and returns an AST for it
241
     *
242
     * @param string|null $fileContent Optional content of the file
243 9
     *
244
     * @return Node[]
245
     */
246
    public static function parseFile(string $fileName, ?string $fileContent = null)
247 9
    {
248 9
        $fileName = PathResolver::realpath($fileName);
249
        if (isset(self::$parsedFiles[$fileName]) && !isset($fileContent)) {
250 9
            return self::$parsedFiles[$fileName];
251 9
        }
252
253 9
        if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) {
254
            array_shift(self::$parsedFiles);
255 9
        }
256
257
        if (!isset($fileContent)) {
258
            $fileContent = file_get_contents($fileName);
259
        }
260
        $treeNode = self::$parser->parse($fileContent);
261
        $treeNode = self::$traverser->traverse($treeNode);
262
263
        self::$parsedFiles[$fileName] = $treeNode;
264
265
        return $treeNode;
266
    }
267 37
268
    /**
269 37
     * Parses a file namespace and returns an AST for it
270
     *
271 37
     * @throws ReflectionException
272 37
     */
273 2
    public static function parseFileNamespace(string $fileName, string $namespaceName): Namespace_
274
    {
275 37
        $topLevelNodes = self::parseFile($fileName);
276 37
        // namespaces can be only top-level nodes, so we can scan them directly
277 37
        foreach ($topLevelNodes as $topLevelNode) {
278
            if (!$topLevelNode instanceof Namespace_) {
279
                continue;
280
            }
281
            $topLevelNodeName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
282
            if (ltrim($topLevelNodeName, '\\') === trim($namespaceName, '\\')) {
283
                return $topLevelNode;
284
            }
285
        }
286
287
        throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName");
288
    }
289
290
    /**
291
     * @return Lexer
292
     */
293
    public static function getLexer(): ?Lexer
294
    {
295
        return self::$lexer;
296
    }
297
}
298