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

move-elevator / composer-translation-validator / 24129407734

08 Apr 2026 09:55AM UTC coverage: 95.378% (-0.1%) from 95.491%
24129407734

Pull #112

github

konradmichalik
fix: apply rector migrations
Pull Request #112: refactor: replace static ParserCache with constructor injection

17 of 20 new or added lines in 3 files covered. (85.0%)

15 existing lines in 2 files now uncovered.

2373 of 2488 relevant lines covered (95.38%)

8.12 hits per line

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

78.43
/src/Validator/AbstractValidator.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-2026 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\{ParserCache, ParserInterface, ParserRegistry};
18
use MoveElevator\ComposerTranslationValidator\Result\Issue;
19
use Psr\Log\LoggerInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21

22
use function in_array;
23
use function is_array;
24
use function sprintf;
25

26
/**
27
 * AbstractValidator.
28
 *
29
 * @author Konrad Michalik <km@move-elevator.de>
30
 * @license GPL-3.0-or-later
31
 */
32
abstract class AbstractValidator
33
{
34
    /** @var array<Issue> */
35
    protected array $issues = [];
36

37
    protected string $currentFilePath = '';
38

39
    private ParserCache $parserCache;
40

41
    public function __construct(
251✔
42
        protected ?LoggerInterface $logger = null,
43
        ?ParserCache $parserCache = null,
44
    ) {
45
        $this->parserCache = $parserCache ?? new ParserCache();
251✔
46
    }
47

NEW
UNCOV
48
    public function setParserCache(ParserCache $parserCache): void
×
49
    {
NEW
UNCOV
50
        $this->parserCache = $parserCache;
×
51
    }
52

53
    /**
54
     * @param string[]                           $files
55
     * @param class-string<ParserInterface>|null $parserClass
56
     *
57
     * @return array<string, array<mixed>>
58
     */
59
    public function validate(array $files, ?string $parserClass): array
6✔
60
    {
61
        // Reset state for fresh validation run
62
        $this->resetState();
6✔
63

64
        $name = $this->getShortName();
6✔
65
        $this->logger?->debug(
6✔
66
            sprintf(
6✔
67
                '> Checking for <options=bold,underscore>%s</> ...',
6✔
68
                $name,
6✔
69
            ),
6✔
70
        );
6✔
71

72
        foreach ($files as $filePath) {
6✔
73
            $this->currentFilePath = $filePath;
6✔
74

75
            $file = $this->parserCache->get(
6✔
76
                $filePath,
6✔
77
                $parserClass ?: ParserRegistry::resolveParserClass(
6✔
78
                    $filePath,
6✔
79
                    $this->logger,
6✔
80
                ),
6✔
81
            );
6✔
82
            /* @var ParserInterface $file */
83

84
            if (!$file instanceof ParserInterface) {
6✔
85
                $this->logger?->debug(
×
86
                    sprintf(
×
87
                        'The file <fg=cyan>%s</> could not be parsed by the '
×
88
                        .'validator <fg=red>%s</>.',
×
89
                        $filePath,
×
UNCOV
90
                        static::class,
×
UNCOV
91
                    ),
×
UNCOV
92
                );
×
UNCOV
93
                continue;
×
94
            }
95

96
            if (!in_array($file::class, $this->supportsParser(), true)) {
6✔
97
                $this->logger?->debug(
1✔
98
                    sprintf(
1✔
99
                        'The file <fg=cyan>%s</> is not supported by the validator <fg=red>%s</>.',
1✔
100
                        $file->getFileName(),
1✔
101
                        static::class,
1✔
102
                    ),
1✔
103
                );
1✔
104
                continue;
1✔
105
            }
106

107
            $this->logger?->debug(
5✔
108
                '> Checking language file: <fg=gray>'
5✔
109
                .$file->getFileDirectory()
5✔
110
                .'</><fg=cyan>'
5✔
111
                .$file->getFileName()
5✔
112
                .'</> ...',
5✔
113
            );
5✔
114

115
            $validationResult = $this->processFile($file);
5✔
116
            if (empty($validationResult)) {
5✔
117
                continue;
1✔
118
            }
119

120
            // Handle case where processFile returns multiple issues
121
            if (isset($validationResult[0]) && is_array($validationResult[0])) {
4✔
122
                // Multiple issues - create one Issue object per item
123
                foreach ($validationResult as $issueData) {
×
124
                    $this->addIssue(new Issue(
×
125
                        $filePath,
×
UNCOV
126
                        $issueData,
×
UNCOV
127
                        $file::class,
×
UNCOV
128
                        $name,
×
UNCOV
129
                    ));
×
130
                }
131
            } else {
132
                // Single issue data - create one Issue object
133
                $this->addIssue(new Issue(
4✔
134
                    $filePath,
4✔
135
                    $validationResult,
4✔
136
                    $file::class,
4✔
137
                    $name,
4✔
138
                ));
4✔
139
            }
140
        }
141

142
        $this->postProcess();
6✔
143

144
        return array_map(fn ($issue) => $issue->toArray(), $this->issues);
6✔
145
    }
146

147
    /**
148
     * @return array<mixed>
149
     */
150
    abstract public function processFile(ParserInterface $file): array;
151

152
    /**
153
     * @return class-string<ParserInterface>[]
154
     */
155
    abstract public function supportsParser(): array;
156

157
    public function postProcess(): void
×
158
    {
159
        // This method can be overridden by subclasses to perform
160
        // additional processing after validation.
UNCOV
161
    }
×
162

163
    public function resultTypeOnValidationFailure(): ResultType
3✔
164
    {
165
        return ResultType::ERROR;
3✔
166
    }
167

168
    public function hasIssues(): bool
13✔
169
    {
170
        return !empty($this->issues);
13✔
171
    }
172

173
    /**
174
     * @return array<Issue>
175
     */
176
    public function getIssues(): array
9✔
177
    {
178
        return $this->issues;
9✔
179
    }
180

181
    public function addIssue(Issue $issue): void
22✔
182
    {
183
        $this->issues[] = $issue;
22✔
184
    }
185

186
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
7✔
187
    {
188
        $details = $issue->getDetails();
7✔
189
        $resultType = $this->resultTypeOnValidationFailure();
7✔
190

191
        $level = $resultType->toString();
7✔
192
        $color = $resultType->toColorString();
7✔
193

194
        $message = $details['message'] ?? 'Validation error';
7✔
195

196
        return "- <fg=$color>$level</> {$prefix}$message";
7✔
197
    }
198

199
    /**
200
     * @return array<string, array<Issue>>
201
     */
202
    public function distributeIssuesForDisplay(FileSet $fileSet): array
2✔
203
    {
204
        $distribution = [];
2✔
205

206
        foreach ($this->issues as $issue) {
2✔
207
            $filePath = $issue->getFile();
2✔
208
            if (empty($filePath)) {
2✔
209
                continue;
1✔
210
            }
211

212
            // Use the full file path directly since it's now stored in Issue objects
213
            $distribution[$filePath] ??= [];
1✔
214
            $distribution[$filePath][] = $issue;
1✔
215
        }
216

217
        return $distribution;
2✔
218
    }
219

220
    public function shouldShowDetailedOutput(): bool
1✔
221
    {
222
        return false;
1✔
223
    }
224

225
    /**
226
     * @param array<Issue> $issues
227
     */
UNCOV
228
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
×
229
    {
230
        // Default implementation: no detailed output
UNCOV
231
    }
×
232

233
    public function getShortName(): string
19✔
234
    {
235
        $classPart = strrchr(static::class, '\\');
19✔
236

237
        return false !== $classPart ? substr($classPart, 1) : static::class;
19✔
238
    }
239

240
    /**
241
     * Reset validator state for fresh validation run.
242
     * Override in subclasses if they have additional state to reset.
243
     */
244
    protected function resetState(): void
11✔
245
    {
246
        $this->issues = [];
11✔
247
    }
248
}
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