Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.91% covered (warning)
66.91%
93 / 139
58.33% covered (warning)
58.33%
21 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiscoveredSymbols
66.91% covered (warning)
66.91%
93 / 139
58.33% covered (warning)
58.33%
21 / 36
241.55
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
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
10.02
 has
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
57.05
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
 getDiscoveredEnums
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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,EnumSymbol>}
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            case EnumSymbol::class:
90                $this->types[self::ENUM_SYMBOL][$symbol->getOriginalFqdnName()] = $symbol;
91                return;
92            default:
93                throw new InvalidArgumentException('Unknown symbol type: ' . get_class($symbol));
94        }
95    }
96
97
98    public function has(DiscoveredSymbol $symbol): bool
99    {
100        switch (get_class($symbol)) {
101            case NamespaceSymbol::class:
102            case Psr0NamespaceSymbol::class:
103                return isset($this->types[self::NAMESPACE_SYMBOL][$symbol->getOriginalFqdnName()]);
104            case ConstantSymbol::class:
105                return isset($this->types[self::CONST_SYMBOL][$symbol->getOriginalFqdnName()]);
106            case ClassSymbol::class:
107                return isset($this->types[self::CLASS_SYMBOL][$symbol->getOriginalFqdnName()]);
108            case FunctionSymbol::class:
109                return isset($this->types[self::FUNCTION_SYMBOL][$symbol->getOriginalFqdnName()]);
110            case InterfaceSymbol::class:
111                return isset($this->types[self::INTERFACE_SYMBOL][$symbol->getOriginalFqdnName()]);
112            case TraitSymbol::class:
113                return isset($this->types[self::TRAIT_SYMBOL][$symbol->getOriginalFqdnName()]);
114            case EnumSymbol::class:
115                return isset($this->types[self::ENUM_SYMBOL][$symbol->getOriginalFqdnName()]);
116            default:
117                throw new InvalidArgumentException('Unknown symbol type: ' . get_class($symbol));
118        }
119    }
120
121    public function get(string $fqdnName): ?DiscoveredSymbol
122    {
123        $found = array_reduce(
124            $this->types,
125            fn (array $carry, array $symbol) => isset($symbol[$fqdnName]) ? array_merge($carry, [$symbol[$fqdnName]]) : $carry,
126            []
127        );
128
129        if (count($found) === 0) {
130            return null;
131        }
132
133        if (count($found) > 1) {
134            $names = array_map(
135                fn (DiscoveredSymbol $symbol):string => $symbol->getOriginalLocalName(),
136                $found
137            );
138            // E.g. an interface and class have the same name.
139            throw new Exception('multiple symbols with the same name: ' . implode(', ', $names));
140        }
141
142        return $this->getClass($fqdnName)
143            ?? $this->getInterface($fqdnName)
144            ?? $this->getTrait($fqdnName)
145            ?? $this->getEnum($fqdnName)
146            ?? $this->getFunction($fqdnName)
147            ?? $this->getConst($fqdnName)
148            ?? $this->getNamespace($fqdnName)
149            ?? $this->getNamespaceSymbolByString($fqdnName);
150    }
151
152    public function getSymbols(): DiscoveredSymbols
153    {
154        return new DiscoveredSymbols(
155            array_merge(
156                array_values($this->getNamespaces()->toArray()),
157                array_values($this->getNamespacedSymbols()->toArray()),
158                array_values($this->getConstants()->toArray()),
159                array_values($this->getDiscoveredFunctions()->toArray()),
160            )
161        );
162    }
163
164    public function getConstants(): DiscoveredSymbols
165    {
166        return new DiscoveredSymbols($this->types[self::CONST_SYMBOL]);
167    }
168
169    public function getNamespaces(): DiscoveredSymbols
170    {
171        return new DiscoveredSymbols($this->types[self::NAMESPACE_SYMBOL]);
172    }
173
174    public function getNamespace(string $namespace): ?NamespaceSymbol
175    {
176        return $this->types[self::NAMESPACE_SYMBOL][trim($namespace, '\\')] ?? null;
177    }
178
179    /**
180     * Get all symbols that may have a namespace (i.e. no classes, interfaces etc.).
181     */
182    public function getNamespacedSymbols(): DiscoveredSymbols
183    {
184        return new DiscoveredSymbols(
185            array_merge(
186                $this->types[self::CLASS_SYMBOL],
187                $this->types[self::TRAIT_SYMBOL],
188                $this->types[self::INTERFACE_SYMBOL],
189                $this->types[self::ENUM_SYMBOL],
190            )
191        );
192    }
193
194    public function getGlobalClassesInterfacesTraits(): DiscoveredSymbols
195    {
196        return new DiscoveredSymbols(
197            array_filter(
198                $this->getNamespacedSymbols()->toArray(),
199                fn($symbol) => ($symbol instanceof NamespacedSymbol) && $symbol->getNamespace()->isGlobal()
200            )
201        );
202    }
203
204    public function getGlobalClassesInterfacesTraitsToRename(): DiscoveredSymbols
205    {
206        return new DiscoveredSymbols(
207            array_filter(
208                $this->getGlobalClassesInterfacesTraits()->toArray(),
209                fn($classSymbol) => $classSymbol->isDoRename()
210            )
211        );
212    }
213
214    public function getAllClasses(): DiscoveredSymbols
215    {
216        return new DiscoveredSymbols($this->types[self::CLASS_SYMBOL]);
217    }
218
219    public function getDiscoveredClasses(?string $classmapPrefix = ''): DiscoveredSymbols
220    {
221        $discoveredClasses = $this->getGlobalClassesInterfacesTraits()->toArray();
222
223        return new DiscoveredSymbols(
224            array_filter(
225                $discoveredClasses,
226                function ($replacement) use ($classmapPrefix) {
227                    return empty($classmapPrefix) || ! str_starts_with($replacement->getLocalReplacement(), $classmapPrefix);
228                }
229            )
230        );
231    }
232
233    /**
234     * @deprecated Use ::getConstants()
235     */
236    public function getDiscoveredConstants(): DiscoveredSymbols
237    {
238        return $this->getConstants();
239    }
240
241    public function getDiscoveredFunctions(): DiscoveredSymbols
242    {
243        return new DiscoveredSymbols($this->types[self::FUNCTION_SYMBOL]);
244    }
245
246    public function getDiscoveredTraits(): DiscoveredSymbols
247    {
248        return new DiscoveredSymbols($this->types[self::TRAIT_SYMBOL]);
249    }
250
251    public function getDiscoveredInterfaces(): DiscoveredSymbols
252    {
253        return new DiscoveredSymbols($this->types[self::INTERFACE_SYMBOL]);
254    }
255
256    public function getDiscoveredEnums(): DiscoveredSymbols
257    {
258        return new DiscoveredSymbols($this->types[self::ENUM_SYMBOL]);
259    }
260
261    public function getToRename(): DiscoveredSymbols
262    {
263        return new DiscoveredSymbols(
264            array_filter(
265                $this->toArray(),
266                fn(DiscoveredSymbol $symbol) => $symbol->isDoRename() && $symbol->getOriginalFqdnName() !== $symbol->getReplacementFqdnName()
267            )
268        );
269    }
270
271    public function getNamespaceSymbolByString(string $namespace): ?NamespaceSymbol
272    {
273        return $this->types[self::NAMESPACE_SYMBOL][$namespace] ?? null;
274    }
275
276    public function getClass(string $fullyQualifiedClassname): ?ClassSymbol
277    {
278        return $this->types[self::CLASS_SYMBOL][$fullyQualifiedClassname] ?? null;
279    }
280    public function getConst(string $const): ?ConstantSymbol
281    {
282        return $this->types[self::CONST_SYMBOL][$const] ?? null;
283    }
284    public function getFunction(string $function): ?FunctionSymbol
285    {
286        return $this->types[self::FUNCTION_SYMBOL][$function] ?? null;
287    }
288    public function getTrait(string $trait): ?TraitSymbol
289    {
290        return $this->types[self::TRAIT_SYMBOL][$trait] ?? null;
291    }
292    public function getInterface(string $interface): ?InterfaceSymbol
293    {
294        return $this->types[self::INTERFACE_SYMBOL][$interface] ?? null;
295    }
296    public function getEnum(string $enumName): ?EnumSymbol
297    {
298        return $this->types[self::ENUM_SYMBOL][$enumName] ?? null;
299    }
300
301    /**
302     * @return array<DiscoveredSymbol>
303     */
304    public function toArray(): array
305    {
306        // 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.
307        return array_merge(...array_values($this->types));
308    }
309
310    /**
311     * @return string[]
312     */
313    public function originalLocalNames(): array
314    {
315        return array_map(
316            fn(DiscoveredSymbol $symbol) => $symbol->getOriginalLocalName(),
317            $this->toArray()
318        );
319    }
320
321    /**
322     * @return DiscoveredSymbol[]
323     */
324    public function __serialize(): array
325    {
326        return $this->toArray();
327    }
328
329    /**
330     * @return Traversable<DiscoveredSymbol>
331     */
332    #[ReturnTypeWillChange]
333    public function getIterator()
334    {
335        return new ArrayIterator($this->toArray());
336    }
337
338    /**
339     * @param int|string $offset
340     *
341     * @return bool
342     */
343    #[ReturnTypeWillChange]
344    public function offsetExists($offset)
345    {
346        $found = [];
347        foreach ($this->types as $type => $symbols) {
348            if (isset($symbols[$offset])) {
349                $found[] = $type;
350            }
351        }
352        switch (count($found)) {
353            case 0:
354                return false;
355            case 1:
356                return true;
357            default:
358                throw new Exception("Symbol $offset found as: " . implode(', ', $found) . ", unable to return with confidence.");
359        }
360    }
361
362    /**
363     * @param string $offset
364     *
365     * @return DiscoveredSymbol|null
366     */
367    #[ReturnTypeWillChange]
368    public function offsetGet($offset)
369    {
370        // This will throw an exception if there are duplicates.
371        $this->offsetExists($offset);
372        return $this->toArray()[$offset] ?? null;
373    }
374
375    /**
376     * @param string $offset
377     * @param DiscoveredSymbol$value
378     *
379     * @return void
380     */
381    #[ReturnTypeWillChange]
382    public function offsetSet($offset, $value)
383    {
384        throw new BadMethodCallException();
385    }
386
387    /**
388     * @param string $offset
389     *
390     * @return void
391     */
392    #[ReturnTypeWillChange]
393    public function offsetUnset($offset)
394    {
395        throw new BadMethodCallException();
396    }
397
398    /**
399     * So `count( $discoveredSymbols )` will work.
400     *
401     * @return int
402     */
403    #[ReturnTypeWillChange]
404    public function count()
405    {
406        return array_reduce(
407            $this->types,
408            fn(int $carry, array $item) => $carry + count($item),
409            0
410        );
411    }
412
413    public function notGlobal(): self
414    {
415        /** @var DiscoveredSymbol[] $all */
416        $all = [];
417        /**
418         * @var array<string, DiscoveredSymbol> $types
419         */
420        foreach ($this->types as $types) {
421            /**
422             * @var DiscoveredSymbol $symbol
423             */
424            foreach ($types as $symbol) {
425                if (!$symbol->isGlobal()) {
426                    $all[] = $symbol;
427                }
428            }
429        }
430        return new self($all);
431    }
432}