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

move-elevator / composer-translation-validator / 18559956393

16 Oct 2025 11:36AM UTC coverage: 95.519%. Remained the same
18559956393

Pull #73

github

jackd248
Merge remote-tracking branch 'origin/main' into php-cs-fixer-preset
Pull Request #73: build: add php-cs-fixer-preset

206 of 210 new or added lines in 16 files covered. (98.1%)

91 existing lines in 20 files now uncovered.

2345 of 2455 relevant lines covered (95.52%)

7.73 hits per line

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

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

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the "composer-translation-validator" Composer plugin.
7
 *
8
 * (c) 2025 Konrad Michalik <km@move-elevator.de>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13

14
namespace MoveElevator\ComposerTranslationValidator\Validator;
15

16
use MoveElevator\ComposerTranslationValidator\FileDetector\FileSet;
17
use MoveElevator\ComposerTranslationValidator\Parser\{JsonParser, ParserInterface, PhpParser, XliffParser, YamlParser};
18
use MoveElevator\ComposerTranslationValidator\Result\Issue;
19
use Symfony\Component\Console\Helper\{Table, TableStyle};
20
use Symfony\Component\Console\Output\OutputInterface;
21

22
use function array_key_exists;
23
use function in_array;
24

25
/**
26
 * MismatchValidator.
27
 *
28
 * @author Konrad Michalik <km@move-elevator.de>
29
 * @license GPL-3.0-or-later
30
 */
31
class MismatchValidator extends AbstractValidator implements ValidatorInterface
32
{
33
    /** @var array<string, array<string>> */
34
    /**
35
     * @var array<string, array<string, string|null>>
36
     */
37
    protected array $keyArray = [];
38

39
    public function processFile(ParserInterface $file): array
4✔
40
    {
41
        $keys = $file->extractKeys();
4✔
42

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

46
            return [];
1✔
47
        }
48
        foreach ($keys as $key) {
3✔
49
            $value = $file->getContentByKey($key);
3✔
50
            $fileKey = !empty($this->currentFilePath) ? $this->currentFilePath : $file->getFileName();
3✔
51
            $this->keyArray[$fileKey][$key] = $value ?? null;
3✔
52
        }
53

54
        return [];
3✔
55
    }
56

57
    public function postProcess(): void
2✔
58
    {
59
        $allKeys = [];
2✔
60
        foreach ($this->keyArray as $values) {
2✔
61
            $allKeys[] = array_keys($values);
2✔
62
        }
63
        $allKeys = array_unique(array_merge(...$allKeys));
2✔
64

65
        foreach ($allKeys as $key) {
2✔
66
            $missingInSome = false;
2✔
67
            foreach ($this->keyArray as $keys) {
2✔
68
                if (!array_key_exists($key, $keys)) {
2✔
69
                    $missingInSome = true;
1✔
70
                    break;
1✔
71
                }
72
            }
73
            if ($missingInSome) {
2✔
74
                $result = [
1✔
75
                    'key' => $key,
1✔
76
                    'files' => [],
1✔
77
                ];
1✔
78
                foreach ($this->keyArray as $file => $keys) {
1✔
79
                    $result['files'][] = [
1✔
80
                        'file' => $file,
1✔
81
                        'value' => $keys[$key] ?? null,
1✔
82
                    ];
1✔
83
                }
84
                $this->addIssue(new Issue(
1✔
85
                    '',
1✔
86
                    $result,
1✔
87
                    '',
1✔
88
                    $this->getShortName(),
1✔
89
                ));
1✔
90
            }
91
        }
92
    }
93

94
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
1✔
95
    {
96
        $details = $issue->getDetails();
1✔
97
        $resultType = $this->resultTypeOnValidationFailure();
1✔
98

99
        $level = $resultType->toString();
1✔
100
        $color = $resultType->toColorString();
1✔
101

102
        $key = $details['key'] ?? 'unknown';
1✔
103
        $files = $details['files'] ?? [];
1✔
104
        $currentFile = basename($issue->getFile());
1✔
105
        $otherFiles = [];
1✔
106
        $currentFileHasValue = false;
1✔
107

108
        foreach ($files as $fileInfo) {
1✔
109
            $fileName = basename($fileInfo['file'] ?? 'unknown');
1✔
110
            if ($fileName === $currentFile) {
1✔
UNCOV
111
                $currentFileHasValue = null !== $fileInfo['value'];
×
112
            } else {
113
                $otherFiles[] = $fileName;
1✔
114
            }
115
        }
116

117
        if ($currentFileHasValue) {
1✔
UNCOV
118
            $action = 'missing from';
×
119
        } else {
120
            $action = 'missing but present in';
1✔
121
        }
122

123
        $otherFilesList = !empty($otherFiles) ? implode('`, `', $otherFiles) : 'other files';
1✔
124

125
        return "- <fg=$color>$level</> {$prefix} the translation key `$key` is $action other translation files (`$otherFilesList`)";
1✔
126
    }
