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

move-elevator / composer-translation-validator / 24129407547

08 Apr 2026 09:55AM UTC coverage: 95.526% (+0.04%) from 95.491%
24129407547

Pull #111

github

konradmichalik
fix: apply rector migrations
Pull Request #111: refactor: split KeyNamingConventionValidator into KeyConverter and ConventionDetector

113 of 117 new or added lines in 3 files covered. (96.58%)

2 existing lines in 1 file now uncovered.

2370 of 2481 relevant lines covered (95.53%)

8.41 hits per line

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

96.61
/src/Validator/ConventionDetector.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\Enum\KeyNamingConvention;
17

18
use function count;
19
use function in_array;
20

21
/**
22
 * ConventionDetector.
23
 *
24
 * @author Konrad Michalik <km@move-elevator.de>
25
 * @license GPL-3.0-or-later
26
 */
27
final class ConventionDetector
28
{
29
    /**
30
     * Detect which conventions a key matches.
31
     *
32
     * @return array<string>
33
     */
34
    public function detectKeyConventions(string $key): array
23✔
35
    {
36
        // For keys with dots, we need to handle dot.notation specially
37
        if (str_contains($key, '.')) {
23✔
38
            $matchingConventions = [];
8✔
39

40
            // First, check if the entire key matches dot.notation
41
            if (KeyNamingConvention::DOT_NOTATION->matches($key)) {
8✔
42
                $matchingConventions[] = KeyNamingConvention::DOT_NOTATION->value;
3✔
43
            }
44

45
            // Then check if all segments follow a consistent non-dot convention
46
            $segments = explode('.', $key);
8✔
47
            $consistentConventions = null;
8✔
48

49
            // Check which conventions ALL segments support (excluding dot.notation)
50
            foreach ($segments as $segment) {
8✔
51
                $segmentMatches = $this->detectSegmentConventions($segment);
8✔
52
                // Remove dot.notation from segment matches as it doesn't apply to individual segments
53
                $segmentMatches = array_filter($segmentMatches, fn ($conv) => $conv !== KeyNamingConvention::DOT_NOTATION->value);
8✔
54

55
                if (null === $consistentConventions) {
8✔
56
                    // First segment - initialize with its conventions
57
                    $consistentConventions = $segmentMatches;
8✔
58
                } else {
59
                    // Subsequent segments - keep only conventions that ALL segments support
60
                    $consistentConventions = array_intersect($consistentConventions, $segmentMatches);
8✔
61
                }
62
            }
63

64
            // Add segment-based conventions to the result
65
            if (!empty($consistentConventions) && !in_array('unknown', $consistentConventions, true)) {
8✔
66
                $matchingConventions = array_merge($matchingConventions, array_values($consistentConventions));
6✔
67
            }
68

69
            // If no convention matches, it's mixed
70
            if (empty($matchingConventions)) {
8✔
71
                return ['mixed_conventions'];
2✔
72
            }
73

74
            return array_unique($matchingConventions);
6✔
75
        } else {
76
            // No dots, check regular conventions
77
            return $this->detectSegmentConventions($key);
16✔
78
        }
79
    }
80

81
    /**
82
     * Detect conventions for a single segment (without dots).
83
     *
84
     * @return array<string>
85
     */
86
    public function detectSegmentConventions(string $segment): array
26✔
87
    {
88
        $matchingConventions = [];
26✔
89

90
        foreach (KeyNamingConvention::cases() as $convention) {
26✔
91
            if ($convention->matches($segment)) {
26✔
92
                $matchingConventions[] = $convention->value;
22✔
93
            }
94
        }
95

96
        // If no convention matches, classify as 'unknown'
97
        if (empty($matchingConventions)) {
26✔
98
            $matchingConventions[] = 'unknown';
6✔
99
        }
100

101
        return $matchingConventions;
26✔
102
    }
103

104
    /**
105
     * Analyze keys for consistency when no convention is configured.
106
     *
107
     * Returns raw data arrays (not Issue objects) so the caller can create Issues.
108
     *
109
     * @param array<string> $keys
110
     *
111
     * @return array<array<string, mixed>>
112
     */
113
    public function analyzeKeyConsistency(array $keys, string $fileName): array
16✔
114
    {
115
        if (empty($keys)) {
16✔
116
            return [];
2✔
117
        }
118

119
        $conventionCounts = [];
14✔
120
        $keyConventions = [];
14✔
121

122
        // Analyze each key to determine which conventions it matches
123
        foreach ($keys as $key) {
14✔
124
            $matchingConventions = $this->detectKeyConventions($key);
14✔
125
            $keyConventions[$key] = $matchingConventions;
14✔
126

127
            foreach ($matchingConventions as $convention) {
14✔
128
                $conventionCounts[$convention] = ($conventionCounts[$convention] ?? 0) + 1;
14✔
129
            }
130
        }
131

132
        // If all keys follow the same convention(s), no issues
133
        if (count($conventionCounts) <= 1) {
14✔
134
            return [];
3✔
135
        }
136

137
        // Find the most common convention
138
        $dominantConvention = array_key_first($conventionCounts);
11✔
139
        $maxCount = $conventionCounts[$dominantConvention];
11✔
140

141
        foreach ($conventionCounts as $convention => $count) {
11✔
142
            if ($count > $maxCount) {
11✔
NEW
143
                $dominantConvention = $convention;
×
NEW
144
                $maxCount = $count;
×
145
            }
146
        }
147

148
        $issues = [];
11✔
149
        $conventionNames = array_keys($conventionCounts);
11✔
150

151
        // Report inconsistencies
152
        foreach ($keys as $key) {
11✔
153
            $keyMatches = $keyConventions[$key];
11✔
154

155
            // If key doesn't match the dominant convention, it's an issue
156
            if (!in_array($dominantConvention, $keyMatches, true)) {
11✔
157
                $issues[] = [
10✔
158
                    'key' => $key,
10✔
159
                    'file' => $fileName,
10✔
160
                    'detected_conventions' => $keyMatches,
10✔
161
                    'dominant_convention' => $dominantConvention,
10✔
162
                    'all_conventions_found' => $conventionNames,
10✔
163
                    'inconsistency_type' => 'mixed_conventions',
10✔
164
                ];
10✔
165
            }
166
        }
167

168
        return $issues;
11✔
169
    }
170
}
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