Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.93% covered (warning)
65.93%
89 / 135
60.00% covered (warning)
60.00%
21 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiscoveredSymbols
65.93% covered (warning)
65.93%
89 / 135
60.00% covered (warning)
60.00%
21 / 35
232.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 add
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
9.03
 has
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 get
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
4.22
 getSymbols
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getConstants
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespacedSymbols
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getGlobalClassesInterfacesTraits
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getGlobalClassesInterfacesTraitsToRename
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getAllClasses
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiscoveredClasses
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getDiscoveredConstants
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDiscoveredFunctions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDiscoveredTraits
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDiscoveredInterfaces
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getToRename
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getNamespaceSymbolByString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConst
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTrait
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInterface
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEnum
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 originalLocalNames
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 __serialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 offsetGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 offsetSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetUnset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 count
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 notGlobal
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * @see \BrianHenryIE\Strauss\Pipeline\FileSymbolScanner
4 */
5
6declare(strict_types=1);
7
8namespace BrianHenryIE\Strauss\Types;
9
10use ArrayAccess;
11use ArrayIterator;
12use BadMethodCallException;
13use Countable;
14use Exception;
15use InvalidArgumentException;
16use IteratorAggregate;
17use ReturnTypeWillChange;
18use Traversable;
19
20/**
21 * @implements IteratorAggregate<string, DiscoveredSymbol>
22 * @implements ArrayAccess<string, DiscoveredSymbol>
23 */
24class DiscoveredSymbols implements IteratorAggregate, ArrayAccess, Countable
25{
26    private const CLASS_SYMBOL = 'CLASS';
27    private const CONST_SYMBOL = 'CONST';
28    private const NAMESPACE_SYMBOL = 'NAMESPACE';
29    private const FUNCTION_SYMBOL = 'FUNCTION';
30    private const TRAIT_SYMBOL = 'TRAIT';
31    private const INTERFACE_SYMBOL = 'INTERFACE';
32    private const ENUM_SYMBOL = 'ENUM';
33
34    /**
35     * All discovered symbols, grouped by type, indexed by original name.
36     *
37     * @var array{'NAMESPACE':array<string,NamespaceSymbol>, 'CONST':array<string,ConstantSymbol>, 'CLASS':array<string,ClassSymbol>, 'FUNCTION':array<string,FunctionSymbol>, 'TRAIT':array<string,TraitSymbol>, 'INTERFACE':array<string,InterfaceSymbol>, 'ENUM':array<string,NamespacedSymbol>}
38     */
39    protected array $types = [
40        self::CLASS_SYMBOL => [],
41        self::CONST_SYMBOL => [],
42        self::NAMESPACE_SYMBOL => [],
43        self::FUNCTION_SYMBOL => [],
44        self::TRAIT_SYMBOL => [],
45        self::INTERFACE_SYMBOL => [],
46        self::ENUM_SYMBOL => [],
47    ];
48
49    /**
50     * @param DiscoveredSymbol[] $symbols
51     */
52    public function __construct(array $symbols = [])
53    {
54        foreach ($symbols as $symbol) {
55            $this->add($symbol);
56        }
57    }
58
59    /**
60     * TODO: This should merge the symbols instead of overwriting them.
61     *
62     * @param DiscoveredSymbol $symbol
63     */
64    public function add(DiscoveredSymbol $symbol): void
65    {
66        // get_class() is intentional: instanceof would match subclasses (e.g. Psr0NamespaceSymbol instanceof NamespaceSymbol),
67        // routing them to the wrong bucket. Each concrete type must be listed explicitly.
68        switch (get_class($symbol)) {
69            case NamespaceSymbol::class:
70                // Fall-through.
71            case Psr0NamespaceSymbol::class:
72                $this->types[self::NAMESPACE_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
73                return;
74            case ConstantSymbol::class:
75                $this->types[self::CONST_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
76                return;
77            case ClassSymbol::class:
78                $this->types[self::CLASS_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
79                return;
80            case FunctionSymbol::class:
81                $this->types[self::FUNCTION_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
82                return;
83            case InterfaceSymbol::class:
84                $this->types[self::INTERFACE_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
85                return;
86            case TraitSymbol::class:
87                $this->types[self::TRAIT_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
88                return;
89            default:
90                throw new InvalidArgumentException('Unknown symbol type: ' . get_class($symbol));
91        }
92    }
93
94
95    public function has(DiscoveredSymbol $symbol): bool
96    {
97        switch (get_class($symbol)) {
98            case NamespaceSymbol::class:
99                return isset($this->types[self::NAMESPACE_SYMBOL][$symbol->getOriginalFqdnName()]);
100            case ConstantSymbol::class:
101                return isset($this->types[self::CONST_SYMBOL][$symbol->getOriginalFqdnName()]);
102            case ClassSymbol::class:
103                return isset($this->types[self::CLASS_SYMBOL][$symbol->getOriginalFqdnName()]);
104            case FunctionSymbol::class:
105                return isset($this->types[self::FUNCTION_SYMBOL][$symbol->getOriginalFqdnName()]);
106            case InterfaceSymbol::class:
107                return isset($this->types[self::INTERFACE_SYMBOL][$symbol->getOriginalFqdnName()]);
108            case TraitSymbol::class:
109                return isset($this->types[self::TRAIT_SYMBOL][$symbol->getOriginalFqdnName()]);
110            default:
111                throw new InvalidArgumentException('Unknown symbol type: ' . get_class($symbol));
112        }
113    }
114
115    public function get(string $fqdnName): ?DiscoveredSymbol
116    {
117        $found = array_reduce(
118            $this->types,
119            fn (array $carry, array $symbol) => isset($symbol[$fqdnName]) ? array_merge($carry, [$symbol[$fqdnName]]) : $carry,
120            []
121        );
122
123        if (count($found) === 0) {
124            return null;
125        }
126
127        if (count($found) > 1) {
128            $names = array_map(
129                fn (DiscoveredSymbol $symbol):string => $symbol->getOriginalLocalName(),
130                $found
131            );
132            // E.g. an interface and class have the same name.
133            throw new Exception('multiple symbols with the same name: ' . implode(', ', $names));
134        }
135
136        return $this->getClass($fqdnName)
137            ?? $this->getInterface($fqdnName)
138            ?? $this->getTrait($fqdnName)
139            ?? $this->getEnum($fqdnName)
140            ?? $this->getFunction($fqdnName)
141            ?? $this->getConst($fqdnName)
142            ?? $this->getNamespace($fqdnName)
143            ?? $this->getNamespaceSymbolByString($fqdnName);
144    }
145
146    public function getSymbols(): DiscoveredSymbols
147    {
148        return new DiscoveredSymbols(
149            array_merge(
150                array_values($this->getNamespaces()->toArray()),
151                array_values($this->getNamespacedSymbols()->toArray()),
152                array_values($this->getConstants()->toArray()),
153                array_values($this->getDiscoveredFunctions()->toArray()),
154            )
155        );
156    }
157
158    public function getConstants(): DiscoveredSymbols
159    {
160        return new DiscoveredSymbols($this->types[self::CONST_SYMBOL]);
161    }
162
163    public function getNamespaces(): DiscoveredSymbols
164    {
165        return new DiscoveredSymbols($this->types[self::NAMESPACE_SYMBOL]);
166    }
167
168    public function getNamespace(string $namespace): ?NamespaceSymbol
169    {
170        return $this->types[self::NAMESPACE_SYMBOL][trim($namespace, '\\')] ?? null;
171    }
172
173    /**
174     * Get all symbols that may have a namespace (i.e. no classes, interfaces etc.).
175     */
176    public function getNamespacedSymbols(): DiscoveredSymbols
177    {
178        return new DiscoveredSymbols(
179            array_merge(
180                $this->types[self::CLASS_SYMBOL],
181                $this->types[self::TRAIT_SYMBOL],
182                $this->types[self::INTERFACE_SYMBOL],
183                $this->types[self::ENUM_SYMBOL],
184            )
185        );
186    }
187
188    public function getGlobalClassesInterfacesTraits(): DiscoveredSymbols
189    {
190        return new DiscoveredSymbols(
191            array_filter(
192                $this->getNamespacedSymbols()->toArray(),
193                fn($symbol) => ($symbol instanceof NamespacedSymbol) && $symbol->getNamespace()->isGlobal()
194            )
195        );
196    }
197
198    public function getGlobalClassesInterfacesTraitsToRename(): DiscoveredSymbols
199    {
200        return new DiscoveredSymbols(
201            array_filter(
202                $this->getGlobalClassesInterfacesTraits()->toArray(),
203                fn($classSymbol) => $classSymbol->isDoRename()
204            )
205        );
206    }
207
208    public function getAllClasses(): DiscoveredSymbols
209    {
210        return new DiscoveredSymbols($this->types[self::CLASS_SYMBOL]);
211    }
212
213    public function getDiscoveredClasses(?string $classmapPrefix = ''): DiscoveredSymbols
214    {
215        $discoveredClasses = $this->getGlobalClassesInterfacesTraits()->toArray();
216
217        return new DiscoveredSymbols(
218            array_filter(
219                $discoveredClasses,
220                function ($replacement) use ($classmapPrefix) {
221                    return empty($classmapPrefix) || ! str_starts_with($replacement->getLocalReplacement(), $classmapPrefix);
222                }
223            )
224        );
225    }
226
227    /**
228     * @deprecated Use ::getConstants()
229     */
230    public function getDiscoveredConstants(): DiscoveredSymbols
231    {
232        return $this->getConstants();
233    }
234
235    public function getDiscoveredFunctions(): DiscoveredSymbols
236    {
237        return new DiscoveredSymbols($this->types[self::FUNCTION_SYMBOL]);
238    }
239
240    public function getDiscoveredTraits(): DiscoveredSymbols
241    {
242        return new DiscoveredSymbols($this->types[self::TRAIT_SYMBOL]);
243    }
244
245    public function getDiscoveredInterfaces(): DiscoveredSymbols
246    {
247        return new DiscoveredSymbols($this->types[self::INTERFACE_SYMBOL]);
248    }
249
250    public function getToRename(): DiscoveredSymbols
251    {
252        return new DiscoveredSymbols(
253            array_filter(
254                $this->toArray(),
255                fn(DiscoveredSymbol $symbol) => $symbol->isDoRename() && $symbol->getOriginalFqdnName() !== $symbol->getReplacementFqdnName()
256            )
257        );
258    }
259
260    public function getNamespaceSymbolByString(string $namespace): ?NamespaceSymbol
261    {
262        return $this->types[self::NAMESPACE_SYMBOL][$namespace] ?? null;
263    }
264
265    public function getClass(string $fullyQualifiedClassname): ?ClassSymbol
266    {
267        return $this->types[self::CLASS_SYMBOL][$fullyQualifiedClassname] ?? null;
268    }
269    public function getConst(string $const): ?ConstantSymbol
270    {
271        return $this->types[self::CONST_SYMBOL][$const] ?? null;
272    }
273    public function getFunction(string $function): ?FunctionSymbol
274    {
275        return $this->types[self::FUNCTION_SYMBOL][$function] ?? null;
276    }
277    public function getTrait(string $trait): ?TraitSymbol
278    {
279        return $this->types[self::TRAIT_SYMBOL][$trait] ?? null;
280    }
281    public function getInterface(string $interface): ?InterfaceSymbol
282    {
283        return $this->types[self::INTERFACE_SYMBOL][$interface] ?? null;
284    }
285    public function getEnum(string $enumName): ?NamespacedSymbol
286    {
287        return $this->types[self::ENUM_SYMBOL][$enumName] ?? null;
288    }
289
290    /**
291     * @return array<DiscoveredSymbol>
292     */
293    public function toArray(): array
294    {
295        // TODO: Can this lose data with common array keys? Check does the count of the sum of all arrays still equal the count of what is being returned.
296        return array_merge(...array_values($this->types));
297    }
298
299    /**
300     * @return string[]
301     */
302    public function originalLocalNames(): array
303    {
304        return array_map(
305            fn(DiscoveredSymbol $symbol) => $symbol->getOriginalLocalName(),
306            $this->toArray()
307        );
308    }
309
310    /**
311     * @return DiscoveredSymbol[]
312     */
313    public function __serialize(): array
314    {
315        return $this->toArray();
316    }
317
318    /**
319     * @return Traversable<DiscoveredSymbol>
320     */
321    #[ReturnTypeWillChange]
322    public function getIterator()
323    {
324        return new ArrayIterator($this->toArray());
325    }
326
327    /**
328     * @param int|string $offset
329     *
330     * @return bool
331     */
332    #[ReturnTypeWillChange]
333    public function offsetExists($offset)
334    {
335        $found = [];
336        foreach ($this->types as $type => $symbols) {
337            if (isset($symbols[$offset])) {
338                $found[] = $type;
339            }
340        }
341        switch (count($found)) {
342            case 0:
343                return false;
344            case 1:
345                return true;
346            default:
347                throw new Exception("Symbol $offset found as: " . implode(', ', $found) . ", unable to return with confidence.");
348        }
349    }
350
351    /**
352     * @param string $offset
353     *
354     * @return DiscoveredSymbol|null
355     */
356    #[ReturnTypeWillChange]
357    public function offsetGet($offset)
358    {
359        // This will throw an exception if there are duplicates.
360        $this->offsetExists($offset);
361        return $this->toArray()[$offset] ?? null;
362    }
363
364    /**
365     * @param string $offset
366     * @param DiscoveredSymbol$value
367     *
368     * @return void
369     */
370    #[ReturnTypeWillChange]
371    public function offsetSet($offset, $value)
372    {
373        throw new BadMethodCallException();
374    }
375
376    /**
377     * @param string $offset
378     *
379     * @return void
380     */
381    #[ReturnTypeWillChange]
382    public function offsetUnset($offset)
383    {
384        throw new BadMethodCallException();
385    }
386
387    /**
388     * So `count( $discoveredSymbols )` will work.
389     *
390     * @return int
391     */
392    #[ReturnTypeWillChange]
393    public function count()
394    {
395        return array_reduce(
396            $this->types,
397            fn(int $carry, array $item) => $carry + count($item),
398            0
399        );
400    }
401
402    public function notGlobal(): self
403    {
404        /** @var DiscoveredSymbol[] $all */
405        $all = [];
406        /**
407         * @var array<string, DiscoveredSymbol> $types
408         */
409        foreach ($this->types as $types) {
410            /**
411             * @var DiscoveredSymbol $symbol
412             */
413            foreach ($types as $symbol) {
414                if (!$symbol->isGlobal()) {
415                    $all[] = $symbol;
416                }
417            }
418        }
419        return new self($all);
420    }
421}