Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.00% covered (danger)
18.00%
18 / 100
18.75% covered (danger)
18.75%
3 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
MarkSymbolsForRenaming
18.00% covered (danger)
18.00%
18 / 100
18.75% covered (danger)
18.75%
3 / 16
1661.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 scanSymbols
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
11.06
 fileIsAutoloaded
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 excludeFromPrefix
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 isSymbolFoundInFileThatIsNotCopied
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 isSymbolFoundInFileThatIsCopied
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 isExcludeFromCopyPackage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isExcludeFromPrefixPackage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 isExcludeFromPrefixNamespace
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 isExcludedFromPrefixFilePattern
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 isExcludeConstants
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isExcludeConstantsPackage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isExcludeConstantsNamespace
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 isExcludedConstantsFilePattern
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 isExcludeConstantName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preparePattern
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Symbols found in autoloaded files should be prefixed, unless:
5 * * The `exclude_from_prefix` rules apply to the discovered symbols.
6 * * The file is in `exclude_from_copy`
7 */
8
9namespace BrianHenryIE\Strauss\Pipeline;
10
11use BrianHenryIE\Strauss\Config\MarkSymbolsForRenamingConfigInterface;
12use BrianHenryIE\Strauss\Files\File;
13use BrianHenryIE\Strauss\Files\FileBase;
14use BrianHenryIE\Strauss\Helpers\FileSystem;
15use BrianHenryIE\Strauss\Types\ConstantSymbol;
16use BrianHenryIE\Strauss\Types\DiscoveredSymbol;
17use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
18use BrianHenryIE\Strauss\Types\NamespaceSymbol;
19use Psr\Log\LoggerAwareTrait;
20use Psr\Log\LoggerInterface;
21
22class MarkSymbolsForRenaming
23{
24    use LoggerAwareTrait;
25
26    protected MarkSymbolsForRenamingConfigInterface $config;
27
28    protected FileSystem $filesystem;
29
30    public function __construct(
31        MarkSymbolsForRenamingConfigInterface $config,
32        FileSystem                            $filesystem,
33        LoggerInterface                       $logger
34    ) {
35        $this->config = $config;
36        $this->filesystem = $filesystem;
37        $this->setLogger($logger);
38    }
39
40    public function scanSymbols(DiscoveredSymbols $symbols): void
41    {
42        $allSymbols = $symbols->getSymbols();
43        foreach ($allSymbols as $symbol) {
44            // $this->config->getFlatDependencyTree
45
46            if (!$this->fileIsAutoloaded($symbol)) {
47//                $this->logger->debug()
48                $symbol->setDoRename(false);
49                continue;
50            }
51
52            // If the symbol's package is excluded from copy, don't prefix it
53            if ($this->isExcludeFromCopyPackage($symbol->getPackageName())) {
54                $symbol->setDoRename(false);
55                continue;
56            }
57
58            if ($this->excludeFromPrefix($symbol)) {
59                $symbol->setDoRename(false);
60                continue;
61            }
62
63            // Constant-only exclusion: extra.strauss.exclude_constants
64            if ($symbol instanceof ConstantSymbol && $this->isExcludeConstants($symbol)) {
65                $symbol->setDoRename(false);
66                continue;
67            }
68
69//            if ($this->isSymbolFoundInFileThatIsNotCopied($symbol)) {
70//                if (count($symbol->getSourceFiles())===1) {
71//                    $symbol->setDoRename(false);
72//                }
73//            }
74            if ($this->config->getVendorDirectory() !== $this->config->getTargetDirectory()
75                && !$this->isSymbolFoundInFileThatIsCopied($symbol)) {
76                $symbol->setDoRename(false);
77            }
78        }
79    }
80
81    /**
82     * If all the files a symbol is defined in are autoloaded, prefix the symbol.
83     *
84     * There are packages where a class may be defined in two different files and they are conditionally loaded.
85     * TODO: How best to handle this scenario?
86     */
87    protected function fileIsAutoloaded(DiscoveredSymbol $symbol): bool
88    {
89        // The same namespace symbols are found in lots of files so this test isn't useful.
90        if ($symbol instanceof NamespaceSymbol) {
91            return true;
92        }
93
94        $sourceFiles = array_filter(
95            $symbol->getSourceFiles(),
96            fn (FileBase $file) => basename($file->getVendorRelativePath()) !== 'composer.json'
97        );
98
99        return array_reduce(
100            $sourceFiles,
101            fn(bool $carry, FileBase $fileBase) => $carry && $fileBase->isAutoloaded(),
102            true
103        );
104    }
105
106    /**
107     * Check the `exclude_from_prefix` rules for this symbol's package name, namespace and file-paths.
108     */
109    protected function excludeFromPrefix(DiscoveredSymbol $symbol): bool
110    {
111        return $this->isExcludeFromPrefixPackage($symbol->getPackageName())
112            || $this->isExcludeFromPrefixNamespace($symbol->getNamespace())
113            || $this->isExcludedFromPrefixFilePattern($symbol->getSourceFiles());
114    }
115
116    /**
117     * If any of the files the symbol was found in are marked not to prefix, don't prefix the symbol.
118     *
119     * `config.strauss.exclude_from_copy`.
120     *
121     * This requires {@see FileCopyScanner} to have been run first.
122     */
123    protected function isSymbolFoundInFileThatIsNotCopied(DiscoveredSymbol $symbol): bool
124    {
125        if ($this->config->getVendorDirectory() === $this->config->getTargetDirectory()) {
126            return false;
127        }
128
129        return !array_reduce(
130            $symbol->getSourceFiles(),
131            fn(bool $carry, FileBase $file) => $carry && $file->isDoCopy(),
132            true
133        );
134    }
135
136    protected function isSymbolFoundInFileThatIsCopied(DiscoveredSymbol $symbol): bool
137    {
138        if ($this->config->getVendorDirectory() === $this->config->getTargetDirectory()) {
139            return false;
140        }
141
142        return array_reduce(
143            $symbol->getSourceFiles(),
144            fn(bool $carry, FileBase $file) => $carry || $file->isDoCopy(),
145            false
146        );
147    }
148
149    /**
150     * Config: `extra.strauss.exclude_from_copy.packages`.
151     */
152    protected function isExcludeFromCopyPackage(?string $packageName): bool
153    {
154        return !is_null($packageName) && in_array($packageName, $this->config->getExcludePackagesFromCopy(), true);
155    }
156
157    /**
158     * Config: `extra.strauss.exclude_from_prefix.packages`.
159     */
160    protected function isExcludeFromPrefixPackage(?string $packageName): bool
161    {
162        if (is_null($packageName)) {
163            return false;
164        }
165
166        if (in_array(
167            $packageName,
168            $this->config->getExcludePackagesFromPrefixing(),
169            true
170        )) {
171            return true;
172        }
173
174        return false;
175    }
176
177    /**
178     * Config: `extra.strauss.exclude_from_prefix.namespaces`.
179     */
180    protected function isExcludeFromPrefixNamespace(?string $namespace): bool
181    {
182        if (empty($namespace)) {
183            return false;
184        }
185
186        foreach ($this->config->getExcludeNamespacesFromPrefixing() as $excludeNamespace) {
187            if (str_starts_with($namespace, $excludeNamespace)) {
188                return true;
189            }
190        }
191
192        return false;
193    }
194
195    /**
196     * Compares the relative path from the vendor dir with `exclude_file_patterns` config.
197     *
198     * Config: `extra.strauss.exclude_from_prefix.file_patterns`.
199     *
200     * @param array<FileBase> $files
201     */
202    protected function isExcludedFromPrefixFilePattern(array $files): bool
203    {
204        /** @var File $file */
205        foreach ($files as $file) {
206            $absoluteFilePath = $file->getAbsoluteTargetPath();
207            if (empty($absoluteFilePath)) {
208                // root namespace is in a fake file.
209                continue;
210            }
211            $vendorRelativePath = $file->getVendorRelativePath();
212            foreach ($this->config->getExcludeFilePatternsFromPrefixing() as $excludeFilePattern) {
213                if (1 === preg_match($this->preparePattern($excludeFilePattern), $vendorRelativePath)) {
214                    return true;
215                }
216            }
217        }
218        return false;
219    }
220
221    /**
222     * Config: extra.strauss.exclude_constants – applies only to constants.
223     */
224    protected function isExcludeConstants(ConstantSymbol $symbol): bool
225    {
226        return $this->isExcludeConstantsPackage($symbol->getPackageName())
227            || $this->isExcludeConstantsNamespace($symbol->getNamespace())
228            || $this->isExcludedConstantsFilePattern($symbol->getSourceFiles())
229            || $this->isExcludeConstantName($symbol->getOriginalSymbol());
230    }
231
232    protected function isExcludeConstantsPackage(?string $packageName): bool
233    {
234        if (is_null($packageName)) {
235            return false;
236        }
237        return in_array($packageName, $this->config->getExcludePackagesFromConstantPrefixing(), true);
238    }
239
240    protected function isExcludeConstantsNamespace(?string $namespace): bool
241    {
242        if (empty($namespace)) {
243            return false;
244        }
245        foreach ($this->config->getExcludeNamespacesFromConstantPrefixing() as $excludeNamespace) {
246            if (str_starts_with($namespace, $excludeNamespace)) {
247                return true;
248            }
249        }
250        return false;
251    }
252
253    /**
254     * @param array<FileBase> $files
255     */
256    protected function isExcludedConstantsFilePattern(array $files): bool
257    {
258        /** @var File $file */
259        foreach ($files as $file) {
260            $absoluteFilePath = $file->getAbsoluteTargetPath();
261            if (empty($absoluteFilePath)) {
262                continue;
263            }
264            $vendorRelativePath = $file->getVendorRelativePath();
265            foreach ($this->config->getExcludeFilePatternsFromConstantPrefixing() as $excludeFilePattern) {
266                if (1 === preg_match($this->preparePattern($excludeFilePattern), $vendorRelativePath)) {
267                    return true;
268                }
269            }
270        }
271        return false;
272    }
273
274    protected function isExcludeConstantName(string $constantName): bool
275    {
276        return in_array($constantName, $this->config->getExcludeConstantNames(), true);
277    }
278
279    /**
280     * TODO: This should be moved into the class parsing the config.
281     */
282    private function preparePattern(string $pattern): string
283    {
284        $delimiter = '#';
285
286        if (substr($pattern, 0, 1) !== substr($pattern, - 1, 1)) {
287            $pattern = $delimiter . $pattern . $delimiter;
288        }
289
290        return $pattern;
291    }
292}