Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
Extractor
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
5 / 5
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extract
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 merge
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 conflict
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 fold
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Verba\Tool;
6
7/**
8 * Runs a domain's scanners, keeps the messages that belong to it, and merges
9 * duplicates by context and id (unioning locations, preferring a plural form
10 * when any call site supplied one).
11 *
12 * @api
13 */
14final class Extractor
15{
16    public function __construct(
17        private readonly Domain $domain,
18    ) {}
19
20    /**
21     * @return array{
22     *     messages: array<string, Message>,
23     *     contexts: array<string, array<string, Message>>,
24     *     warnings: list<string>,
25     * }
26     */
27    public function extract(): array
28    {
29        $merged = [];
30        $contexts = [];
31        $warnings = [];
32
33        foreach ($this->domain->scanners as $scanner) {
34            foreach ($scanner->scan() as $message) {
35                if (!$this->domain->owns($message->domain)) {
36                    continue;
37                }
38
39                if ($message->context === null) {
40                    $existing = $merged[$message->id] ?? null;
41                    $merged[$message->id] = $this->merge($existing, $message, $warnings);
42                } else {
43                    $existing = $contexts[$message->context][$message->id] ?? null;
44                    $contexts[$message->context][$message->id] = $this->merge(
45                        $existing,
46                        $message,
47                        $warnings,
48                    );
49                }
50            }
51
52            foreach ($scanner->warnings() as $warning) {
53                $warnings[] = $warning;
54            }
55        }
56
57        ksort($merged, SORT_STRING);
58        ksort($contexts, SORT_STRING);
59
60        foreach ($contexts as &$messages) {
61            ksort($messages, SORT_STRING);
62        }
63
64        unset($messages);
65
66        return ['messages' => $merged, 'contexts' => $contexts, 'warnings' => $warnings];
67    }
68
69    /**
70     * @param list<string> $warnings
71     */
72    private function merge(?Message $existing, Message $next, array &$warnings): Message
73    {
74        $warning = $this->conflict($existing, $next);
75
76        if ($warning !== null) {
77            $warnings[] = $warning;
78        }
79
80        return $this->fold($existing, $next);
81    }
82
83    private function conflict(?Message $existing, Message $next): ?string
84    {
85        if ($existing === null || $existing->plural === $next->plural) {
86            return null;
87        }
88
89        $location = $next->locations[0] ?? 'unknown location';
90        $id = "message id '{$next->id}'";
91
92        if ($next->context !== null) {
93            $id .= " in context '{$next->context}'";
94        }
95
96        if ($existing->plural === null || $next->plural === null) {
97            return "Mixed singular and plural calls for {$id} at {$location}";
98        }
99
100        return "Conflicting plural forms for {$id} at {$location}";
101    }
102
103    private function fold(?Message $existing, Message $next): Message
104    {
105        if ($existing === null) {
106            return new Message(null, $next->id, $next->plural, $next->locations, $next->context);
107        }
108
109        return new Message(
110            null,
111            $next->id,
112            $existing->plural ?? $next->plural,
113            [...$existing->locations, ...$next->locations],
114            $next->context,
115        );
116    }
117}