Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
39.18% |
38 / 97 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
FileSymbolScanner | |
39.18% |
38 / 97 |
|
0.00% |
0 / 8 |
278.06 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
add | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
90 | |||
findInFiles | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
find | |
75.00% |
30 / 40 |
|
0.00% |
0 / 1 |
11.56 | |||
addDiscoveredClassChange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
addDiscoveredNamespaceChange | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getBuiltIns | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
loadBuiltIns | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * The purpose of this class is only to find changes that should be made. |
4 | * i.e. classes and namespaces to change. |
5 | * Those recorded are updated in a later step. |
6 | */ |
7 | |
8 | namespace BrianHenryIE\Strauss\Pipeline; |
9 | |
10 | use BrianHenryIE\Strauss\Config\FileSymbolScannerConfigInterface; |
11 | use BrianHenryIE\Strauss\Files\DiscoveredFiles; |
12 | use BrianHenryIE\Strauss\Files\File; |
13 | use BrianHenryIE\Strauss\Types\ClassSymbol; |
14 | use BrianHenryIE\Strauss\Types\ConstantSymbol; |
15 | use BrianHenryIE\Strauss\Types\DiscoveredSymbol; |
16 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
17 | use BrianHenryIE\Strauss\Types\FunctionSymbol; |
18 | use BrianHenryIE\Strauss\Types\NamespaceSymbol; |
19 | use League\Flysystem\FilesystemReader; |
20 | use PhpParser\Node; |
21 | use PhpParser\NodeTraverser; |
22 | use PhpParser\ParserFactory; |
23 | use Psr\Log\LoggerAwareTrait; |
24 | use Psr\Log\LoggerInterface; |
25 | use Psr\Log\NullLogger; |
26 | |
27 | class FileSymbolScanner |
28 | { |
29 | use LoggerAwareTrait; |
30 | |
31 | /** @var string[] */ |
32 | protected array $excludeNamespacesFromPrefixing = array(); |
33 | |
34 | protected DiscoveredSymbols $discoveredSymbols; |
35 | |
36 | protected FilesystemReader $filesystem; |
37 | |
38 | /** @var string[] */ |
39 | protected array $builtIns = []; |
40 | |
41 | /** |
42 | * @var string[] |
43 | */ |
44 | protected array $loggedSymbols = []; |
45 | |
46 | /** |
47 | * FileScanner constructor. |
48 | */ |
49 | public function __construct( |
50 | FileSymbolScannerConfigInterface $config, |
51 | FilesystemReader $filesystem, |
52 | ?LoggerInterface $logger = null |
53 | ) { |
54 | $this->discoveredSymbols = new DiscoveredSymbols(); |
55 | $this->excludeNamespacesFromPrefixing = $config->getExcludeNamespacesFromPrefixing(); |
56 | |
57 | $this->filesystem = $filesystem; |
58 | $this->logger = $logger ?? new NullLogger(); |
59 | } |
60 | |
61 | protected function add(DiscoveredSymbol $symbol): void |
62 | { |
63 | $this->discoveredSymbols->add($symbol); |
64 | |
65 | $level = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? 'debug' : 'info'; |
66 | $newText = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? '' : 'new '; |
67 | $noNewText = in_array($symbol->getOriginalSymbol(), $this->loggedSymbols) ? ' ' : ''; |
68 | |
69 | $this->loggedSymbols[] = $symbol->getOriginalSymbol(); |
70 | |
71 | switch (get_class($symbol)) { |
72 | case NamespaceSymbol::class: |
73 | $this->logger->log($level, "Found {$newText}namespace: {$noNewText}" . $symbol->getOriginalSymbol()); |
74 | break; |
75 | case ConstantSymbol::class: |
76 | $this->logger->log($level, "Found {$newText}constant: {$noNewText}" . $symbol->getOriginalSymbol()); |
77 | break; |
78 | case ClassSymbol::class: |
79 | $this->logger->log($level, "Found {$newText}class. {$noNewText}" . $symbol->getOriginalSymbol()); |
80 | break; |
81 | case FunctionSymbol::class: |
82 | $this->logger->log($level, "Found {$newText}function {$noNewText}" . $symbol->getOriginalSymbol()); |
83 | break; |
84 | default: |
85 | $this->logger->log($level, "Found {$newText} " . get_class($symbol) . $noNewText . ' ' . $symbol->getOriginalSymbol()); |
86 | } |
87 | } |
88 | |
89 | /** |
90 | * @param DiscoveredFiles $files |
91 | */ |
92 | public function findInFiles(DiscoveredFiles $files): DiscoveredSymbols |
93 | { |
94 | foreach ($files->getFiles() as $file) { |
95 | if (!$file->isPhpFile()) { |
96 | $this->logger->debug('Skipping non-PHP file: ' . $file->getSourcePath()); |
97 | continue; |
98 | } |
99 | |
100 | $this->logger->info('Scanning file: ' . $file->getSourcePath()); |
101 | $this->find( |
102 | $this->filesystem->read($file->getSourcePath()), |
103 | $file |
104 | ); |
105 | } |
106 | |
107 | return $this->discoveredSymbols; |
108 | } |
109 | |
110 | /** |
111 | * TODO: Don't use preg_replace_callback! |
112 | * |
113 | * @uses self::addDiscoveredNamespaceChange() |
114 | * @uses self::addDiscoveredClassChange() |
115 | */ |
116 | protected function find(string $contents, File $file): void |
117 | { |
118 | // If the entire file is under one namespace, all we want is the namespace. |
119 | // If there were more than one namespace, it would appear as `namespace MyNamespace { ...`, |
120 | // a file with only a single namespace will appear as `namespace MyNamespace;`. |
121 | $singleNamespacePattern = '/ |
122 | (<?php|\r\n|\n) # A new line or the beginning of the file. |
123 | \s* # Allow whitespace before |
124 | namespace\s+(?<namespace>[0-9A-Za-z_\x7f-\xff\\\\]+)[\s\n]*; # Match a single namespace in the file. |
125 | /x'; // # x: ignore whitespace in regex. |
126 | if (1 === preg_match($singleNamespacePattern, $contents, $matches)) { |
127 | $this->addDiscoveredNamespaceChange($matches['namespace'], $file); |
128 | |
129 | return; |
130 | } |
131 | |
132 | if (0 < preg_match_all('/\s*define\s*\(\s*["\']([^"\']*)["\']\s*,\s*["\'][^"\']*["\']\s*\)\s*;/', $contents, $constants)) { |
133 | foreach ($constants[1] as $constant) { |
134 | $constantObj = new ConstantSymbol($constant, $file); |
135 | $this->add($constantObj); |
136 | } |
137 | } |
138 | |
139 | // TODO traits |
140 | |
141 | // TODO: Is the ";" in this still correct since it's being taken care of in the regex just above? |
142 | // Looks like with the preceding regex, it will never match. |
143 | |
144 | preg_replace_callback( |
145 | ' |
146 | ~ # Start the pattern |
147 | [\r\n]+\s*namespace\s+([a-zA-Z0-9_\x7f-\xff\\\\]+)[;{\s\n]{1}[\s\S]*?(?=namespace|$) |
148 | # Look for a preceding namespace declaration, |
149 | # followed by a semicolon, open curly bracket, space or new line |
150 | # up until a |
151 | # potential second namespace declaration or end of file. |
152 | # if found, match that much before continuing the search on |
153 | | # the remainder of the string. |
154 | \/\*[\s\S]*?\*\/ | # Skip multiline comments |
155 | ^\s*\/\/.*$ | # Skip single line comments |
156 | \s* # Whitespace is allowed before |
157 | (?:abstract\sclass|class|interface)\s+ # Look behind for class, abstract class, interface |
158 | ([a-zA-Z0-9_\x7f-\xff]+) # Match the word until the first non-classname-valid character |
159 | \s? # Allow a space after |
160 | (?:{|extends|implements|\n|$) # Class declaration can be followed by {, extends, implements |
161 | # or a new line |
162 | ~x', // # x: ignore whitespace in regex. |
163 | function ($matches) use ($file) { |
164 | |
165 | // If we're inside a namespace other than the global namespace: |
166 | if (1 === preg_match('/^\s*namespace\s+[a-zA-Z0-9_\x7f-\xff\\\\]+[;{\s\n]{1}.*/', $matches[0])) { |
167 | $this->addDiscoveredNamespaceChange($matches[1], $file); |
168 | |
169 | return $matches[0]; |
170 | } |
171 | |
172 | if (count($matches) < 3) { |
173 | return $matches[0]; |
174 | } |
175 | |
176 | // TODO: Why is this [2] and not [1] (which seems to be always empty). |
177 | $this->addDiscoveredClassChange($matches[2], $file); |
178 | |
179 | return $matches[0]; |
180 | }, |
181 | $contents |
182 | ); |
183 | |
184 | $parser = (new ParserFactory())->createForNewestSupportedVersion(); |
185 | $ast = $parser->parse($contents); |
186 | |
187 | $traverser = new NodeTraverser(); |
188 | $visitor = new class extends \PhpParser\NodeVisitorAbstract { |
189 | protected array $functions = []; |
190 | public function enterNode(Node $node) |
191 | { |
192 | if ($node instanceof Node\Stmt\Function_) { |
193 | $this->functions[] = $node->name->name; |
194 | } |
195 | return $node; |
196 | } |
197 | |
198 | /** |
199 | * @return string[] Function names. |
200 | */ |
201 | public function getFunctions(): array |
202 | { |
203 | return $this->functions; |
204 | } |
205 | }; |
206 | $traverser->addVisitor($visitor); |
207 | |
208 | /** @var Node $node */ |
209 | foreach ((array) $ast as $node) { |
210 | $traverser->traverse([ $node ]); |
211 | } |
212 | foreach ($visitor->getFunctions() as $functionName) { |
213 | if (in_array($functionName, $this->getBuiltIns())) { |
214 | continue; |
215 | } |
216 | $functionSymbol = new FunctionSymbol($functionName, $file); |
217 | $this->add($functionSymbol); |
218 | } |
219 | } |
220 | |
221 | protected function addDiscoveredClassChange(string $classname, File $file): void |
222 | { |
223 | // TODO: This should be included but marked not to prefix. |
224 | if (in_array($classname, $this->getBuiltIns())) { |
225 | return; |
226 | } |
227 | |
228 | $classSymbol = new ClassSymbol($classname, $file); |
229 | $this->add($classSymbol); |
230 | } |
231 | |
232 | protected function addDiscoveredNamespaceChange(string $namespace, File $file): void |
233 | { |
234 | |
235 | foreach ($this->excludeNamespacesFromPrefixing as $excludeNamespace) { |
236 | if (0 === strpos($namespace, $excludeNamespace)) { |
237 | // TODO: Log. |
238 | return; |
239 | } |
240 | } |
241 | |
242 | $namespaceObj = $this->discoveredSymbols->getNamespace($namespace); |
243 | if ($namespaceObj) { |
244 | $namespaceObj->addSourceFile($file); |
245 | $file->addDiscoveredSymbol($namespaceObj); |
246 | return; |
247 | } else { |
248 | $namespaceObj = new NamespaceSymbol($namespace, $file); |
249 | } |
250 | |
251 | $this->add($namespaceObj); |
252 | } |
253 | |
254 | /** |
255 | * Get a list of PHP built-in classes etc. so they are not prefixed. |
256 | * |
257 | * Polyfilled classes were being prefixed, but the polyfills are only active when the PHP version is below X, |
258 | * so calls to those prefixed polyfilled classnames would fail on newer PHP versions. |
259 | * |
260 | * NB: This list is not exhaustive. Any unloaded PHP extensions are not included. |
261 | * |
262 | * @see https://github.com/BrianHenryIE/strauss/issues/79 |
263 | * |
264 | * ``` |
265 | * array_filter( |
266 | * get_declared_classes(), |
267 | * function(string $className): bool { |
268 | * $reflector = new \ReflectionClass($className); |
269 | * return empty($reflector->getFileName()); |
270 | * } |
271 | * ); |
272 | * ``` |
273 | * |
274 | * @return string[] |
275 | */ |
276 | protected function getBuiltIns(): array |
277 | { |
278 | if (empty($this->builtIns)) { |
279 | $this->loadBuiltIns(); |
280 | } |
281 | |
282 | return $this->builtIns; |
283 | } |
284 | |
285 | /** |
286 | * Load the file containing the built-in PHP classes etc. and flatten to a single array of strings and store. |
287 | */ |
288 | protected function loadBuiltIns(): void |
289 | { |
290 | $builtins = include __DIR__ . '/FileSymbol/builtinsymbols.php'; |
291 | |
292 | $flatArray = array(); |
293 | array_walk_recursive( |
294 | $builtins, |
295 | function ($array) use (&$flatArray) { |
296 | if (is_array($array)) { |
297 | $flatArray = array_merge($flatArray, array_values($array)); |
298 | } else { |
299 | $flatArray[] = $array; |
300 | } |
301 | } |
302 | ); |
303 | |
304 | $this->builtIns = $flatArray; |
305 | } |
306 | } |