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