• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

move-elevator / composer-translation-validator / 16137064113

08 Jul 2025 07:41AM UTC coverage: 78.302% (-16.2%) from 94.526%
16137064113

Pull #19

github

web-flow
Merge 5f8b3255c into 3e904fb25
Pull Request #19: refactor: improve output style

93 of 278 new or added lines in 7 files covered. (33.45%)

103 existing lines in 7 files now uncovered.

830 of 1060 relevant lines covered (78.3%)

4.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.13
/src/Validator/MismatchValidator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace MoveElevator\ComposerTranslationValidator\Validator;
6

7
use MoveElevator\ComposerTranslationValidator\FileDetector\FileSet;
8
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
9
use MoveElevator\ComposerTranslationValidator\Parser\XliffParser;
10
use MoveElevator\ComposerTranslationValidator\Parser\YamlParser;
11
use MoveElevator\ComposerTranslationValidator\Result\Issue;
12
use Symfony\Component\Console\Helper\Table;
13
use Symfony\Component\Console\Helper\TableStyle;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Output\OutputInterface;
16

17
class MismatchValidator extends AbstractValidator implements ValidatorInterface
18
{
19
    /** @var array<string, array<string>> */
20
    protected array $keyArray = [];
21

22
    public function processFile(ParserInterface $file): array
4✔
23
    {
24
        $keys = $file->extractKeys();
4✔
25

26
        if (!$keys) {
4✔
27
            $this->logger?->error('The source file '.$file->getFileName().' is not valid.');
1✔
28

29
            return [];
1✔
30
        }
31
        foreach ($keys as $key) {
3✔
32
            $value = $file->getContentByKey($key);
3✔
33
            $this->keyArray[$file->getFileName()][$key] = $value ?? null;
3✔
34
        }
35

36
        return [];
3✔
37
    }
38

39
    public function postProcess(): void
2✔
40
    {
41
        $allKeys = [];
2✔
42
        foreach ($this->keyArray as $values) {
2✔
43
            $allKeys[] = array_keys($values);
2✔
44
        }
45
        $allKeys = array_unique(array_merge(...$allKeys));
2✔
46

47
        foreach ($allKeys as $key) {
2✔
48
            $missingInSome = false;
2✔
49
            foreach ($this->keyArray as $keys) {
2✔
50
                if (!array_key_exists($key, $keys)) {
2✔
51
                    $missingInSome = true;
1✔
52
                    break;
1✔
53
                }
54
            }
55
            if ($missingInSome) {
2✔
56
                $result = [
1✔
57
                    'key' => $key,
1✔
58
                    'files' => [],
1✔
59
                ];
1✔
60
                foreach ($this->keyArray as $file => $keys) {
1✔
61
                    $result['files'][] = [
1✔
62
                        'file' => $file,
1✔
63
                        'value' => $keys[$key] ?? null,
1✔
64
                    ];
1✔
65
                }
66
                $this->addIssue(new Issue(
1✔
67
                    '',
1✔
68
                    $result,
1✔
69
                    '',
1✔
70
                    'MismatchValidator'
1✔
71
                ));
1✔
72
            }
73
        }
74
    }
75

76
    /**
77
     * @param array<string, array<int, array<mixed>>> $issueSets
78
     */
79
    public function renderIssueSets(InputInterface $input, OutputInterface $output, array $issueSets): void
1✔
80
    {
81
        $rows = [];
1✔
82
        $header = ['Key'];
1✔
83
        $allFiles = [];
1✔
84

85
        foreach ($issueSets as $issuesPerFile) {
1✔
86
            foreach ($issuesPerFile as $issues) {
1✔
87
                // Handle both new format (with 'issues' key) and old format (direct data)
88
                if (isset($issues['issues']) && is_array($issues['issues'])) {
1✔
89
                    $issueData = $issues['issues'];
×
90
                } else {
91
                    $issueData = $issues;
1✔
92
                }
93
                $key = $issueData['key'];
1✔
94
                $files = $issueData['files'];
1✔
95
                if (empty($allFiles)) {
1✔
96
                    $allFiles = array_column($files, 'file');
1✔
97
                    $header = array_merge(['Key'], array_map(static fn ($f) => "<fg=red>$f</>", $allFiles));
1✔
98
                }
99
                $row = [$key];
1✔
100
                foreach ($files as $fileInfo) {
1✔
101
                    $row[] = $fileInfo['value'] ?? '<fg=yellow><missing></>';
1✔
102
                }
103
                $rows[] = $row;
1✔
104
            }
105
        }
106

107
        (new Table($output))
1✔
108
            ->setHeaders($header)
1✔
109
            ->setRows($rows)
1✔
110
            ->setStyle(
1✔
111
                (new TableStyle())
1✔
112
                    ->setCellHeaderFormat('%s')
1✔
113
            )
1✔
114
            ->render();
1✔
115
    }
116

117
    public function explain(): string
1✔
118
    {
119
        return 'This validator checks for keys that are present in some files but not in others. '
1✔
120
            .'It helps to identify mismatches in translation keys across different translation files.';
1✔
121
    }
122

123
    /**
124
     * @return class-string<ParserInterface>[]
125
     */
126
    public function supportsParser(): array
1✔
127
    {
128
        return [XliffParser::class, YamlParser::class];
1✔
129
    }
130

131
    protected function resetState(): void
1✔
132
    {
133
        parent::resetState();
1✔
134
        $this->keyArray = [];
1✔
135
    }
136

NEW
137
    public function resultTypeOnValidationFailure(): ResultType
×
138
    {
NEW
139
        return ResultType::WARNING;
×
140
    }
141

NEW
142
    public function formatIssueMessage(Issue $issue, string $prefix = '', bool $isVerbose = false): string
×
143
    {
NEW
144
        $details = $issue->getDetails();
×
NEW
145
        $resultType = $this->resultTypeOnValidationFailure();
×
146

NEW
147
        $level = $resultType->toString();
×
NEW
148
        $color = $resultType->toColorString();
×
149

150
        // Details contains key mismatch information
NEW
151
        $key = $details['key'] ?? 'unknown';
×
NEW
152
        $files = $details['files'] ?? [];
×
NEW
153
        $currentFile = basename($issue->getFile());
×
NEW
154
        $otherFiles = [];
×
NEW
155
        $currentFileHasValue = false;
×
156

NEW
157
        foreach ($files as $fileInfo) {
×
NEW
158
            $fileName = $fileInfo['file'] ?? 'unknown';
×
NEW
159
            if ($fileName === $currentFile) {
×
NEW
160
                $currentFileHasValue = null !== $fileInfo['value'];
×
161
            } else {
NEW
162
                $otherFiles[] = $fileName;
×
163
            }
164
        }
165

NEW
166
        if ($currentFileHasValue) {
×
NEW
167
            $action = 'missing from';
×
168
        } else {
NEW
169
            $action = 'present in';
×
170
        }
171

NEW
172
        $otherFilesList = !empty($otherFiles) ? implode('`, `', $otherFiles) : 'other files';
×
173

NEW
174
        return "- <fg=$color>$level</> {$prefix}translation key `$key` is $action other translation files (`$otherFilesList`)";
×
175
    }
176

NEW
177
    public function distributeIssuesForDisplay(FileSet $fileSet): array
×
178
    {
NEW
179
        $distribution = [];
×
180

NEW
181
        foreach ($this->issues as $issue) {
×
NEW
182
            $details = $issue->getDetails();
×
NEW
183
            $files = $details['files'] ?? [];
×
184

185
            // Add the issue to each affected file
NEW
186
            foreach ($files as $fileInfo) {
×
NEW
187
                $fileName = $fileInfo['file'] ?? '';
×
NEW
188
                if (!empty($fileName)) {
×
189
                    // Construct full file path from fileSet
NEW
190
                    $basePath = rtrim($fileSet->getPath(), '/');
×
NEW
191
                    $filePath = $basePath.'/'.$fileName;
×
192

193
                    // Create a new issue specific to this file
NEW
194
                    $fileSpecificIssue = new Issue(
×
NEW
195
                        $filePath,
×
NEW
196
                        $details,
×
NEW
197
                        $issue->getParser(),
×
NEW
UNCOV
198
                        $issue->getValidatorType()
×
NEW
199
                    );
×
200

NEW
UNCOV
201
                    if (!isset($distribution[$filePath])) {
×
NEW
202
                        $distribution[$filePath] = [];
×
203
                    }
204

NEW
UNCOV
205
                    $distribution[$filePath][] = $fileSpecificIssue;
×
206
                }
207
            }
208
        }
209

NEW
UNCOV
210
        return $distribution;
×
211
    }
212

NEW
UNCOV
213
    public function shouldShowDetailedOutput(): bool
×
214
    {
NEW
UNCOV
215
        return true;
×
216
    }
217

NEW
UNCOV
218
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
×
219
    {
NEW
UNCOV
220
        if (empty($issues)) {
×
NEW
UNCOV
221
            return;
×
222
        }
223

NEW
UNCOV
224
        $rows = [];
×
NEW
225
        $allKeys = [];
×
NEW
226
        $allFilesData = [];
×
227

228
        // Collect all data
NEW
229
        foreach ($issues as $issue) {
×
NEW
230
            $details = $issue->getDetails();
×
NEW
231
            $key = $details['key'] ?? 'unknown';
×
NEW
232
            $files = $details['files'] ?? [];
×
NEW
233
            $currentFile = basename($issue->getFile());
×
234

NEW
UNCOV
235
            if (!in_array($key, $allKeys)) {
×
NEW
UNCOV
236
                $allKeys[] = $key;
×
237
            }
238

NEW
UNCOV
239
            foreach ($files as $fileInfo) {
×
NEW
UNCOV
240
                $fileName = $fileInfo['file'] ?? '';
×
NEW
UNCOV
241
                $value = $fileInfo['value'];
×
242

NEW
UNCOV
243
                if (!isset($allFilesData[$key])) {
×
NEW
UNCOV
244
                    $allFilesData[$key] = [];
×
245
                }
NEW
UNCOV
246
                $allFilesData[$key][$fileName] = $value;
×
247
            }
248
        }
249

250
        // Get first issue to determine current file and file order
NEW
UNCOV
251
        $firstIssue = $issues[0];
×
NEW
UNCOV
252
        $currentFile = basename($firstIssue->getFile());
×
NEW
UNCOV
253
        $firstDetails = $firstIssue->getDetails();
×
NEW
UNCOV
254
        $firstFiles = $firstDetails['files'] ?? [];
×
255

256
        // Order files: current file first, then others
NEW
UNCOV
257
        $fileOrder = [$currentFile];
×
NEW
UNCOV
258
        foreach ($firstFiles as $fileInfo) {
×
NEW
UNCOV
259
            $fileName = $fileInfo['file'] ?? '';
×
NEW
UNCOV
260
            if ($fileName !== $currentFile && !in_array($fileName, $fileOrder)) {
×
NEW
UNCOV
261
                $fileOrder[] = $fileName;
×
262
            }
263
        }
264

NEW
UNCOV
265
        $header = ['Translation Key', $currentFile];
×
NEW
UNCOV
266
        foreach ($fileOrder as $fileName) {
×
NEW
UNCOV
267
            if ($fileName !== $currentFile) {
×
NEW
UNCOV
268
                $header[] = $fileName;
×
269
            }
270
        }
271

272
        // Build rows
NEW
UNCOV
273
        foreach ($allKeys as $key) {
×
NEW
UNCOV
274
            $row = [$key];
×
NEW
UNCOV
275
            foreach ($fileOrder as $fileName) {
×
NEW
UNCOV
276
                $value = $allFilesData[$key][$fileName] ?? null;
×
NEW
UNCOV
277
                $row[] = $value ?? '';  // Empty string instead of <missing>
×
278
            }
NEW
UNCOV
279
            $rows[] = $row;
×
280
        }
281

NEW
UNCOV
282
        $table = new Table($output);
×
NEW
UNCOV
283
        $table->setHeaders($header)
×
NEW
UNCOV
284
              ->setRows($rows)
×
NEW
UNCOV
285
              ->setStyle(
×
NEW
UNCOV
286
                  (new TableStyle())
×
NEW
UNCOV
287
                      ->setCellHeaderFormat('%s')
×
NEW
UNCOV
288
              )
×
NEW
UNCOV
289
              ->render();
×
290
    }
291
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc