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