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