Passed
Pull Request — master (#40)
by Def
02:00
created

TranslationExtractor   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Test Coverage

Coverage 99.08%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 50
eloc 122
c 1
b 0
f 0
dl 0
loc 282
ccs 108
cts 109
cp 0.9908
rs 8.4

13 Methods

Rating   Name   Duplication   Size   Complexity  
A hasSkippedLines() 0 3 1
A getDefaultCategory() 0 3 1
A withTranslator() 0 5 1
A extractMessagesFromFile() 0 13 2
C extractMessagesFromTokens() 0 43 12
B splitTokensAsParams() 0 27 11
A getMessageStringFromTokens() 0 23 6
A extractParametersFromTokens() 0 17 4
A getSkippedLines() 0 3 1
A extract() 0 19 5
A tokensEqual() 0 7 3
A getMessageFromPath() 0 17 2
A setDefaultCategory() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TranslationExtractor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TranslationExtractor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Translator;
6
7
use Yiisoft\Files\FileHelper;
8
use Yiisoft\Files\PathMatcher\PathMatcher;
9
10
/**
11
 * Extractor messages
12
 */
13
final class TranslationExtractor
14
{
15
    private string $translator = '->translate';
16
17
    /**
18
     * @var array<string|array{0: int, 1: string, 2: int}>
19
     */
20
    private array $translatorTokens = [];
21
    private int $sizeOfTranslator = 0;
22
23
    private array $skippedLines = [];
24
    private array $skippedLinesOfFile = [];
25
26
    private string $defaultCategory = '';
27
28
    /**
29
     * @var string[]
30
     */
31
    private array $only = ['**.php'];
32
33
    /**
34
     * @var string[]
35
     */
36
    private array $except = [
37
        '.svn',
38
        '.git',
39
        '.gitignore',
40
        '.gitkeep',
41
        '.hgignore',
42
        '.hgkeep',
43
        '/messages',
44
    ];
45
46
    private static array $commaSpare = [
47
        ')' => '(',
48
        ']' => '[',
49
        '}' => '{',
50
    ];
51
52
    /**
53
     * @param string $path
54
     * @param string[]|null $only
55
     * @param string[]|null $except
56
     * @return array
57
     */
58 9
    public function extract(string $path, ?array $only = null, ?array $except = null): array
59
    {
60 9
        if (!is_dir($path)) {
61 1
            throw new \RuntimeException(sprintf('Directory "%s" does not exist.', $path));
62
        }
63
64 8
        $translatorTokens = token_get_all('<?php ' . $this->translator);
65 8
        array_shift($translatorTokens);
66 8
        $this->translatorTokens = $translatorTokens;
67 8
        $this->sizeOfTranslator = count($this->translatorTokens);
68
69
70 8
        if ($this->sizeOfTranslator < 2) {
71 1
            throw new \RuntimeException('Translator tokens cannot be shorttest 2 tokens.');
72
        }
73
74 7
        $messages = $this->getMessageFromPath($path, $only === null ? $this->only : $only, $except === null ? $this->except : $except);
75
76 7
        return $messages;
77
    }
78
79
    /**
80
     * @param string $path
81
     * @param string[] $only
82
     * @param string[] $except
83
     * @return array
84
     */
85 7
    private function getMessageFromPath(string $path, array $only, array $except): array
86
    {
87 7
        $messages = [];
88
89 7
        $files = FileHelper::findFiles($path, [
90 7
            'filter' => (new pathMatcher)->only(...$only)->except(...$except),
91
            'recursive' => true,
92
        ]);
93
94
        /**
95
         * @var string[] $files
96
         */
97 7
        foreach ($files as $file) {
98 7
            $messages = array_merge_recursive($messages, $this->extractMessagesFromFile($file));
99
        }
100
101 7
        return $messages;
102
    }
103
104 7
    private function extractMessagesFromFile(string $fileName): array
105
    {
106 7
        $fileContent = file_get_contents($fileName);
107 7
        $tokens = token_get_all($fileContent);
108
109 7
        $this->skippedLinesOfFile = [];
110 7
        $messages = $this->extractMessagesFromTokens($tokens);
111
112 7
        if (!empty($this->skippedLinesOfFile)) {
113 4
            $this->skippedLines[$fileName] = $this->skippedLinesOfFile;
114
        }
115
116 7
        return $messages;
117
    }
118
119
    /**
120
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
121
     * @return array<array-key|string, mixed|non-empty-list<string>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key|string, ...non-empty-list<string>> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key|string, mixed|non-empty-list<string>>.
Loading history...
122
     */
123 7
    private function extractMessagesFromTokens(array $tokens): array
124
    {
125 7
        $messages = $buffer = [];
126 7
        $matchedTokensCount = $pendingParenthesisCount = 0;
127 7
        $isStartedTranslator = false;
128
129 7
        foreach ($tokens as $tokenIndex => $token) {
130 7
            if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
131 7
                continue;
132
            }
133
134 7
            if ($isStartedTranslator) {
135 7
                if ($this->tokensEqual($token, ')') && $pendingParenthesisCount === 0) {
136 7
                    $messages = array_merge_recursive($messages, $this->extractParametersFromTokens($buffer));
137 7
                    $isStartedTranslator = false;
138 7
                    $pendingParenthesisCount = 0;
139 7
                    $buffer = [];
140
                } else {
141 7
                    if ($this->tokensEqual($token, '(')) {
142 7
                        $pendingParenthesisCount++;
143 7
                    } elseif ($this->tokensEqual($token, ')')) {
144 7
                        $pendingParenthesisCount--;
145
                    }
146 7
                    $buffer[] = $token;
147
                }
148
149
            } else {
150 7
                if ($matchedTokensCount === $this->sizeOfTranslator) {
151 7
                    if ($this->tokensEqual($token, '(')) {
152 7
                        $isStartedTranslator = true;
153
                    }
154 7
                    $matchedTokensCount = 0;
155
                }
156
157 7
                if ($this->tokensEqual($token, $this->translatorTokens[$matchedTokensCount])) {
158 7
                    $matchedTokensCount++;
159
                } else {
160 7
                    $matchedTokensCount = 0;
161
                }
162
            }
163
        }
164
165 7
        return $messages;
166
    }
167
168
    /**
169
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
170
     * @return array<array-key|string, mixed|non-empty-list<string>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key|string, ...non-empty-list<string>> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key|string, mixed|non-empty-list<string>>.
Loading history...
171
     */
172 7
    private function extractParametersFromTokens(array $tokens): array
173
    {
174 7
        $messages = [];
175 7
        $parameters = $this->splitTokensAsParams($tokens);
176
177 7
        if ($parameters === null || $parameters['id'] === null) {
178 4
            $this->skippedLinesOfFile[] = $tokens;
179
        } else {
180 5
            $messages[$parameters['category'] ?? $this->defaultCategory][] = $parameters['id'];
181
182
            // Get translation messages from parameters
183 5
            if ($parameters['parameters'] !== null) {
184 4
                $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($parameters['parameters']));
185
            }
186
        }
187
188 7
        return $messages;
189
    }
