• 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

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

3
declare(strict_types=1);
4

5
namespace MoveElevator\ComposerTranslationValidator\Result;
6

7
use MoveElevator\ComposerTranslationValidator\Validator\ResultType;
8
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorInterface;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
use Symfony\Component\Console\Style\SymfonyStyle;
12

13
class ValidationResultCliRenderer
14
{
15
    private readonly SymfonyStyle $io;
16

17
    public function __construct(
12✔
18
        private readonly OutputInterface $output,
19
        private readonly InputInterface $input,
20
        private readonly bool $dryRun = false,
21
        private readonly bool $strict = false,
22
    ) {
23
        $this->io = new SymfonyStyle($this->input, $this->output);
12✔
24
    }
25

26
    public function render(ValidationResult $validationResult): int
12✔
27
    {
28
        if ($this->output->isVerbose()) {
12✔
29
            $this->renderVerboseOutput($validationResult);
3✔
30
        } else {
31
            $this->renderCompactOutput($validationResult);
10✔
32
        }
33

34
        $this->renderSummary($validationResult->getOverallResult());
12✔
35

36
        return $validationResult->getOverallResult()->resolveErrorToCommandExitCode($this->dryRun, $this->strict);
12✔
37
    }
38

39
    private function renderCompactOutput(ValidationResult $validationResult): void
10✔
40
    {
41
        $validatorPairs = $validationResult->getValidatorFileSetPairs();
10✔
42

43
        if (empty($validatorPairs)) {
10✔
44
            return;
3✔
45
        }
46

47
        // Group by file path
48
        $groupedByFile = [];
7✔
49
        foreach ($validatorPairs as $pair) {
7✔
50
            $validator = $pair['validator'];
7✔
51
            $fileSet = $pair['fileSet'];
7✔
52

53
            if (!$validator->hasIssues()) {
7✔
54
                continue;
×
55
            }
56

57
            // Use validator's distribution method to handle file-specific issues
58
            $distributedIssues = $validator->distributeIssuesForDisplay($fileSet);
7✔
59

60
            foreach ($distributedIssues as $filePath => $issues) {
7✔
61
                if (!isset($groupedByFile[$filePath])) {
4✔
62
                    $groupedByFile[$filePath] = [];
4✔
63
                }
64

65
                foreach ($issues as $issue) {
4✔
66
                    $groupedByFile[$filePath][] = [
4✔
67
                        'validator' => $validator,
4✔
68
                        'issue' => $issue,
4✔
69
                    ];
4✔
70
                }
71
            }
72
        }
73

74
        foreach ($groupedByFile as $filePath => $fileIssues) {
7✔
75
            $relativePath = $this->getRelativePath($filePath);
4✔
76
            $this->io->writeln("<fg=cyan>$relativePath</>");
4✔
77
            $this->io->newLine();
4✔
78

79
            // Sort issues by severity (errors first, warnings second)
80
            $sortedIssues = $this->sortIssuesBySeverity($fileIssues);
4✔
81

82
            foreach ($sortedIssues as $fileIssue) {
4✔
83
                $validator = $fileIssue['validator'];
4✔
84
                $issue = $fileIssue['issue'];
4✔
85
                $validatorName = $this->getValidatorShortName($validator::class);
4✔
86

87
                $message = $this->formatIssueMessage($validator, $issue, $validatorName);
4✔
88
                // Handle multiple lines from formatIssueMessage
89
                $lines = explode("\n", $message);
4✔
90
                foreach ($lines as $line) {
4✔
91
                    if (!empty(trim($line))) {
4✔
92
                        $this->io->writeln($line);
4✔
93
                    }
94
                }
95
            }
96

97
            $this->io->newLine();
4✔
98
        }
99
    }
100

101
    private function renderVerboseOutput(ValidationResult $validationResult): void
3✔
102
    {
103
        $validatorPairs = $validationResult->getValidatorFileSetPairs();
3✔
104

105
        if (empty($validatorPairs)) {
3✔
106
            return;
1✔
107
        }
108

109
        // Group by file path, then by validator
110
        $groupedByFile = [];
2✔
111
        foreach ($validatorPairs as $pair) {
2✔
112
            $validator = $pair['validator'];
2✔
113
            $fileSet = $pair['fileSet'];
2✔
114

115
            if (!$validator->hasIssues()) {
2✔
NEW
116
                continue;
×
117
            }
118

119
            // Use validator's distribution method to handle file-specific issues
120
            $distributedIssues = $validator->distributeIssuesForDisplay($fileSet);
2✔
121
            $validatorClass = $validator::class;
2✔
122

123
            foreach ($distributedIssues as $filePath => $issues) {
2✔
124
                if (!isset($groupedByFile[$filePath])) {
2✔
125
                    $groupedByFile[$filePath] = [];
2✔
126
                }
127
                if (!isset($groupedByFile[$filePath][$validatorClass])) {
2✔
128
                    $groupedByFile[$filePath][$validatorClass] = [
2✔
129
                        'validator' => $validator,
2✔
130
                        'issues' => [],
2✔
131
                    ];
2✔
132
                }
133

134
                foreach ($issues as $issue) {
2✔
135
                    $groupedByFile[$filePath][$validatorClass]['issues'][] = $issue;
2✔
136
                }
137
            }
138
        }
139

140
        foreach ($groupedByFile as $filePath => $validatorGroups) {
2✔
141
            $relativePath = $this->getRelativePath($filePath);
2✔
142
            $this->io->writeln("<fg=cyan>$relativePath</>");
2✔
143
            $this->io->newLine();
2✔
144

145
            // Sort validator groups by severity (errors first, warnings second)
146
            $sortedValidatorGroups = $this->sortValidatorGroupsBySeverity($validatorGroups);
2✔
147

148
            foreach ($sortedValidatorGroups as $validatorClass => $data) {
2✔
149
                $validator = $data['validator'];
2✔
150
                $issues = $data['issues'];
2✔
151
                $validatorName = $this->getValidatorShortName($validatorClass);
2✔
152

153
                $this->io->writeln("  <options=bold>$validatorName</>");
2✔
154

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

166
                // Show detailed tables for certain validators in verbose mode
167
                if ($validator->shouldShowDetailedOutput()) {
2✔
NEW
UNCOV
168
                    $this->io->newLine();
×
NEW
UNCOV
169
                    $validator->renderDetailedOutput($this->output, $issues);
×
170
                }
171

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

177
    private function getValidatorShortName(string $validatorClass): string
5✔
178
    {
179
        $parts = explode('\\', $validatorClass);
5✔
180

181
        return end($parts);
5✔
182
    }
183

184
    private function getRelativePath(string $filePath): string
5✔
185
    {
186
        // If already relative (starts with ./), return as-is
187
        if (str_starts_with($filePath, './')) {
5✔
NEW
188
            return $filePath;
×
189
        }
190

191
        $cwd = getcwd();
5✔
192
        if ($cwd && str_starts_with($filePath, $cwd)) {
5✔
NEW
193
            return '.'.substr($filePath, strlen($cwd));
×
194
        }
195

196
        return $filePath;
5✔
197
    }
198

199
    /**
200
     * @param array<array{validator: ValidatorInterface, issue: Issue}> $fileIssues
201
     *
202
     * @return array<array{validator: ValidatorInterface, issue: Issue}>
203
     */
204
    private function sortIssuesBySeverity(array $fileIssues): array
4✔
205
    {
206
        usort($fileIssues, function ($a, $b) {
4✔
NEW
207
            $severityA = $this->getIssueSeverity($a['validator']);
×
NEW
208
            $severityB = $this->getIssueSeverity($b['validator']);
×
209

210
            // Errors (1) come before warnings (2)
NEW
211
            return $severityA <=> $severityB;
×
212
        });
4✔
213

214
        return $fileIssues;
4✔
215
    }
216

217
    /**
218
     * @param array<string, array{validator: ValidatorInterface, issues: array<Issue>}> $validatorGroups
219
     *
220
     * @return array<string, array{validator: ValidatorInterface, issues: array<Issue>}>
221
     */
222
    private function sortValidatorGroupsBySeverity(array $validatorGroups): array
2✔
223
    {
224
        uksort($validatorGroups, function ($validatorClassA, $validatorClassB) {
2✔
NEW
225
            $severityA = $this->getValidatorSeverity($validatorClassA);
×
NEW
UNCOV
226
            $severityB = $this->getValidatorSeverity($validatorClassB);
×
227

228
            // Errors (1) come before warnings (2)
NEW
229
            return $severityA <=> $severityB;
×
230
        });
2✔
231

232
        return $validatorGroups;
2✔
233
    }
234

NEW
235
    private function getIssueSeverity(ValidatorInterface $validator): int
×
236
    {
NEW
UNCOV
237
        return $this->getValidatorSeverity($validator::class);
×
238
    }
239

NEW
UNCOV
240
    private function getValidatorSeverity(string $validatorClass): int
×
241
    {
242
        // For SchemaValidator, maintain current behavior (always ERROR)
NEW
243
        if (str_contains($validatorClass, 'SchemaValidator')) {
×
NEW
244
            return 1; // Error
×
245
        }
246

247
        // For other validators, use their ResultType to determine severity
248
        try {
NEW
249
            $reflection = new \ReflectionClass($validatorClass);
×
NEW
250
            if ($reflection->isInstantiable()) {
×
NEW
UNCOV
251
                $validator = $reflection->newInstance();
×
NEW
UNCOV
252
                if ($validator instanceof ValidatorInterface) {
×
NEW
UNCOV
253
                    $resultType = $validator->resultTypeOnValidationFailure();
×
NEW
UNCOV
254
                    return $resultType === ResultType::ERROR ? 1 : 2;
×
255
                }
256
            }
NEW
UNCOV
257
        } catch (\ReflectionException | \Throwable $e) {
×
258
            // Fallback to error if we can't instantiate the validator
259
        }
260

261
        // Fallback to error
NEW
UNCOV
262
        return 1; // Error
×
263
    }
264

265

266
    private function formatIssueMessage(ValidatorInterface $validator, Issue $issue, string $validatorName = '', bool $isVerbose = false): string
5✔
267
    {
268
        $prefix = $isVerbose ? '' : "($validatorName) ";
5✔
269

270
        return $validator->formatIssueMessage($issue, $prefix, $isVerbose);
5✔
271
    }
272

273

274
    private function renderSummary(ResultType $resultType): void
12✔
275
    {
276
        if ($resultType->notFullySuccessful()) {
12✔
277
            $this->io->newLine();
9✔
278
            $message = $this->dryRun
9✔
279
                ? 'Language validation failed and completed in dry-run mode.'
1✔
280
                : 'Language validation failed.';
8✔
281

282
            if (!$this->output->isVerbose()) {
9✔
283
                $message .= ' See more details with the `-v` verbose option.';
8✔
284
            }
285

286
            $this->io->{$this->dryRun || ResultType::WARNING === $resultType ? 'warning' : 'error'}($message);
9✔
287
        } else {
288
            $message = 'Language validation succeeded.';
3✔
289
            $this->output->isVerbose()
3✔
290
                ? $this->io->success($message)
1✔
291
                : $this->output->writeln('<fg=green>'.$message.'</>');
2✔
292
        }
293
    }
294
}
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