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

move-elevator / composer-translation-validator / 16598200090

29 Jul 2025 01:55PM UTC coverage: 95.366% (-1.4%) from 96.758%
16598200090

Pull #61

github

jackd248
test: enhance CollectorTest by adding assertions for parser classes and refining detector expectations
Pull Request #61: refactor: clean up tests by removing redundant cases and improving validation logic

3 of 3 new or added lines in 1 file covered. (100.0%)

33 existing lines in 3 files now uncovered.

2264 of 2374 relevant lines covered (95.37%)

7.52 hits per line

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

82.39
/src/Result/ValidationResultCliRenderer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer plugin "composer-translation-validator".
7
 *
8
 * Copyright (C) 2025 Konrad Michalik <km@move-elevator.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace MoveElevator\ComposerTranslationValidator\Result;
25

26
use MoveElevator\ComposerTranslationValidator\Utility\PathUtility;
27
use MoveElevator\ComposerTranslationValidator\Validator\ResultType;
28
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorInterface;
29
use ReflectionClass;
30
use Symfony\Component\Console\Input\InputInterface;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Style\SymfonyStyle;
33
use Throwable;
34

35
class ValidationResultCliRenderer extends AbstractValidationResultRenderer
36
{
37
    private readonly SymfonyStyle $io;
38

39
    public function __construct(
21✔
40
        OutputInterface $output,
41
        private readonly InputInterface $input,
42
        bool $dryRun = false,
43
        bool $strict = false,
44
    ) {
45
        parent::__construct($output, $dryRun, $strict);
21✔
46
        $this->io = new SymfonyStyle($this->input, $this->output);
21✔
47
    }
48

49
    public function render(ValidationResult $validationResult): int
18✔
50
    {
51
        if ($this->output->isVerbose()) {
18✔
52
            $this->renderVerboseOutput($validationResult);
3✔
53
        } else {
54
            $this->renderCompactOutput($validationResult);
15✔
55
        }
56

57
        $this->renderSummary($validationResult->getOverallResult());
18✔
58

59
        if ($this->output->isVerbose()) {
18✔
60
            $this->renderStatistics($validationResult);
3✔
61
        }
62

63
        return $this->calculateExitCode($validationResult);
18✔
64
    }
65

66
    private function renderHeader(): void
3✔
67
    {
68
        $this->io->title('Composer Translation Validator');
3✔
69
        $this->io->text([
3✔
70
            'A comprehensive tool for validating translation files (XLIFF, YAML, JSON and PHP).',
3✔
71
            'Checks for mismatches, duplicates, placeholder consistency and schema compliance.',
3✔
72
            '',
3✔
73
            'For more information and usage examples, run: <fg=cyan>composer validate-translations --help</>',
3✔
74
        ]);
3✔
75
        $this->io->newLine();
3✔
76
    }
77

78
    private function renderCompactOutput(ValidationResult $validationResult): void
15✔
79
    {
80
        $groupedByFile = $this->groupIssuesByFile($validationResult);
15✔
81

82
        if (empty($groupedByFile)) {
15✔
83
            return;
11✔
84
        }
85

86
        // Check if we have any errors (not just warnings)
87
        $hasErrors = false;
4✔
88
        foreach ($groupedByFile as $validatorGroups) {
4✔
89
            foreach ($validatorGroups as $validatorData) {
4✔
90
                if ($validatorData['validator']->hasIssues()
4✔
91
                    && ResultType::ERROR === $validatorData['validator']
4✔
92
                        ->resultTypeOnValidationFailure()) {
4✔
93
                    $hasErrors = true;
3✔
94
                    break 2;
3✔
95
                }
96
            }
97
        }
98

99
        // Only show detailed output for errors, not warnings
100
        if (!$hasErrors) {
4✔
101
            return;
1✔
102
        }
103

104
        foreach ($groupedByFile as $filePath => $validatorGroups) {
3✔
105
            $relativePath = PathUtility::normalizeFolderPath($filePath);
3✔
106
            $this->io->writeln("<fg=cyan>$relativePath</>");
3✔
107
            $this->io->newLine();
3✔
108

109
            $fileIssues = [];
3✔
110
            foreach ($validatorGroups as $validatorData) {
3✔
111
                foreach ($validatorData['issues'] as $issue) {
3✔
112
                    $fileIssues[] = [
3✔
113
                        'validator' => $validatorData['validator'],
3✔
114
                        'issue' => $issue,
3✔
115
                    ];
3✔
116
                }
117
            }
118

119
            $sortedIssues = $this->sortIssuesBySeverity($fileIssues);
3✔
120

121
            foreach ($sortedIssues as $fileIssue) {
3✔
122
                $validator = $fileIssue['validator'];
3✔
123
                $issue = $fileIssue['issue'];
3✔
124
                $validatorName = $validator->getShortName();
3✔
125

126
                $message = $this->formatIssueMessage($validator, $issue, $validatorName);
3✔
127
                $lines = explode("\n", $message);
3✔
128
                foreach ($lines as $line) {
3✔
129
                    if (!empty(trim($line))) {
3✔
130
                        $this->io->writeln($line);
3✔
131
                    }
132
                }
133
            }
134

135
            $this->io->newLine();
3✔
136
        }
137
    }
138

139
    private function renderVerboseOutput(ValidationResult $validationResult): void
3✔
140
    {
141
        $this->renderHeader();
3✔
142

143
        $groupedByFile = $this->groupIssuesByFile($validationResult);
3✔
144

145
        if (empty($groupedByFile)) {
3✔
146
            return;
2✔
147
        }
148

149
        foreach ($groupedByFile as $filePath => $validatorGroups) {
1✔
150
            $relativePath = PathUtility::normalizeFolderPath($filePath);
1✔
151
            $this->io->writeln("<fg=cyan>$relativePath</>");
1✔
152
            $this->io->newLine();
1✔
153

154
            $sortedValidatorGroups = $this->sortValidatorGroupsBySeverity($validatorGroups);
1✔
155

156
            foreach ($sortedValidatorGroups as $validatorName => $validatorData) {
1✔
157
                $validator = $validatorData['validator'];
1✔
158
                $issues = $validatorData['issues'];
1✔
159

160
                $this->io->writeln("  <options=bold>$validatorName</>");
1✔
161

162
                foreach ($issues as $issue) {
1✔
163
                    $message = $this->formatIssueMessage($validator, $issue, '', true);
1✔
164
                    $lines = explode("\n", $message);
1✔
165
                    foreach ($lines as $line) {
1✔
166
                        if (!empty(trim($line))) {
1✔
167
                            $this->io->writeln("    $line");
1✔
168
                        }
169
                    }
170
                }
171

172
                if ($validator->shouldShowDetailedOutput()) {
1✔
173
                    $this->io->newLine();
×
174
                    $validator->renderDetailedOutput($this->output, $issues);
×
175
                }
176

177
                $this->io->newLine();
1✔
178
            }
179
        }
180
    }
181

182
    /**
183
     * @param array<array{validator: ValidatorInterface, issue: Issue}> $fileIssues
184
     *
185
     * @return array<array{validator: ValidatorInterface, issue: Issue}>
186
     */
187
    private function sortIssuesBySeverity(array $fileIssues): array
3✔
188
    {
189
        usort($fileIssues, function ($a, $b) {
3✔
UNCOV
190
            $severityA = $this->getIssueSeverity($a['validator']);
×
UNCOV
191
            $severityB = $this->getIssueSeverity($b['validator']);
×
192

UNCOV
193
            return $severityA <=> $severityB;
×
194
        });
3✔
195

196
        return $fileIssues;
3✔
197
    }
198

199
    /**
200
     * @param array<string, array{validator: ValidatorInterface, type: string, issues: array<Issue>}> $validatorGroups
201
     *
202
     * @return array<string, array{validator: ValidatorInterface, type: string, issues: array<Issue>}>
203
     */
204
    private function sortValidatorGroupsBySeverity(array $validatorGroups): array
1✔
205
    {
206
        uksort($validatorGroups, function ($validatorNameA, $validatorNameB) use ($validatorGroups) {
1✔
UNCOV
207
            $validatorA = $validatorGroups[$validatorNameA]['validator'];
×
UNCOV
208
            $validatorB = $validatorGroups[$validatorNameB]['validator'];
×
209

UNCOV
210
            $severityA = $this->getValidatorSeverity($validatorA::class);
×
UNCOV
211
            $severityB = $this->getValidatorSeverity($validatorB::class);
×
212

UNCOV
213
            return $severityA <=> $severityB;
×
214
        });
1✔
215

216
        return $validatorGroups;
1✔
217
    }
218

UNCOV
219
    private function getIssueSeverity(ValidatorInterface $validator): int
×
220
    {
UNCOV
221
        return $this->getValidatorSeverity($validator::class);
×
222
    }
223

UNCOV
224
    private function getValidatorSeverity(string $validatorClass): int
×
225
    {
UNCOV
226
        if (str_contains($validatorClass, 'SchemaValidator')) {
×
UNCOV
227
            return 1;
×
228
        }
229

230
        try {
UNCOV
231
            if (!class_exists($validatorClass)) {
×
UNCOV
232
                return 1;
×
233
            }
234
            /** @var class-string $validatorClass */
UNCOV
235
            $reflection = new ReflectionClass($validatorClass);
×
UNCOV
236
            if ($reflection->isInstantiable()) {
×
UNCOV
237
                $validator = $reflection->newInstance();
×
UNCOV
238
                if ($validator instanceof ValidatorInterface) {
×
UNCOV
239
                    $resultType = $validator->resultTypeOnValidationFailure();
×
240

241
                    return ResultType::ERROR === $resultType ? 1 : 2;
×
242
                }
243
            }
UNCOV
244
        } catch (Throwable) {
×
245
        }
246

UNCOV
247
        return 1;
×
248
    }
249

250
    private function formatIssueMessage(
4✔
251
        ValidatorInterface $validator,
252
        Issue $issue,
253
        string $validatorName = '',
254
        bool $isVerbose = false,
255
    ): string {
256
        $prefix = $isVerbose ? '' : "($validatorName) ";
4✔
257

258
        return $validator->formatIssueMessage($issue, $prefix);
4✔
259
    }
260

261
    private function renderSummary(ResultType $resultType): void
18✔
262
    {
263
        if ($resultType->notFullySuccessful()) {
18✔
264
            $message = $this->generateMessage(new ValidationResult([], $resultType));
14✔
265

266
            if (!$this->output->isVerbose()) {
14✔
267
                $message .= ' See more details with the `-v` verbose option.';
12✔
268

269
                if (ResultType::WARNING === $resultType && !$this->strict) {
12✔
270
                    $message .= ' Use `--strict` to treat warnings as errors.';
4✔
271
                }
272
            }
273

274
            if (ResultType::WARNING === $resultType
14✔
275
                && !$this->dryRun
14✔
276
                && !$this->strict
14✔
277
                && !$this->output->isVerbose()) {
14✔
278
                $this->output->writeln('<fg=yellow>'.$message.'</>');
4✔
279
            } else {
280
                $this->io->newLine();
10✔
281
                $this->io->{$this->dryRun || (ResultType::WARNING === $resultType && !$this->strict) ? 'warning' : 'error'}($message);
10✔
282
            }
283
        } else {
284
            $message = $this->generateMessage(new ValidationResult([], $resultType));
4✔
285
            $this->output->isVerbose()
4✔
286
                ? $this->io->success($message)
1✔
287
                : $this->output->writeln('<fg=green>'.$message.'</>');
3✔
288
        }
289
    }
290

291
    private function renderStatistics(ValidationResult $validationResult): void
3✔
292
    {
293
        $statisticsData = $this->formatStatisticsForOutput($validationResult);
3✔
294

295
        if (empty($statisticsData)) {
3✔
296
            return;
2✔
297
        }
298

299
        $this->io->newLine();
1✔
300
        $this->output->writeln('<fg=gray>Execution time: '.$statisticsData['execution_time_formatted'].'</>');
1✔
301
        $this->output->writeln('<fg=gray>Files checked: '.$statisticsData['files_checked'].'</>');
1✔
302
        $this->output->writeln('<fg=gray>Keys checked: '.$statisticsData['keys_checked'].'</>');
1✔
303
        $this->output->writeln('<fg=gray>Validators run: '.$statisticsData['validators_run'].'</>');
1✔
304
        $this->output->writeln('<fg=gray>Parsers cached: '.$statisticsData['parsers_cached'].'</>');
1✔
305
    }
306
}
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