Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.91% |
231 / 282 |
|
33.33% |
4 / 12 |
CRAP | |
0.00% |
0 / 1 |
Prefixer | |
81.91% |
231 / 282 |
|
33.33% |
4 / 12 |
146.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
replaceInFiles | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
replaceInProjectFiles | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
replaceInString | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
8.14 | |||
replaceNamespace | |
85.19% |
23 / 27 |
|
0.00% |
0 / 1 |
4.05 | |||
replaceClassname | |
83.33% |
25 / 30 |
|
0.00% |
0 / 1 |
8.30 | |||
replaceGlobalClassInsideNamedNamespace | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
1 | |||
replaceConstants | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
replaceConstant | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
replaceFunctions | |
96.77% |
30 / 31 |
|
0.00% |
0 / 1 |
4 | |||
getModifiedFiles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prepareRelativeNamespaces | |
93.69% |
104 / 111 |
|
0.00% |
0 / 1 |
53.70 |
1 | <?php |
2 | |
3 | namespace BrianHenryIE\Strauss\Pipeline; |
4 | |
5 | use BrianHenryIE\Strauss\Composer\ComposerPackage; |
6 | use BrianHenryIE\Strauss\Config\PrefixerConfigInterface; |
7 | use BrianHenryIE\Strauss\Files\File; |
8 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
9 | use BrianHenryIE\Strauss\Helpers\NamespaceSort; |
10 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
11 | use BrianHenryIE\Strauss\Types\FunctionSymbol; |
12 | use BrianHenryIE\Strauss\Types\NamespaceSymbol; |
13 | use Exception; |
14 | use League\Flysystem\FilesystemException; |
15 | use PhpParser\Node; |
16 | use PhpParser\NodeTraverser; |
17 | use PhpParser\ParserFactory; |
18 | use PhpParser\PrettyPrinter\Standard; |
19 | use Psr\Log\LoggerAwareTrait; |
20 | use Psr\Log\LoggerInterface; |
21 | use Psr\Log\NullLogger; |
22 | |
23 | class Prefixer |
24 | { |
25 | use LoggerAwareTrait; |
26 | |
27 | protected PrefixerConfigInterface $config; |
28 | |
29 | protected FileSystem $filesystem; |
30 | |
31 | /** |
32 | * array<$filePath, $package> or null if the file is not from a dependency (i.e. a project file). |
33 | * |
34 | * @var array<string, ?ComposerPackage> |
35 | */ |
36 | protected array $changedFiles = array(); |
37 | |
38 | public function __construct( |
39 | PrefixerConfigInterface $config, |
40 | FileSystem $filesystem, |
41 | ?LoggerInterface $logger = null |
42 | ) { |
43 | $this->config = $config; |
44 | $this->filesystem = $filesystem; |
45 | $this->logger = $logger ?? new NullLogger(); |
46 | } |
47 | |
48 | // Don't replace a classname if there's an import for a class with the same name. |
49 | // but do replace \Classname always |
50 | |
51 | /** |
52 | * @param DiscoveredSymbols $discoveredSymbols |
53 | * ///param array<string,array{dependency:ComposerPackage,sourceAbsoluteFilepath:string,targetRelativeFilepath:string}> $phpFileArrays |
54 | * @param array<File> $files |
55 | * |
56 | * @throws FilesystemException |
57 | * @throws FilesystemException |
58 | */ |
59 | public function replaceInFiles(DiscoveredSymbols $discoveredSymbols, array $files): void |
60 | { |
61 | foreach ($files as $file) { |
62 | if ($this->filesystem->directoryExists($file->getAbsoluteTargetPath())) { |
63 | $this->logger->debug("is_dir() / nothing to do : {$file->getAbsoluteTargetPath()}"); |
64 | continue; |
65 | } |
66 | |
67 | if (! $this->filesystem->fileExists($file->getAbsoluteTargetPath())) { |
68 | $this->logger->warning("Expected file does not exist: {$file->getAbsoluteTargetPath()}"); |
69 | continue; |
70 | } |
71 | |
72 | if (!$file->isPhpFile()) { |
73 | continue; |
74 | } |
75 | |
76 | /** |
77 | * Throws an exception, but unlikely to happen. |
78 | */ |
79 | $contents = $this->filesystem->read($file->getAbsoluteTargetPath()); |
80 | |
81 | $updatedContents = $this->replaceInString($discoveredSymbols, $contents); |
82 | |
83 | if ($updatedContents !== $contents) { |
84 | // TODO: diff here and debug log. |
85 | $file->setDidUpdate(); |
86 | $this->filesystem->write($file->getAbsoluteTargetPath(), $updatedContents); |
87 | $this->logger->info('Updated contents of file: ' . $file->getAbsoluteTargetPath()); |
88 | } else { |
89 | $this->logger->debug('No changes to file: ' . $file->getAbsoluteTargetPath()); |
90 | } |
91 | } |
92 | } |
93 | |
94 | /** |
95 | * @param DiscoveredSymbols $discoveredSymbols |
96 | * @param string[] $absoluteFilePathsArray |
97 | * |
98 | * @return void |
99 | * @throws FilesystemException |
100 | */ |
101 | public function replaceInProjectFiles(DiscoveredSymbols $discoveredSymbols, array $absoluteFilePathsArray): void |
102 | { |
103 | |
104 | foreach ($absoluteFilePathsArray as $fileAbsolutePath) { |
105 | if ($this->filesystem->directoryExists($fileAbsolutePath)) { |
106 | $this->logger->debug("is_dir() / nothing to do : {$fileAbsolutePath}"); |
107 | continue; |
108 | } |
109 | |
110 | if (! $this->filesystem->fileExists($fileAbsolutePath)) { |
111 | $this->logger->warning("Expected file does not exist: {$fileAbsolutePath}"); |
112 | continue; |
113 | } |
114 | |
115 | // Throws an exception, but unlikely to happen. |
116 | $contents = $this->filesystem->read($fileAbsolutePath); |
117 | |
118 | $updatedContents = $this->replaceInString($discoveredSymbols, $contents); |
119 | |
120 | if ($updatedContents !== $contents) { |
121 | $this->changedFiles[ $fileAbsolutePath ] = null; |
122 | $this->filesystem->write($fileAbsolutePath, $updatedContents); |
123 | $this->logger->info('Updated contents of file: ' . $fileAbsolutePath); |
124 | } else { |
125 | $this->logger->debug('No changes to file: ' . $fileAbsolutePath); |
126 | } |
127 | } |
128 | } |
129 | |
130 | /** |
131 | * @param DiscoveredSymbols $discoveredSymbols |
132 | * @param string $contents |
133 | * |
134 | * @throws Exception |
135 | */ |
136 | public function replaceInString(DiscoveredSymbols $discoveredSymbols, string $contents): string |
137 | { |
138 | $namespacesChanges = $discoveredSymbols->getDiscoveredNamespaces($this->config->getNamespacePrefix()); |
139 | $classes = $discoveredSymbols->getDiscoveredClasses($this->config->getClassmapPrefix()); |
140 | $constants = $discoveredSymbols->getDiscoveredConstants($this->config->getConstantsPrefix()); |
141 | $functions = $discoveredSymbols->getDiscoveredFunctions(); |
142 | |
143 | $contents = $this->prepareRelativeNamespaces($contents, $namespacesChanges); |
144 | |
145 | foreach ($classes as $originalClassname) { |
146 | $classmapPrefix = $this->config->getClassmapPrefix(); |
147 | if ($classmapPrefix) { |
148 | $contents = $this->replaceClassname($contents, $originalClassname, $classmapPrefix); |
149 | } |
150 | } |
151 | |
152 | // TODO: Move this out of the loop. |
153 | $namespacesChangesStrings = []; |
154 | foreach ($namespacesChanges as $originalNamespace => $namespaceSymbol) { |
155 | if (in_array($originalNamespace, $this->config->getExcludeNamespacesFromPrefixing())) { |
156 | $this->logger->info("Skipping namespace: $originalNamespace"); |
157 | continue; |
158 | } |
159 | $namespacesChangesStrings[$originalNamespace] = $namespaceSymbol->getReplacement(); |
160 | } |
161 | // This matters... it shouldn't. |
162 | uksort($namespacesChangesStrings, new NamespaceSort(NamespaceSort::SHORTEST)); |
163 | foreach ($namespacesChangesStrings as $originalNamespace => $replacementNamespace) { |
164 | $contents = $this->replaceNamespace($contents, $originalNamespace, $replacementNamespace); |
165 | } |
166 | |
167 | if (!is_null($this->config->getConstantsPrefix())) { |
168 | $contents = $this->replaceConstants($contents, $constants, $this->config->getConstantsPrefix()); |
169 | } |
170 | |
171 | foreach ($functions as $functionSymbol) { |
172 | $contents = $this->replaceFunctions($contents, $functionSymbol); |
173 | } |
174 | |
175 | return $contents; |
176 | } |
177 | |
178 | /** |
179 | * TODO: Test against traits. |
180 | * |
181 | * @param string $contents The text to make replacements in. |
182 | * @param string $originalNamespace |
183 | * @param string $replacement |
184 | * |
185 | * @return string The updated text. |
186 | * @throws Exception |
187 | */ |
188 | public function replaceNamespace(string $contents, string $originalNamespace, string $replacement): string |
189 | { |
190 | |
191 | $searchNamespace = '\\'.rtrim($originalNamespace, '\\') . '\\'; |
192 | $searchNamespace = str_replace('\\\\', '\\', $searchNamespace); |
193 | $searchNamespace = str_replace('\\', '\\\\{0,2}', $searchNamespace); |
194 | |
195 | $pattern = " |
196 | / # Start the pattern |
197 | ( |
198 | ^\s* # start of the string |
199 | |\\n\s* # start of the line |
200 | |(<?php\s+namespace|^\s*namespace|[\r\n]+\s*namespace)\s+ # the namespace keyword |
201 | |use\s+ # the use keyword |
202 | |use\s+function\s+ # the use function syntax |
203 | |new\s+ |
204 | |static\s+ |
205 | |\" # inside a string that does not contain spaces - needs work |
206 | |' # right now its just inside a string that doesnt start with a space |
207 | |implements\s+ |
208 | |extends\s+ # when the class being extended is namespaced inline |
209 | |return\s+ |
210 | |instanceof\s+ # when checking the class type of an object in a conditional |
211 | |\(\s* # inside a function declaration as the first parameters type |
212 | |,\s* # inside a function declaration as a subsequent parameter type |
213 | |\.\s* # as part of a concatenated string |
214 | |=\s* # as the value being assigned to a variable |
215 | |\*\s+@\w+\s* # In a comments param etc |
216 | |&\s* # a static call as a second parameter of an if statement |
217 | |\|\s* |
218 | |!\s* # negating the result of a static call |
219 | |=>\s* # as the value in an associative array |
220 | |\[\s* # In a square array |
221 | |\?\s* # In a ternary operator |
222 | |:\s* # In a ternary operator |
223 | |< # In a generic type declaration |
224 | |\(string\)\s* # casting a namespaced class to a string |
225 | ) |
226 | @? # Maybe preceded by the @ symbol for error suppression |
227 | (?<searchNamespace> |
228 | {$searchNamespace} # followed by the namespace to replace |
229 | ) |
230 | (?!:) # Not followed by : which would only be valid after a classname |
231 | ( |
232 | \s*; # followed by a semicolon |
233 | |\s*{ # or an opening brace for multiple namespaces per file |
234 | |\\\\{1,2}[a-zA-Z0-9_\x7f-\xff]{1,} # or a classname no slashes |
235 | |\s+as # or the keyword as |
236 | |\" # or quotes |
237 | |' # or single quote |
238 | |: # or a colon to access a static |
239 | |\\\\{ |
240 | |> # In a generic type declaration (end) |
241 | ) |
242 | /Ux"; // U: Non-greedy matching, x: ignore whitespace in pattern. |
243 | |
244 | $replacingFunction = function ($matches) use ($originalNamespace, $replacement) { |
245 | $singleBackslash = '\\'; |
246 | $doubleBackslash = '\\\\'; |
247 | |
248 | if (false !== strpos($matches['0'], $doubleBackslash)) { |
249 | $originalNamespace = str_replace($singleBackslash, $doubleBackslash, $originalNamespace); |
250 | $replacement = str_replace($singleBackslash, $doubleBackslash, $replacement); |
251 | } |
252 | |
253 | return str_replace($originalNamespace, $replacement, $matches[0]); |
254 | }; |
255 | |
256 | $result = preg_replace_callback($pattern, $replacingFunction, $contents); |
257 | |
258 | $matchingError = preg_last_error(); |
259 | if (0 !== $matchingError) { |
260 | $message = "Matching error {$matchingError}"; |
261 | if (PREG_BACKTRACK_LIMIT_ERROR === $matchingError) { |
262 | $message = 'Preg Backtrack limit was exhausted!'; |
263 | } |
264 | throw new Exception($message); |
265 | } |
266 | |
267 | // For prefixed functions which do not begin with a backslash, add one. |
268 | // I'm not certain this is a good idea. |
269 | // @see https://github.com/BrianHenryIE/strauss/issues/65 |
270 | $functionReplacingPattern = '/\\\\?('.preg_quote(ltrim($replacement, '\\'), '/').'\\\\(?:[a-zA-Z0-9_\x7f-\xff]+\\\\)*[a-zA-Z0-9_\x7f-\xff]+\\()/'; |
271 | |
272 | return preg_replace( |
273 | $functionReplacingPattern, |
274 | "\\\\$1", |
275 | $result |
276 | ); |
277 | } |
278 | |
279 | /** |
280 | * In a namespace: |
281 | * * use \Classname; |
282 | * * new \Classname() |
283 | * |
284 | * In a global namespace: |
285 | * * new Classname() |
286 | * |
287 | * @param string $contents |
288 | * @param string $originalClassname |
289 | * @param string $classnamePrefix |
290 | * |
291 | * @throws Exception |
292 | */ |
293 | public function replaceClassname(string $contents, string $originalClassname, string $classnamePrefix): string |
294 | { |
295 | $searchClassname = preg_quote($originalClassname, '/'); |
296 | |
297 | // This could be more specific if we could enumerate all preceding and proceeding words ("new", "("...). |
298 | $pattern = ' |
299 | / # Start the pattern |
300 | (^\s*namespace|\r\n\s*namespace)\s+[a-zA-Z0-9_\x7f-\xff\\\\]+\s*{(.*?)(namespace|\z) |
301 | # Look for a preceding namespace declaration, up until a |
302 | # potential second namespace declaration. |
303 | | # if found, match that much before continuing the search on |
304 | # the remainder of the string. |
305 | (^\s*namespace|\r\n\s*namespace)\s+[a-zA-Z0-9_\x7f-\xff\\\\]+\s*;(.*) # Skip lines just declaring the namespace. |
306 | | |
307 | ([^a-zA-Z0-9_\x7f-\xff\$\\\])('. $searchClassname . ')([^a-zA-Z0-9_\x7f-\xff\\\]) # outside a namespace the class will not be prefixed with a slash |
308 | |
309 | /xsm'; // # x: ignore whitespace in regex. s dot matches newline, m: ^ and $ match start and end of line |
310 | |
311 | $replacingFunction = function ($matches) use ($originalClassname, $classnamePrefix) { |
312 | |
313 | // If we're inside a namespace other than the global namespace: |
314 | if (1 === preg_match('/\s*namespace\s+[a-zA-Z0-9_\x7f-\xff\\\\]+[;{\s\n]{1}.*/', $matches[0])) { |
315 | return $this->replaceGlobalClassInsideNamedNamespace( |
316 | $matches[0], |
317 | $originalClassname, |
318 | $classnamePrefix |
319 | ); |
320 | } else { |
321 | $newContents = ''; |
322 | foreach ($matches as $index => $captured) { |
323 | if (0 === $index) { |
324 | continue; |
325 | } |
326 | |
327 | if ($captured == $originalClassname) { |
328 | $newContents .= $classnamePrefix; |
329 | } |
330 | |
331 | $newContents .= $captured; |
332 | } |
333 | return $newContents; |
334 | } |
335 | // return $matches[1] . $matches[2] . $matches[3] . $classnamePrefix . $originalClassname . $matches[5]; |
336 | }; |
337 | |
338 | $result = preg_replace_callback($pattern, $replacingFunction, $contents); |
339 | |
340 | if (is_null($result)) { |
341 | throw new Exception('preg_replace_callback returned null'); |
342 | } |
343 | |
344 | $matchingError = preg_last_error(); |
345 | if (0 !== $matchingError) { |
346 | $message = "Matching error {$matchingError}"; |
347 | if (PREG_BACKTRACK_LIMIT_ERROR === $matchingError) { |
348 | $message = 'Backtrack limit was exhausted!'; |
349 | } |
350 | throw new Exception($message); |
351 | } |
352 | |
353 | return $result; |
354 | } |
355 | |
356 | /** |
357 | * Pass in a string and look for \Classname instances. |
358 | * |
359 | * @param string $contents |
360 | * @param string $originalClassname |
361 | * @param string $classnamePrefix |
362 | * @return string |
363 | */ |
364 | protected function replaceGlobalClassInsideNamedNamespace( |
365 | string $contents, |
366 | string $originalClassname, |
367 | string $classnamePrefix |
368 | ): string { |
369 | $replacement = $classnamePrefix . $originalClassname; |
370 | |
371 | // use Prefixed_Class as Class; |
372 | $usePattern = '/ |
373 | (\s*use\s+) |
374 | ('.$originalClassname.') # Followed by the classname |
375 | \s*; |
376 | /x'; // # x: ignore whitespace in regex. |
377 | |
378 | $contents = preg_replace_callback( |
379 | $usePattern, |
380 | function ($matches) use ($replacement) { |
381 | return $matches[1] . $replacement . ' as '. $matches[2] . ';'; |
382 | }, |
383 | $contents |
384 | ); |
385 | |
386 | $bodyPattern = |
387 | '/([^a-zA-Z0-9_\x7f-\xff] # Not a class character |
388 | \\\) # Followed by a backslash to indicate global namespace |
389 | ('.$originalClassname.') # Followed by the classname |
390 | ([^\\\;]{1}) # Not a backslash or semicolon which might indicate a namespace |
391 | /x'; // # x: ignore whitespace in regex. |
392 | |
393 | return preg_replace_callback( |
394 | $bodyPattern, |
395 | function ($matches) use ($replacement) { |
396 | return $matches[1] . $replacement . $matches[3]; |
397 | }, |
398 | $contents |
399 | ) ?? $contents; // TODO: If this happens, it should raise an exception. |
400 | } |
401 | |
402 | /** |
403 | * TODO: This should be split and brought to FileScanner. |
404 | * |
405 | * @param string $contents |
406 | * @param string[] $originalConstants |
407 | * @param string $prefix |
408 | */ |
409 | protected function replaceConstants(string $contents, array $originalConstants, string $prefix): string |
410 | { |
411 | |
412 | foreach ($originalConstants as $constant) { |
413 | $contents = $this->replaceConstant($contents, $constant, $prefix . $constant); |
414 | } |
415 | |
416 | return $contents; |
417 | } |
418 | |
419 | protected function replaceConstant(string $contents, string $originalConstant, string $replacementConstant): string |
420 | { |
421 | return str_replace($originalConstant, $replacementConstant, $contents); |
422 | } |
423 | |
424 | protected function replaceFunctions(string $contents, FunctionSymbol $functionSymbol): string |
425 | { |
426 | $originalFunctionString = $functionSymbol->getOriginalSymbol(); |
427 | $replacementFunctionString = $functionSymbol->getReplacement(); |
428 | |
429 | if ($originalFunctionString === $replacementFunctionString) { |
430 | return $contents; |
431 | } |
432 | |
433 | $functionsUsingCallable = [ |
434 | 'function_exists', |
435 | 'call_user_func', |
436 | 'call_user_func_array', |
437 | 'forward_static_call', |
438 | 'forward_static_call_array', |
439 | 'register_shutdown_function', |
440 | 'register_tick_function', |
441 | 'unregister_tick_function', |
442 | ]; |
443 | // TODO: Immediately surrounded by quotes is sometimes valid, e.g. passing a callable, but not always. |
444 | // Log cases like this and present a list to users. Maybe CLI confirmation to replace? |
445 | |
446 | $pattern = '/ |
447 | (\s*use\s+function\s+)('.preg_quote($originalFunctionString, '/').')(\s+as|\s+;) # use function as |
448 | | |
449 | |('.implode('|', $functionsUsingCallable).')(\s*\(\s*[\'"])('.preg_quote($originalFunctionString, '/').')([\'"]) # function related calls without closing bracket |
450 | | |
451 | (\s*function\s+)('.preg_quote($originalFunctionString, '/').')(\s*\() # function declaration |
452 | | |
453 | ([;\s]+)('.preg_quote($originalFunctionString, '/').')(\s*\() # function call |
454 | /x'; // x: ignore whitespace in regex. |
455 | |
456 | return preg_replace_callback( |
457 | $pattern, |
458 | function ($matches) use ($originalFunctionString, $replacementFunctionString) { |
459 | foreach ($matches as $index => $match) { |
460 | if ($match == $originalFunctionString) { |
461 | $matches[$index] = $replacementFunctionString; |
462 | } |
463 | } |
464 | unset($matches[0]); |
465 | return implode('', $matches); |
466 | }, |
467 | $contents |
468 | ); |
469 | } |
470 | |
471 | /** |
472 | * TODO: This should be a function on {@see DiscoveredFiles}. |
473 | * |
474 | * @return array<string, ComposerPackage> |
475 | */ |
476 | public function getModifiedFiles(): array |
477 | { |
478 | return $this->changedFiles; |
479 | } |
480 | |
481 | /** |
482 | * In the case of `use Namespaced\Traitname;` by `nette/latte`, the trait uses the full namespace but it is not |
483 | * preceded by a backslash. When everything is moved up a namespace level, this is a problem. I think being |
484 | * explicit about the namespace being a full namespace rather than a relative one should fix this. |
485 | * |
486 | * We will scan the file for `use Namespaced\Traitname` and replace it with `use \Namespaced\Traitname;`. |
487 | * |
488 | * @see https://github.com/nette/latte/blob/0ac0843a459790d471821f6a82f5d13db831a0d3/src/Latte/Loaders/FileLoader.php#L20 |
489 | * |
490 | * @param string $phpFileContent |
491 | * @param NamespaceSymbol[] $discoveredNamespaceSymbols |
492 | */ |
493 | protected function prepareRelativeNamespaces(string $phpFileContent, array $discoveredNamespaceSymbols): string |
494 | { |
495 | $parser = (new ParserFactory())->createForNewestSupportedVersion(); |
496 | |
497 | $ast = $parser->parse($phpFileContent); |
498 | |
499 | $traverser = new NodeTraverser(); |
500 | $visitor = new class($discoveredNamespaceSymbols) extends \PhpParser\NodeVisitorAbstract { |
501 | |
502 | public int $countChanges = 0; |
503 | protected array $discoveredNamespaces; |
504 | |
505 | protected Node $lastNode; |
506 | |
507 | /** |
508 | * The list of `use Namespace\Subns;` statements in the file. |
509 | * |
510 | * @var string[] |
511 | */ |
512 | protected array $using = []; |
513 | |
514 | public function __construct(array $discoveredNamespaceSymbols) |
515 | { |
516 | |
517 | $this->discoveredNamespaces = array_map( |
518 | fn(NamespaceSymbol $symbol) => $symbol->getOriginalSymbol(), |
519 | $discoveredNamespaceSymbols |
520 | ); |
521 | } |
522 | |
523 | public function leaveNode(Node $node) |
524 | { |
525 | |
526 | if ($node instanceof \PhpParser\Node\Stmt\Namespace_) { |
527 | $this->using[] = $node->name->name; |
528 | $this->lastNode = $node; |
529 | return $node; |
530 | } |
531 | // Probably the namespace declaration |
532 | if (empty($this->lastNode) && $node instanceof \PhpParser\Node\Name) { |
533 | $this->using[] = $node->name; |
534 | $this->lastNode = $node; |
535 | return $node; |
536 | } |
537 | if ($node instanceof \PhpParser\Node\Name) { |
538 | return $node; |
539 | } |
540 | if ($node instanceof \PhpParser\Node\Stmt\Use_) { |
541 | foreach ($node->uses as $use) { |
542 | $use->name->name = ltrim($use->name->name, '\\'); |
543 | $this->using[] = $use->name->name; |
544 | } |
545 | $this->lastNode = $node; |
546 | return $node; |
547 | } |
548 | if ($node instanceof \PhpParser\Node\UseItem) { |
549 | return $node; |
550 | } |
551 | |
552 | $nameNodes = []; |
553 | |
554 | $docComment = $node->getDocComment(); |
555 | if ($docComment) { |
556 | foreach ($this->discoveredNamespaces as $namespace) { |
557 | $updatedDocCommentText = preg_replace( |
558 | '/(.*\*\s*@\w+\s+)('.preg_quote($namespace, '/').')/', |
559 | '$1\\\\$2', |
560 | $docComment->getText(), |
561 | 1, |
562 | $count |
563 | ); |
564 | if ($count > 0) { |
565 | $this->countChanges ++; |
566 | $node->setDocComment(new \PhpParser\Comment\Doc($updatedDocCommentText)); |
567 | break; |
568 | } |
569 | } |
570 | } |
571 | |
572 | if ($node instanceof \PhpParser\Node\Stmt\TraitUse) { |
573 | $nameNodes = array_merge($nameNodes, $node->traits); |
574 | } |
575 | |
576 | if ($node instanceof \PhpParser\Node\Param |
577 | && $node->type instanceof \PhpParser\Node\Name |
578 | && !($node->type instanceof \PhpParser\Node\Name\FullyQualified)) { |
579 | $nameNodes[] = $node->type; |
580 | } |
581 | |
582 | if ($node instanceof \PhpParser\Node\NullableType |
583 | && $node->type instanceof \PhpParser\Node\Name |
584 | && !($node->type instanceof \PhpParser\Node\Name\FullyQualified)) { |
585 | $nameNodes[] = $node->type; |
586 | } |
587 | |
588 | if ($node instanceof \PhpParser\Node\Stmt\ClassMethod |
589 | && $node->returnType instanceof \PhpParser\Node\Name |
590 | && !($node->returnType instanceof \PhpParser\Node\Name\FullyQualified)) { |
591 | $nameNodes[] = $node->returnType; |
592 | } |
593 | |
594 | if ($node instanceof \PhpParser\Node\Expr\ClassConstFetch |
595 | && $node->class instanceof \PhpParser\Node\Name |
596 | && !($node->class instanceof \PhpParser\Node\Name\FullyQualified)) { |
597 | $nameNodes[] = $node->class; |
598 | } |
599 | |
600 | if ($node instanceof \PhpParser\Node\Expr\StaticPropertyFetch |
601 | && $node->class instanceof \PhpParser\Node\Name |
602 | && !($node->class instanceof \PhpParser\Node\Name\FullyQualified)) { |
603 | $nameNodes[] = $node->class; |
604 | } |
605 | |
606 | if (property_exists($node, 'name') |
607 | && $node->name instanceof \PhpParser\Node\Name |
608 | && !($node->name instanceof \PhpParser\Node\Name\FullyQualified) |
609 | ) { |
610 | $nameNodes[] = $node->name; |
611 | } |
612 | |
613 | if ($node instanceof \PhpParser\Node\Expr\StaticCall) { |
614 | if (!method_exists($node->class, 'isFullyQualified') || !$node->class->isFullyQualified()) { |
615 | $nameNodes[] = $node->class; |
616 | } |
617 | } |
618 | |
619 | if ($node instanceof \PhpParser\Node\Stmt\TryCatch) { |
620 | foreach ($node->catches as $catch) { |
621 | foreach ($catch->types as $catchType) { |
622 | if ($catchType instanceof \PhpParser\Node\Name |
623 | && !($catchType instanceof \PhpParser\Node\Name\FullyQualified) |
624 | ) { |
625 | $nameNodes[] = $catchType; |
626 | } |
627 | } |
628 | } |
629 | } |
630 | |
631 | if ($node instanceof \PhpParser\Node\Stmt\Class_) { |
632 | foreach ($node->implements as $implement) { |
633 | if ($implement instanceof \PhpParser\Node\Name |
634 | && !($implement instanceof \PhpParser\Node\Name\FullyQualified)) { |
635 | $nameNodes[] = $implement; |
636 | } |
637 | } |
638 | } |
639 | if ($node instanceof \PhpParser\Node\Expr\Instanceof_ |
640 | && $node->class instanceof \PhpParser\Node\Name |
641 | && !($node->class instanceof \PhpParser\Node\Name\FullyQualified)) { |
642 | $nameNodes[] = $node->class; |
643 | } |
644 | |
645 | foreach ($nameNodes as $nameNode) { |
646 | if (!property_exists($nameNode, 'name')) { |
647 | continue; |
648 | } |
649 | // If the name contains a `\` but does not begin with one, it may be a relative namespace; |
650 | if (false !== strpos($nameNode->name, '\\') && 0 !== strpos($nameNode->name, '\\')) { |
651 | $parts = explode('\\', $nameNode->name); |
652 | array_pop($parts); |
653 | $namespace = implode('\\', $parts); |
654 | if (in_array($namespace, $this->discoveredNamespaces)) { |
655 | $nameNode->name = '\\' . $nameNode->name; |
656 | $this->countChanges ++; |
657 | } else { |
658 | foreach ($this->using as $namespaceBase) { |
659 | if (in_array($namespaceBase . '\\' . $namespace, $this->discoveredNamespaces)) { |
660 | $nameNode->name = '\\' . $namespaceBase . '\\' . $nameNode->name; |
661 | $this->countChanges ++; |
662 | break; |
663 | } |
664 | } |
665 | } |
666 | } |
667 | } |
668 | $this->lastNode = $node; |
669 | return $node; |
670 | } |
671 | }; |
672 | $traverser->addVisitor($visitor); |
673 | |
674 | $modifiedStmts = $traverser->traverse($ast); |
675 | |
676 | $updatedContent = (new Standard())->prettyPrintFile($modifiedStmts); |
677 | |
678 | $updatedContent = str_replace('namespace \\', 'namespace ', $updatedContent); |
679 | $updatedContent = str_replace('use \\\\', 'use \\', $updatedContent); |
680 | |
681 | return $visitor->countChanges == 0 |
682 | ? $phpFileContent |
683 | : $updatedContent; |
684 | } |
685 | } |