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

move-elevator / composer-translation-validator / 16402091592

20 Jul 2025 04:47PM UTC coverage: 96.554% (-0.009%) from 96.563%
16402091592

push

github

web-flow
Merge pull request #39 from move-elevator/file-headers

feat: add file headers to all PHP files for licensing and attribution

108 of 118 new or added lines in 32 files covered. (91.53%)

1737 of 1799 relevant lines covered (96.55%)

8.05 hits per line

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

90.14
/src/Validator/EncodingValidator.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\Parser\JsonParser;
27
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
28
use MoveElevator\ComposerTranslationValidator\Parser\PhpParser;
29
use MoveElevator\ComposerTranslationValidator\Parser\XliffParser;
30
use MoveElevator\ComposerTranslationValidator\Parser\YamlParser;
31
use MoveElevator\ComposerTranslationValidator\Result\Issue;
32
use Normalizer;
33

34
class EncodingValidator extends AbstractValidator implements ValidatorInterface
35
{
36
    /**
37
     * @return array<string, string>
38
     */
39
    public function processFile(ParserInterface $file): array
11✔
40
    {
41
        $filePath = $file->getFilePath();
11✔
42
        $issues = [];
11✔
43

44
        // Read raw file content
45
        $content = file_get_contents($filePath);
11✔
46
        if (false === $content) {
11✔
47
            $this->logger?->error(
×
NEW
48
                'Could not read file content: '.$file->getFileName(),
×
49
            );
×
50

51
            return [];
×
52
        }
53

54
        // Early exit for empty files
55
        if ('' === $content) {
11✔
56
            return [];
×
57
        }
58

59
        // Check UTF-8 encoding first - if invalid, other checks may fail
60
        if (!$this->isValidUtf8($content)) {
11✔
61
            $issues['encoding'] = 'File is not valid UTF-8 encoded';
1✔
62

63
            // Skip other checks for invalid UTF-8 content
64
            return $issues;
1✔
65
        }
66

67
        // Check for BOM (fast byte check)
68
        $hasBom = $this->hasByteOrderMark($content);
10✔
69
        if ($hasBom) {
10✔
70
            $issues['bom'] = 'File contains UTF-8 Byte Order Mark (BOM)';
3✔
71
        }
72

73
        // Check for invisible/problematic characters
74
        $invisibleChars = $this->findInvisibleCharacters($content);
10✔
75
        if (!empty($invisibleChars)) {
10✔
76
            $issues['invisible_chars'] = sprintf(
5✔
77
                'File contains invisible characters: %s',
5✔
78
                implode(', ', array_unique($invisibleChars)),
5✔
79
            );
5✔
80
        }
81

82
        // Check Unicode normalization (expensive, only if intl available)
83
        if ($this->hasUnicodeNormalizationIssues($content)) {
10✔
84
            $issues['unicode_normalization'] = 'File contains non-NFC normalized Unicode characters';
1✔
85
        }
86

87
        // Note: JSON syntax validation is handled by JsonParser constructor
88
        // Invalid JSON files will throw exceptions before reaching this validator
89

90
        return $issues;
10✔
91
    }
92

93
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
1✔
94
    {
95
        $details = $issue->getDetails();
1✔
96
        $resultType = $this->resultTypeOnValidationFailure();
1✔
97

98
        $level = $resultType->toString();
1✔
99
        $color = $resultType->toColorString();
1✔
100

101
        $messages = [];
1✔
102
        foreach ($details as $type => $message) {
1✔
103
            if (is_string($type) && is_string($message)) {
1✔
104
                $messages[] = "- <fg=$color>$level</> {$prefix}encoding issue: $message";
1✔
105
            }
106
        }
107

108
        return implode("\n", $messages);
1✔
109
    }
110

111
    /**
112
     * @return class-string<ParserInterface>[]
113
     */
114
    public function supportsParser(): array
1✔
115
    {
116
        return [XliffParser::class, YamlParser::class, JsonParser::class, PhpParser::class];
1✔
117
    }
118

119
    public function resultTypeOnValidationFailure(): ResultType
2✔
120
    {
121
        return ResultType::WARNING;
2✔
122
    }
123

124
    private function isValidUtf8(string $content): bool
11✔
125
    {
126
        return mb_check_encoding($content, 'UTF-8');
11✔
127
    }
128

129
    private function hasByteOrderMark(string $content): bool
10✔
130
    {
131
        // UTF-8 BOM is 0xEF 0xBB 0xBF
132
        return str_starts_with($content, "\xEF\xBB\xBF");
10✔
133
    }
134

135
    /**
136
     * @return array<string>
137
     */
138
    private function findInvisibleCharacters(string $content): array
10✔
139
    {
140
        $problematicChars = [];
10✔
141

142
        // Early exit for ASCII-only content (performance optimization)
143
        if (mb_check_encoding($content, 'ASCII')) {
10✔
144
            // Only check for control characters in ASCII content
145
            if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $content)) {
4✔
146
                $problematicChars[] = 'Control characters';
1✔
147
            }
148

149
            return $problematicChars;
4✔
150
        }
151

152
        // Check for problematic Unicode characters individually for better performance
153
        $charMap = [
6✔
154
            "\u{200B}" => 'Zero-width space',
6✔
155
            "\u{200C}" => 'Zero-width non-joiner',
6✔
156
            "\u{200D}" => 'Zero-width joiner',
6✔
157
            "\u{2060}" => 'Word joiner',
6✔
158
            "\u{FEFF}" => 'Zero-width no-break space',
6✔
159
            "\u{200E}" => 'Left-to-right mark',
6✔
160
            "\u{200F}" => 'Right-to-left mark',
6✔
161
            "\u{00AD}" => 'Soft hyphen',
6✔
162
        ];
6✔
163

164
        foreach ($charMap as $char => $name) {
6✔
165
            if (str_contains($content, $char)) {
6✔
166
                $problematicChars[] = $name;
4✔
167
            }
168
        }
169

170
        // Check for control characters (except allowed whitespace)
171
        if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $content)) {
6✔
172
            $problematicChars[] = 'Control characters';
×
173
        }
174

175
        return $problematicChars;
6✔
176
    }
177

178
    private function hasUnicodeNormalizationIssues(string $content): bool
10✔
179
    {
180
        if (!class_exists('Normalizer')) {
10✔
181
            return false;
×
182
        }
183

184
        $normalized = Normalizer::normalize($content, Normalizer::FORM_C);
10✔
185

186
        return false !== $normalized && $content !== $normalized;
10✔
187
    }
188
}
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