Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.91% covered (warning)
81.91%
231 / 282
33.33% covered (danger)
33.33%
4 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Prefixer
81.91% covered (warning)
81.91%
231 / 282
33.33% covered (danger)
33.33%
4 / 12
146.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 replaceInFiles
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 replaceInProjectFiles
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 replaceInString
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
8.14
 replaceNamespace
85.19% covered (warning)
85.19%
23 / 27
0.00% covered (danger)
0.00%
0 / 1
4.05
 replaceClassname
83.33% covered (warning)
83.33%
25 / 30
0.00% covered (danger)
0.00%
0 / 1
8.30
 replaceGlobalClassInsideNamedNamespace
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 replaceConstants
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 replaceConstant
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceFunctions
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
4
 getModifiedFiles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prepareRelativeNamespaces
93.69% covered (success)
93.69%
104 / 111
0.00% covered (danger)
0.00%
0 / 1
53.70
1<?php
2
3namespace BrianHenryIE\Strauss\Pipeline;
4
5use BrianHenryIE\Strauss\Composer\ComposerPackage;
6use BrianHenryIE\Strauss\Config\PrefixerConfigInterface;
7use BrianHenryIE\Strauss\Files\File;
8use BrianHenryIE\Strauss\Helpers\FileSystem;
9use BrianHenryIE\Strauss\Helpers\NamespaceSort;
10use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
11use BrianHenryIE\Strauss\Types\FunctionSymbol;
12use BrianHenryIE\Strauss\Types\NamespaceSymbol;
13use Exception;
14use League\Flysystem\FilesystemException;
15use PhpParser\Node;
16use PhpParser\NodeTraverser;
17use PhpParser\ParserFactory;
18use PhpParser\PrettyPrinter\Standard;
19use Psr\Log\LoggerAwareTrait;
20use Psr\Log\LoggerInterface;
21use Psr\Log\NullLogger;
22
23class 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}