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

move-elevator / composer-translation-validator / 18560103885

16 Oct 2025 11:42AM UTC coverage: 95.519%. Remained the same
18560103885

Pull #73

github

jackd248
refactor: remove unnecessary type hint from MismatchValidator
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

82.39
/src/Result/ValidationResultCliRenderer.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\Result;
15

16
use MoveElevator\ComposerTranslationValidator\Utility\PathUtility;
17
use MoveElevator\ComposerTranslationValidator\Validator\{ResultType, ValidatorInterface};
18
use ReflectionClass;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Style\SymfonyStyle;
22
use Throwable;
23

24
/**
25
 * ValidationResultCliRenderer.
26
 *
27
 * @author Konrad Michalik <km@move-elevator.de>
28
 * @license GPL-3.0-or-later
29
 */
30
class ValidationResultCliRenderer extends AbstractValidationResultRenderer
31
{
32
    private readonly SymfonyStyle $io;
33

34
    public function __construct(
21✔
35
        OutputInterface $output,
36
        private readonly InputInterface $input,
37
        bool $dryRun = false,
38
        bool $strict = false,
39
    ) {
40
        parent::__construct($output, $dryRun, $strict);
21✔
41
        $this->io = new SymfonyStyle($this->input, $this->output);
21✔
42
    }
43

44
    public function render(ValidationResult $validationResult): int
18✔
45
    {
46
        if ($this->output->isVerbose()) {
18✔
47
            $this->renderVerboseOutput($validationResult);
3✔
48
        } else {
49
            $this->renderCompactOutput($validationResult);
15✔
50
        }
51

52
        $this->renderSummary($validationResult->getOverallResult());
18✔
53

54
        if ($this->output->isVerbose()) {
18✔
55
            $this->renderStatistics($validationResult);
3✔
56
        }
57

58
        return $this->calculateExitCode($validationResult);
18✔
59
    }
60

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

73
    private function renderCompactOutput(ValidationResult $validationResult): void
15✔
74
    {
75
        $groupedByFile = $this->groupIssuesByFile($validationResult);
15✔
76

77
        if (empty($groupedByFile)) {
15✔
78
            return;
11✔
79
        }
80

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

94
        // Only show detailed output for errors, not warnings
95
        if (!$hasErrors) {
4✔
96
            return;
1✔
97
        }
98

99
        foreach ($groupedByFile as $filePath => $validatorGroups) {
3✔
100
            $relativePath = PathUtility::normalizeFolderPath($filePath);
3✔
101
            $this->io->writeln("<fg=cyan>$relativePath</>");
3✔
102
            $this->io->newLine();
3✔
103

104
            $fileIssues = [];
3✔
105
            foreach ($validatorGroups as $validatorData) {
3✔
106
                foreach ($validatorData['issues'] as $issue) {
3✔
107
                    $fileIssues[] = [
3✔
108
                        'validator' => $validatorData['validator'],
3✔
109
                        'issue' => $issue,
3✔
110
                    ];
3✔
111
                }
112
            }
113

114
            $sortedIssues = $this->sortIssuesBySeverity($fileIssues);
3✔
115

116
            foreach ($sortedIssues as $fileIssue) {
3✔
117
                $validator = $fileIssue['validator'];
3✔
118
                $issue = $fileIssue['issue'];
3✔
119
                $validatorName = $validator->getShortName();
3✔
120

121
                $message = $this->formatIssueMessage($validator, $issue, $validatorName);
3✔
122
                $lines = explode("\n", $message);
3✔
123
                foreach ($lines as $line) {
3✔
124
                    if (!empty(trim($line))) {
3✔
125
                        $this->io->writeln($line);
3✔
126
                    }
127
                }
128
            }
129

130
            $this->io->newLine();
3✔
131
        }
132
    }
133

134
    private function renderVerboseOutput(ValidationResult $validationResult): void
3✔
135
    {
136
        $this->renderHeader();
3✔
137

138
        $groupedByFile = $this->groupIssuesByFile($validationResult);
3✔
139

140
        if (empty($groupedByFile)) {
3✔
141
            return;
2✔
142
        }
143

144
        foreach ($groupedByFile as $filePath => $validatorGroups) {
1✔
145
            $relativePath = PathUtility::normalizeFolderPath($filePath);
1✔
146
            $this->io->writeln("<fg=cyan>$relativePath</>");
1✔
147
            $this->io->newLine();
1✔
148

149
            $sortedValidatorGroups = $this->sortValidatorGroupsBySeverity($validatorGroups);
1✔
150

151
            foreach ($sortedValidatorGroups as $validatorName => $validatorData) {
1✔
152
                $validator = $validatorData['validator'];
1✔
153
                $issues = $validatorData['issues'];
1✔
154

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

157
                foreach ($issues as $issue) {
1✔
158
                    $message = $this->formatIssueMessage($validator, $issue, '', true);
1✔
159
                    $lines = explode("\n", $message);
1✔
160
                    foreach ($lines as $line) {
1✔
161
                        if (!empty(trim($line))) {
1✔
162
                            $this->io->writeln("    $line");
1✔
163
                        }
164
                    }
165
                }
166

167
                if ($validator->shouldShowDetailedOutput()) {
1✔
UNCOV
168
                    $this->io->newLine();
×
UNCOV
169
                    $validator->renderDetailedOutput($this->output, $issues);
×
170
                }
171

172
                $this->io->newLine();
1✔
173
            }
174
        }
175
    }
176

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

UNCOV
188
            return $severityA <=> $severityB;
×
189
        });
3✔
190

191
        return $fileIssues;
3✔
192
    }
193

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

UNCOV
205
            $severityA = $this->getValidatorSeverity($validatorA::class);
×
UNCOV
206
            $severityB = $this->getValidatorSeverity($validatorB::class);
×
207

208
            return $severityA <=> $severityB;
×
209
        });
1✔
210

211
        return $validatorGroups;
1✔
212
    }
213

214
    private function getIssueSeverity(ValidatorInterface $validator): int
×
215
    {
UNCOV
216
        return $this->getValidatorSeverity($validator::class);
×
217
    }
218

UNCOV
219
    private function getValidatorSeverity(string $validatorClass): int
×
220
    {
UNCOV
221
        if (str_contains($validatorClass, 'SchemaValidator')) {
×
222
            return 1;
×
223
        }
224

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

236
                    return ResultType::ERROR === $resultType ? 1 : 2;
×
237
                }
238
            }
239
        } catch (Throwable) {
×
240
        }
241

242
        return 1;
×
243
    }
244

245
    private function formatIssueMessage(
4✔
246
        ValidatorInterface $validator,
247
        Issue $issue,
248
        string $validatorName = '',
249
        bool $isVerbose = false,
250
    ): string {
251
        $prefix = $isVerbose ? '' : "($validatorName) ";
4✔
252

253
        return $validator->formatIssueMessage($issue, $prefix);
4✔
254
    }
255

256
    private function renderSummary(ResultType $resultType): void
18✔
257
    {
258
        if ($resultType->notFullySuccessful()) {
18✔
259
            $message = $this->generateMessage(new ValidationResult([], $resultType));
14✔
260

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

264
                if (ResultType::WARNING === $resultType && !$this->strict) {
12✔
265
                    $message .= ' Use `--strict` to treat warnings as errors.';
4✔
266
                }
267
            }
268

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

286
    private function renderStatistics(ValidationResult $validationResult): void
3✔
287
    {
288
        $statisticsData = $this->formatStatisticsForOutput($validationResult);
3✔
289

290
        if (empty($statisticsData)) {
3✔
291
            return;
2✔
292
        }
293

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