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

move-elevator / composer-translation-validator / 16538106049

26 Jul 2025 08:47AM UTC coverage: 96.072% (-0.4%) from 96.456%
16538106049

Pull #46

github

jackd248
test: add unit tests for Collector and XliffSchemaValidator functionality
Pull Request #46: feat: add KeyNamingConventionValidator with configurable naming conventions

256 of 285 new or added lines in 12 files covered. (89.82%)

2128 of 2215 relevant lines covered (96.07%)

8.19 hits per line

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

79.8
/src/Validator/AbstractValidator.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\Validator;
25

26
use MoveElevator\ComposerTranslationValidator\FileDetector\FileSet;
27
use MoveElevator\ComposerTranslationValidator\Parser\ParserCache;
28
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
29
use MoveElevator\ComposerTranslationValidator\Parser\ParserRegistry;
30
use MoveElevator\ComposerTranslationValidator\Result\Issue;
31
use Psr\Log\LoggerInterface;
32
use Symfony\Component\Console\Output\OutputInterface;
33

34
abstract class AbstractValidator
35
{
36
    /** @var array<Issue> */
37
    protected array $issues = [];
38

39
    protected string $currentFilePath = '';
40

41
    public function __construct(protected ?LoggerInterface $logger = null) {}
195✔
42

43
    /**
44
     * @param string[]                           $files
45
     * @param class-string<ParserInterface>|null $parserClass
46
     *
47
     * @return array<string, array<mixed>>
48
     */
49
    public function validate(array $files, ?string $parserClass): array
6✔
50
    {
51
        // Reset state for fresh validation run
52
        $this->resetState();
6✔
53

54
        $name = $this->getShortName();
6✔
55
        $this->logger?->debug(
6✔
56
            sprintf(
6✔
57
                '> Checking for <options=bold,underscore>%s</> ...',
6✔
58
                $name,
6✔
59
            ),
6✔
60
        );
6✔
61

62
        foreach ($files as $filePath) {
6✔
63
            $this->currentFilePath = $filePath;
6✔
64

65
            $file = ParserCache::get(
6✔
66
                $filePath,
6✔
67
                $parserClass ?: ParserRegistry::resolveParserClass(
6✔
68
                    $filePath,
6✔
69
                    $this->logger,
6✔
70
                ),
6✔
71
            );
6✔
72
            /* @var ParserInterface $file */
73

74
            if (!$file instanceof ParserInterface) {
6✔
75
                $this->logger?->debug(
×
76
                    sprintf(
×
77
                        'The file <fg=cyan>%s</> could not be parsed by the '
×
78
                        .'validator <fg=red>%s</>.',
×
79
                        $filePath,
×
80
                        static::class,
×
81
                    ),
×
82
                );
×
83
                continue;
×
84
            }
85

86
            if (!in_array($file::class, $this->supportsParser(), true)) {
6✔
87
                $this->logger?->debug(
1✔
88
                    sprintf(
1✔
89
                        'The file <fg=cyan>%s</> is not supported by the validator <fg=red>%s</>.',
1✔
90
                        $file->getFileName(),
1✔
91
                        static::class,
1✔
92
                    ),
1✔
93
                );
1✔
94
                continue;
1✔
95
            }
96

97
            $this->logger?->debug(
5✔
98
                '> Checking language file: <fg=gray>'
5✔
99
                .$file->getFileDirectory()
5✔
100
                .'</><fg=cyan>'
5✔
101
                .$file->getFileName()
5✔
102
                .'</> ...',
5✔
103
            );
5✔
104

105
            $validationResult = $this->processFile($file);
5✔
106
            if (empty($validationResult)) {
5✔
107
                continue;
1✔
108
            }
109

110
            // Handle case where processFile returns multiple issues
111
            if (isset($validationResult[0]) && is_array($validationResult[0])) {
4✔
112
                // Multiple issues - create one Issue object per item
NEW
113
                foreach ($validationResult as $issueData) {
×
NEW
114
                    $this->addIssue(new Issue(
×
NEW
115
                        $filePath,
×
NEW
116
                        $issueData,
×
NEW
117
                        $file::class,
×
NEW
118
                        $name,
×
NEW
119
                    ));
×
120
                }
121
            } else {
122
                // Single issue data - create one Issue object
123
                $this->addIssue(new Issue(
4✔
124
                    $filePath,
4✔
125
                    $validationResult,
4✔
126
                    $file::class,
4✔
127
                    $name,
4✔
128
                ));
4✔
129
            }
130
        }
131

132
        $this->postProcess();
6✔
133

134
        return array_map(fn ($issue) => $issue->toArray(), $this->issues);
6✔
135
    }
136

137
    /**
138
     * @return array<mixed>
139
     */
140
    abstract public function processFile(ParserInterface $file): array;
141

142
    /**
143
     * @return class-string<ParserInterface>[]
144
     */
145
    abstract public function supportsParser(): array;
146

147
    public function postProcess(): void
×
148
    {
149
        // This method can be overridden by subclasses to perform
150
        // additional processing after validation.
151
    }
×
152

153
    public function resultTypeOnValidationFailure(): ResultType
2✔
154
    {
155
        return ResultType::ERROR;
2✔
156
    }
157

158
    public function hasIssues(): bool
13✔
159
    {
160
        return !empty($this->issues);
13✔
161
    }
162

163
    /**
164
     * @return array<Issue>
165
     */
166
    public function getIssues(): array
9✔
167
    {
168
        return $this->issues;
9✔
169
    }
170

171
    public function addIssue(Issue $issue): void
20✔
172
    {
173
        $this->issues[] = $issue;
20✔
174
    }
175

176
    /**
177
     * Reset validator state for fresh validation run.
178
     * Override in subclasses if they have additional state to reset.
179
     */
180
    protected function resetState(): void
11✔
181
    {
182
        $this->issues = [];
11✔
183
    }
184

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

190
        $level = $resultType->toString();
3✔
191
        $color = $resultType->toColorString();
3✔
192

193
        $message = $details['message'] ?? 'Validation error';
3✔
194

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

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

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

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

216
        return $distribution;
2✔
217
    }
218

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

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

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

236
        return false !== $classPart ? substr($classPart, 1) : static::class;
15✔
237
    }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc