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

move-elevator / composer-translation-validator / 16190747108

10 Jul 2025 08:59AM UTC coverage: 94.556% (+0.06%) from 94.496%
16190747108

Pull #22

github

web-flow
Merge 2da39f618 into 7695e6067
Pull Request #22: feat: implement ParserCache for caching parser instances and add cache statistics

40 of 49 new or added lines in 6 files covered. (81.63%)

5 existing lines in 1 file now uncovered.

938 of 992 relevant lines covered (94.56%)

5.72 hits per line

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

75.0
/src/Validator/AbstractValidator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace MoveElevator\ComposerTranslationValidator\Validator;
6

7
use MoveElevator\ComposerTranslationValidator\FileDetector\FileSet;
8
use MoveElevator\ComposerTranslationValidator\Parser\ParserCache;
9
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
10
use MoveElevator\ComposerTranslationValidator\Parser\ParserRegistry;
11
use MoveElevator\ComposerTranslationValidator\Result\Issue;
12
use Psr\Log\LoggerInterface;
13
use Symfony\Component\Console\Output\OutputInterface;
14

15
abstract class AbstractValidator
16
{
17
    /** @var array<Issue> */
18
    protected array $issues = [];
19

20
    public function __construct(protected ?LoggerInterface $logger = null)
56✔
21
    {
22
    }
56✔
23

24
    /**
25
     * @param string[]                           $files
26
     * @param class-string<ParserInterface>|null $parserClass
27
     *
28
     * @return array<string, array<mixed>>
29
     */
30
    public function validate(array $files, ?string $parserClass): array
5✔
31
    {
32
        // Reset state for fresh validation run
33
        $this->resetState();
5✔
34

35
        $name = $this->getShortName();
5✔
36
        $this->logger->debug(
5✔
37
            sprintf(
5✔
38
                '> Checking for <options=bold,underscore>%s</> ...',
5✔
39
                $name
5✔
40
            )
5✔
41
        );
5✔
42

43
        foreach ($files as $filePath) {
5✔
44
            $file = ParserCache::get($filePath, $parserClass ?: ParserRegistry::resolveParserClass($filePath, $this->logger));
5✔
45
            /* @var ParserInterface $file */
46

47
            if (!$file instanceof ParserInterface) {
5✔
NEW
48
                $this->logger?->debug(
×
NEW
49
                    sprintf(
×
NEW
50
                        'The file <fg=cyan>%s</> could not be parsed by the validator <fg=red>%s</>.',
×
NEW
51
                        $filePath,
×
NEW
52
                        static::class
×
NEW
53
                    )
×
NEW
54
                );
×
NEW
55
                continue;
×
56
            }
57

58
            if (!in_array($file::class, $this->supportsParser(), true)) {
5✔
59
                $this->logger?->debug(
×
60
                    sprintf(
×
61
                        'The file <fg=cyan>%s</> is not supported by the validator <fg=red>%s</>.',
×
62
                        $file->getFileName(),
×
63
                        static::class
×
64
                    )
×
65
                );
×
66
                continue;
×
67
            }
68

69
            $this->logger->debug('> Checking language file: <fg=gray>'.$file->getFileDirectory().'</><fg=cyan>'.$file->getFileName().'</> ...');
5✔
70

71
            $validationResult = $this->processFile($file);
5✔
72
            if (!empty($validationResult)) {
5✔
73
                $this->addIssue(new Issue(
4✔
74
                    $file->getFileName(),
4✔
75
                    $validationResult,
4✔
76
                    $file::class,
4✔
77
                    $name
4✔
78
                ));
4✔
79
            }
80
        }
81

82
        $this->postProcess();
5✔
83

84
        return array_map(fn ($issue) => $issue->toArray(), $this->issues);
5✔
85
    }
86

87
    /**
88
     * @return array<mixed>
89
     */
90
    abstract public function processFile(ParserInterface $file): array;
91

92
    /**
93
     * @return class-string<ParserInterface>[]
94
     */
95
    abstract public function supportsParser(): array;
96

97
    public function postProcess(): void
×
98
    {
99
        // This method can be overridden by subclasses to perform additional processing after validation.
100
    }
×
101

102
    public function resultTypeOnValidationFailure(): ResultType
2✔
103
    {
104
        return ResultType::ERROR;
2✔
105
    }
106

107
    public function hasIssues(): bool
6✔
108
    {
109
        return !empty($this->issues);
6✔
110
    }
111

112
    /**
113
     * @return array<Issue>
114
     */
115
    public function getIssues(): array
5✔
116
    {
117
        return $this->issues;
5✔
118
    }
119

120
    public function addIssue(Issue $issue): void
13✔
121
    {
122
        $this->issues[] = $issue;
13✔
123
    }
124

125
    /**
126
     * Reset validator state for fresh validation run.
127
     * Override in subclasses if they have additional state to reset.
128
     */
129
    protected function resetState(): void
8✔
130
    {
131
        $this->issues = [];
8✔
132
    }
133

134
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
3✔
135
    {
136
        $details = $issue->getDetails();
3✔
137
        $resultType = $this->resultTypeOnValidationFailure();
3✔
138

139
        $level = $resultType->toString();
3✔
140
        $color = $resultType->toColorString();
3✔
141

142
        $message = $details['message'] ?? 'Validation error';
3✔
143

144
        return "- <fg=$color>$level</> {$prefix}$message";
3✔
145
    }
146

147
    /**
148
     * @return array<string, array<Issue>>
149
     */
150
    public function distributeIssuesForDisplay(FileSet $fileSet): array
2✔
151
    {
152
        $distribution = [];
2✔
153

154
        foreach ($this->issues as $issue) {
2✔
155
            $fileName = $issue->getFile();
2✔
156
            if (empty($fileName)) {
2✔
157
                continue;
1✔
158
            }
159

160
            // Build full path from fileSet and filename for consistency
161
            $basePath = rtrim($fileSet->getPath(), '/');
1✔
162
            $filePath = $basePath.'/'.$fileName;
1✔
163

164
            if (!isset($distribution[$filePath])) {
1✔
165
                $distribution[$filePath] = [];
1✔
166
            }
167
            $distribution[$filePath][] = $issue;
1✔
168
        }
169

170
        return $distribution;
2✔
171
    }
172

173
    public function shouldShowDetailedOutput(): bool
1✔
174
    {
175
        return false;
1✔
176
    }
177

178
    /**
179
     * @param array<Issue> $issues
180
     */
181
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
×
182
    {
183
        // Default implementation: no detailed output
184
    }
×
185

186
    public function getShortName(): string
7✔
187
    {
188
        $classPart = strrchr(static::class, '\\');
7✔
189

190
        return false !== $classPart ? substr($classPart, 1) : static::class;
7✔
191
    }
192
}
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