190
191
    /**
192
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
193
     * @return null|array{category: null|string, id: null|string, parameters: non-empty-list<array{0: int, 1: string, 2: int}|string>|null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|array{category: nul..., 2: int}|string>|null} at position 18 could not be parsed: Unknown type name 'non-empty-list' at position 18 in null|array{category: null|string, id: null|string, parameters: non-empty-list<array{0: int, 1: string, 2: int}|string>|null}.
Loading history...
194
     */
195 7
    private function splitTokensAsParams(array $tokens): ?array
196
    {
197 7
        $parameters = [];
198 7
        $parameterIndex = 0;
199 7
        $commaStack = [];
200
201 7
        foreach ($tokens as $token) {
202 7
            if (empty($commaStack) && $token === ',') {
203 6
                $parameterIndex++;
204 6
                continue;
205
            }
206 7
            if (is_string($token)) {
207 6
                if (in_array($token, static::$commaSpare)) {
0 ignored issues
show
Bug introduced by
Since $commaSpare is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $commaSpare to at least protected.
Loading history...
208 6
                    array_push($commaStack, $token);
209 6
                } elseif (isset(static::$commaSpare[$token])) {
210 6
                    if (array_pop($commaStack) !== static::$commaSpare[$token]) {
211
                        return null;
212
                    }
213
                }
214
            }
215 7
            $parameters[$parameterIndex][] = $token;
216
        }
217
218
        return [
219 7
            'id' => isset($parameters[0]) ? $this->getMessageStringFromTokens($parameters[0]) : null,
220 7
            'parameters' => isset($parameters[1]) ? $parameters[1] : null,
221 7
            'category' => isset($parameters[2]) ? $this->getMessageStringFromTokens($parameters[2]) : null,
222
        ];
223
    }
224
225
    /**
226
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
227
     * @return string|null
228
     */
229 7
    private function getMessageStringFromTokens(array $tokens): ?string
230
    {
231 7
        if ($tokens[0][0] !== T_CONSTANT_ENCAPSED_STRING) {
232 4
            return null;
233
        }
234
235 7
        $fullMessage = substr($tokens[0][1], 1, -1);
236
237 7
        $i = 1;
238 7
        while ($i < count($tokens) && $tokens[$i] === '.') {
239
240 6
            if ($tokens[$i + 1][0] === T_CONSTANT_ENCAPSED_STRING) {
241 6
                $fullMessage .= substr($tokens[$i + 1][1], 1, -1);
242 4
            } elseif (in_array($tokens[$i + 1][0], [T_LNUMBER, T_DNUMBER])) {
243 4
                $fullMessage .= $tokens[$i + 1][1];
244
            } else {
245 3
                return null;
246
            }
247
248 6
            $i += 2;
249
        }
250
251 5
        return stripcslashes($fullMessage);
252
    }
253
254
    /**
255
     * Finds out if two PHP tokens are equal.
256
     *
257
     * @param array{0: int, 1: string, 2: int}|string $a
258
     * @param array{0: int, 1: string, 2: int}|string $b
259
     * @return bool
260
     */
261 7
    private function tokensEqual($a, $b): bool
262
    {
263 7
        if (is_string($a)) {
264 7
            return $a === $b;
265
        }
266
267 7
        return $a[0] === $b[0] && $a[1] == $b[1];
268
    }
269
270 7
    public function hasSkippedLines(): bool
271
    {
272 7
        return !empty($this->skippedLines);
273
    }
274
275 4
    public function getSkippedLines(): array
276
    {
277 4
        return $this->skippedLines;
278
    }
279
280 2
    public function getDefaultCategory(): string
281
    {
282 2
        return $this->defaultCategory;
283
    }
284
285 6
    public function setDefaultCategory(string $defaultCategory): void
286
    {
287 6
        $this->defaultCategory = $defaultCategory;
288 6
    }
289
290 2
    public function withTranslator(string $translator): self
291
    {
292 2
        $new = clone $this;
293 2
        $new->translator = $translator;
294 2
        return $new;
295
    }
296
}
297