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

move-elevator / composer-translation-validator / 16517396283

25 Jul 2025 08:21AM UTC coverage: 96.5% (-0.002%) from 96.502%
16517396283

Pull #46

github

jackd248
feat: add KeyNamingConventionValidator with configurable naming conventions
Pull Request #46: feat: add KeyNamingConventionValidator with configurable naming conventions

139 of 144 new or added lines in 6 files covered. (96.53%)

22 existing lines in 6 files now uncovered.

1875 of 1943 relevant lines covered (96.5%)

8.95 hits per line

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

85.87
/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
    public function __construct(protected ?LoggerInterface $logger = null) {}
178✔
40

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

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

60
        foreach ($files as $filePath) {
6✔
61
            $file = ParserCache::get(
6✔
62
                $filePath,
6✔
63
                $parserClass ?: ParserRegistry::resolveParserClass(
6✔
64
                    $filePath,
6✔
65
                    $this->logger,
6✔
66
                ),
6✔
67
            );
6✔
68
            /* @var ParserInterface $file */
69

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

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

93
            $this->logger?->debug(
5✔
94
                '> Checking language file: <fg=gray>'
5✔
95
                .$file->getFileDirectory()
5✔
96
                .'</><fg=cyan>'
5✔
97
                .$file->getFileName()
5✔
98
                .'</> ...',
5✔
99
            );
5✔
100

101
            $validationResult = $this->processFile($file);
5✔
102
            if (empty($validationResult)) {
5✔
103
                continue;
1✔
104
            }
105

106
            $this->addIssue(new Issue(
4✔
107
                $file->getFileName(),
4✔
108
                $validationResult,
4✔
109
                $file::class,
4✔
110
                $name,
4✔
111
            ));
4✔
112
        }
113

114
        $this->postProcess();
6✔
115

116
        return array_map(fn ($issue) => $issue->toArray(), $this->issues);
6✔
117
    }
118

119
    /**
120
     * @return array<mixed>
121
     */
122
    abstract public function processFile(ParserInterface $file): array;
123

124
    /**
125
     * @return class-string<ParserInterface>[]
126
     */
127
    abstract public function supportsParser(): array;
128

129
    public function postProcess(): void
×
130
    {
131
        // This method can be overridden by subclasses to perform
132
        // additional processing after validation.
UNCOV
133
    }
×
134

135
    public function resultTypeOnValidationFailure(): ResultType
2✔
136
    {
137
        return ResultType::ERROR;
2✔
138
    }
139

140
    public function hasIssues(): bool
9✔
141
    {
142
        return !empty($this->issues);
9✔
143
    }
144

145
    /**
146
     * @return array<Issue>
147
     */
148
    public function getIssues(): array
7✔
149
    {
150
        return $this->issues;
7✔
151
    }
152

153
    public function addIssue(Issue $issue): void
16✔
154
    {
155
        $this->issues[] = $issue;
16✔
156
    }
157

158
    /**
159
     * Reset validator state for fresh validation run.
160
     * Override in subclasses if they have additional state to reset.
161
     */
162
    protected function resetState(): void
10✔
163
    {
164
        $this->issues = [];
10✔
165
    }
166

167
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
3✔
168
    {
169
        $details = $issue->getDetails();
3✔
170
        $resultType = $this->resultTypeOnValidationFailure();
3✔
171

172
        $level = $resultType->toString();
3✔
173
        $color = $resultType->toColorString();
3✔
174

175
        $message = $details['message'] ?? 'Validation error';
3✔
176

177
        return "- <fg=$color>$level</> {$prefix}$message";
3✔
178
    }
179

180
    /**
181
     * @return array<string, array<Issue>>
182
     */
183
    public function distributeIssuesForDisplay(FileSet $fileSet): array
2✔
184
    {
185
        $distribution = [];
2✔
186

187
        foreach ($this->issues as $issue) {
2✔
188
            $fileName = $issue->getFile();
2✔
189
            if (empty($fileName)) {
2✔
190
                continue;
1✔
191
            }
192

193
            // Build full path from fileSet and filename for consistency
194
            $basePath = rtrim($fileSet->getPath(), '/');
1✔
195
            $filePath = $basePath.'/'.$fileName;
1✔
196

197
            $distribution[$filePath] ??= [];
1✔
198
            $distribution[$filePath][] = $issue;
1✔
199
        }
200

201
        return $distribution;
2✔
202
    }
203

204
    public function shouldShowDetailedOutput(): bool
1✔
205
    {
206
        return false;
1✔
207
    }
208

209
    /**
210
     * @param array<Issue> $issues
211
     */
UNCOV
212
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
×
213
    {
214
        // Default implementation: no detailed output
UNCOV
215
    }
×
216

217
    public function getShortName(): string
12✔
218
    {
219
        $classPart = strrchr(static::class, '\\');
12✔
220

221
        return false !== $classPart ? substr($classPart, 1) : static::class;
12✔
222
    }
223
}
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