127

128
    public function distributeIssuesForDisplay(FileSet $fileSet): array
1✔
129
    {
130
        $distribution = [];
1✔
131

132
        foreach ($this->issues as $issue) {
1✔
133
            $details = $issue->getDetails();
1✔
134
            $files = $details['files'] ?? [];
1✔
135

136
            foreach ($files as $fileInfo) {
1✔
137
                $filePath = $fileInfo['file'] ?? '';
1✔
138
                if (!empty($filePath)) {
1✔
139
                    $fileSpecificIssue = new Issue(
1✔
140
                        $filePath,
1✔
141
                        $details,
1✔
142
                        $issue->getParser(),
1✔
143
                        $issue->getValidatorType(),
1✔
144
                    );
1✔
145

146
                    if (!isset($distribution[$filePath])) {
1✔
147
                        $distribution[$filePath] = [];
1✔
148
                    }
149

150
                    $distribution[$filePath][] = $fileSpecificIssue;
1✔
151
                }
152
            }
153
        }
154

155
        return $distribution;
1✔
156
    }
157

158
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
1✔
159
    {
160
        if (empty($issues)) {
1✔
UNCOV
161
            return;
×
162
        }
163

164
        $rows = [];
1✔
165
        $allKeys = [];
1✔
166
        $allFilesData = [];
1✔
167

168
        foreach ($issues as $issue) {
1✔
169
            $details = $issue->getDetails();
1✔
170
            $key = $details['key'] ?? 'unknown';
1✔
171
            $files = $details['files'] ?? [];
1✔
172
            $currentFile = basename($issue->getFile());
1✔
173

174
            if (!in_array($key, $allKeys)) {
1✔
175
                $allKeys[] = $key;
1✔
176
            }
177

178
            foreach ($files as $fileInfo) {
1✔
179
                $fileName = basename($fileInfo['file'] ?? '');
1✔
180
                $value = $fileInfo['value'];
1✔
181

182
                if (!isset($allFilesData[$key])) {
1✔
183
                    $allFilesData[$key] = [];
1✔
184
                }
185
                $allFilesData[$key][$fileName] = $value;
1✔
186
            }
187
        }
188

189
        $firstIssue = $issues[0];
1✔
190
        $currentFile = basename($firstIssue->getFile());
1✔
191
        $firstDetails = $firstIssue->getDetails();
1✔
192
        $firstFiles = $firstDetails['files'] ?? [];
1✔
193

194
        $fileOrder = [$currentFile];
1✔
195
        foreach ($firstFiles as $fileInfo) {
1✔
196
            $fileName = basename($fileInfo['file'] ?? '');
1✔
197
            if ($fileName !== $currentFile && !in_array($fileName, $fileOrder, true)) {
1✔
198
                $fileOrder[] = $fileName;
1✔
199
            }
200
        }
201

202
        $header = ['Translation Key', $currentFile];
1✔
203
        foreach ($fileOrder as $fileName) {
1✔
204
            if ($fileName !== $currentFile) {
1✔
205
                $header[] = $fileName;
1✔
206
            }
207
        }
208

209
        foreach ($allKeys as $key) {
1✔
210
            $row = [$key];
1✔
211
            foreach ($fileOrder as $fileName) {
1✔
212
                $value = $allFilesData[$key][$fileName] ?? null;
1✔
213
                $row[] = $value ?? '';
1✔
214
            }
215
            $rows[] = $row;
1✔
216
        }
217

218
        $table = new Table($output);
1✔
219
        $table->setHeaders($header)
1✔
220
            ->setRows($rows)
1✔
221
            ->setStyle(
1✔
222
                (new TableStyle())
1✔
223
                    ->setCellHeaderFormat('%s'),
1✔
224
            )
1✔
225
            ->render();
1✔
226
    }
227

228
    /**
229
     * @return class-string<ParserInterface>[]
230
     */
231
    public function supportsParser(): array
1✔
232
    {
233
        return [XliffParser::class, YamlParser::class, JsonParser::class, PhpParser::class];
1✔
234
    }
235

236
    public function shouldShowDetailedOutput(): bool
1✔
237
    {
238
        return true;
1✔
239
    }
240

241
    protected function resetState(): void
1✔
242
    {
243
        parent::resetState();
1✔
244
        $this->keyArray = [];
1✔
245
    }
246
}
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