Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.81% covered (warning)
63.81%
67 / 105
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangeEnumerator
63.81% covered (warning)
63.81%
67 / 105
0.00% covered (danger)
0.00%
0 / 4
61.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 determineReplacements
80.72% covered (warning)
80.72%
67 / 83
0.00% covered (danger)
0.00%
0 / 1
25.47
 globalOrPsr0
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 determineNamespaceReplacement
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Determine the replacements to be made to the discovered symbols.
4 *
5 * Typically, this will just be a prefix, but more complex rules allow for replacements specific to individual symbols/namespaces.
6 */
7
8namespace BrianHenryIE\Strauss\Pipeline;
9
10use BrianHenryIE\Strauss\Config\ChangeEnumeratorConfigInterface;
11use BrianHenryIE\Strauss\Types\ClassSymbol;
12use BrianHenryIE\Strauss\Types\ConstantSymbol;
13use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
14use BrianHenryIE\Strauss\Types\FunctionSymbol;
15use BrianHenryIE\Strauss\Types\NamespacedSymbol;
16use BrianHenryIE\Strauss\Types\NamespaceSymbol;
17use BrianHenryIE\Strauss\Types\Psr0NamespaceSymbol;
18use Psr\Log\LoggerAwareTrait;
19use Psr\Log\LoggerInterface;
20
21class ChangeEnumerator
22{
23    use LoggerAwareTrait;
24
25    protected ChangeEnumeratorConfigInterface $config;
26
27    public function __construct(
28        ChangeEnumeratorConfigInterface $config,
29        LoggerInterface $logger
30    ) {
31        $this->config = $config;
32        $this->setLogger($logger);
33    }
34
35    public function determineReplacements(DiscoveredSymbols $discoveredSymbols): void
36    {
37        $discoveredNamespaces = $discoveredSymbols->getNamespaces();
38
39        foreach ($discoveredNamespaces as $symbol) {
40            if (!$symbol->isDoRename()) {
41                continue;
42            }
43
44            // This line seems redundant.
45            if ($symbol instanceof NamespaceSymbol) {
46                $namespaceReplacementPatterns = $this->config->getNamespaceReplacementPatterns();
47
48                if (in_array(
49                    $symbol->getOriginalFqdnName(),
50                    $this->config->getExcludeNamespacesFromPrefixing(),
51                    true
52                )) {
53                    $symbol->setDoRename(false);
54                }
55
56                // `namespace_prefix` is just a shorthand for a replacement pattern that applies to all namespaces.
57
58                // TODO: Maybe need to preg_quote and add regex delimiters to the patterns here.
59                foreach ($namespaceReplacementPatterns as $pattern => $replacement) {
60                    if (substr($pattern, 0, 1) !== substr($pattern, - 1, 1)) {
61                        unset($namespaceReplacementPatterns[ $pattern ]);
62                        $pattern                                  = '~' . preg_quote($pattern, '~') . '~';
63                        $namespaceReplacementPatterns[ $pattern ] = $replacement;
64                    }
65                    unset($pattern, $replacement);
66                }
67
68                $namespacePrefix = $this->config->getNamespacePrefix();
69                if (! is_null($namespacePrefix)) {
70                    $stripPattern   = '~^(' . preg_quote($namespacePrefix, '~') . '\\\\*)*(.*)~';
71                    $strippedSymbol = preg_replace(
72                        $stripPattern,
73                        '$2',
74                        $symbol->getOriginalFqdnName()
75                    ) ?? (function () {
76                        throw new \Exception(preg_last_error_msg(), preg_last_error());
77                    })();
78                    $namespaceReplacementPatterns[ "~(" . preg_quote($namespacePrefix, '~') . '\\\\*)*' . preg_quote($strippedSymbol, '~') . '~' ]
79                                    = "{$namespacePrefix}\\{$strippedSymbol}";
80                    unset($stripPattern, $strippedSymbol);
81                }
82
83                // `namespace_replacement_patterns` should be ordered by priority.
84                foreach ($namespaceReplacementPatterns as $namespaceReplacementPattern => $replacement) {
85                    $prefixed = preg_replace(
86                        $namespaceReplacementPattern,
87                        $replacement,
88                        $symbol->getOriginalFqdnName()
89                    ) ?? (function () {
90                        throw new \Exception(preg_last_error_msg(), preg_last_error());
91                    })();
92
93                    if ($prefixed !== $symbol->getOriginalFqdnName()) {
94                        $symbol->setLocalReplacement($prefixed);
95                        continue 2;
96                    }
97                }
98                $this->logger->debug("Namespace {$symbol->getOriginalFqdnName()} not changed.");
99            }
100        }
101
102        $classmapPrefix = $this->config->getClassmapPrefix();
103
104        $classesTraitsInterfaces = array_merge(
105            $discoveredSymbols->getDiscoveredTraits()->toArray(),
106            $discoveredSymbols->getDiscoveredInterfaces()->toArray(),
107            $discoveredSymbols->getDiscoveredEnums()->toArray(),
108            $discoveredSymbols->getAllClasses()->toArray()
109        );
110
111        /** @var NamespacedSymbol $symbol */
112        foreach ($classesTraitsInterfaces as $symbol) {
113            // If we're a namespaced class/interface/trait, apply the fqdnchange.
114            if (!$symbol->getNamespace()->isGlobal()) {
115                if ($discoveredNamespaces->has($symbol->getNamespace())) {
116//                if (isset($discoveredNamespaces[ $symbol->getNamespace() ])) {
117                    /** @var NamespaceSymbol $newNamespace */
118                    $newNamespace = $discoveredNamespaces->get($symbol->getNamespace()->getOriginalFqdnName());
119                    $replacement  = $this->determineNamespaceReplacement(
120                        $newNamespace->getOriginalFqdnName(),
121                        $newNamespace->getReplacementFqdnName(),
122                        $symbol->getOriginalFqdnName()
123                    );
124
125                    $symbol->setLocalReplacement($replacement);
126
127                    unset($newNamespace, $replacement);
128                }
129                continue;
130            }
131
132            // If there is no global prefix to set, continue.
133            if (!$classmapPrefix) {
134                continue;
135            }
136
137            // If the classname/interfacename/traitname already appears to begin with the prefix, skip.
138            if (str_starts_with($symbol->getOriginalFqdnName(), $classmapPrefix) && $symbol->getOriginalFqdnName() !== $classmapPrefix) {
139                // Already prefixed / second scan.
140                continue;
141            }
142
143            // Prefix global class.
144            $replacement = $classmapPrefix . $symbol->getOriginalFqdnName();
145            $symbol->setLocalReplacement($replacement);
146        }
147        unset($classmapPrefix, $classesTraitsInterfaces);
148
149        $functionsSymbols = $discoveredSymbols->getDiscoveredFunctions();
150
151        /** @var FunctionSymbol $symbol */
152        foreach ($functionsSymbols as $symbol) {
153            // Don't prefix functions in a namespace – that will be addressed by the namespace prefix.
154            if (!$symbol->getNamespace()->isGlobal()) {
155                continue;
156            }
157            $functionPrefix = $this->config->getFunctionsPrefix();
158            if (empty($functionPrefix) || str_starts_with($symbol->getOriginalFqdnName(), $functionPrefix)) {
159                continue;
160            }
161            $this->globalOrPsr0($symbol, $functionPrefix, $discoveredSymbols);
162
163            unset($symbol);
164        }
165        unset($functionsSymbols);
166
167        if ($this->config->getConstantsPrefix()) {
168            $constantPrefix = $this->config->getConstantsPrefix();
169            foreach ($discoveredSymbols->getConstants() as $constantSymbol) {
170                $constantSymbol->setLocalReplacement($constantPrefix . $constantSymbol->getOriginalLocalName());
171            }
172        }
173    }
174
175    protected function globalOrPsr0(NamespacedSymbol $symbol, string $globalPrefix, DiscoveredSymbols $discoveredSymbols): void
176    {
177        if ($symbol->isPsr0Autoloaded()) {
178            /**
179             * @var Psr0NamespaceSymbol $psr0Namespace
180             *
181             * `::isPsr0Autoloaded()` proves it is not null.
182             * @phpstan-ignore argument.type
183             */
184            $psr0Namespace = $discoveredSymbols->getNamespace($symbol->getPsr0NamespaceString());
185
186            $underscoredOriginalNamespace = str_replace('\\', '_', $psr0Namespace->getOriginalLocalName());
187            $underscoredNewNamespace = str_replace('\\', '_', $psr0Namespace->getReplacementFqdnName());
188
189            $classnameParts = explode('_', $symbol->getOriginalFqdnName());
190            $classname = array_pop($classnameParts);
191            $originalNamespace = implode('_', $classnameParts);
192
193            // Still global
194            if (empty($originalNamespace)) {
195                $replacement = $globalPrefix . $symbol->getOriginalFqdnName();
196                $symbol->setLocalReplacement($replacement);
197            } else {
198                $unnamespacedClass = preg_replace('#^' . $underscoredOriginalNamespace . '#', '', $symbol->getOriginalFqdnName());
199                $replacementPsr0Classname = trim($underscoredNewNamespace . $unnamespacedClass, '_');
200                $symbol->setLocalReplacement($replacementPsr0Classname);
201            }
202        } else {
203            $replacement = $globalPrefix . ltrim($symbol->getOriginalFqdnName(), '\\');
204            $symbol->setLocalReplacement($replacement);
205        }
206    }
207
208    /**
209     *`str_replace` was replacing multiple. This stops after one. Maybe should be tied to start of string.
210     */
211    protected function determineNamespaceReplacement(string $originalNamespace, string $newNamespace, string $fqdnClassname): string
212    {
213        $search = '/' . preg_quote($originalNamespace, '/') . '/';
214
215        return preg_replace($search, $newNamespace, $fqdnClassname, 1)
216                ?? (function () {
217                    throw new \Exception(preg_last_error_msg(), preg_last_error());
218                })();
219    }
220}