Passed
Pull Request — master (#41)
by Alexander
02:56 queued 43s
created

ContentParser   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 43
eloc 110
c 2
b 1
f 0
dl 0
loc 257
ccs 109
cts 109
cp 1
rs 8.96

12 Methods

Rating   Name   Duplication   Size   Complexity  
A extract() 0 6 1
A __construct() 0 4 1
A getLinesData() 0 15 4
B extractMessagesFromTokens() 0 49 11
A hasSkippedLines() 0 3 1
A setDefaultCategory() 0 3 1
A tokensEqual() 0 7 3
A getSkippedLines() 0 3 1
A extractParametersFromTokens() 0 16 3
A setTranslator() 0 10 2
B getMessageStringFromTokens() 0 23 7
B splitTokensAsParams() 0 25 8

How to fix   Complexity   

Complex Class

Complex classes like ContentParser 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 ContentParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Translator\Extractor;
6
7
use RuntimeException;
8
9
/**
10
 * Extracts translation keys from a string given.
11
 */
12
final class ContentParser
13
{
14
    private string $translator = '->translate';
15
16
    /** @var array<string|array{0: int, 1: string, 2: int}> */
17
    private array $translatorTokens = [];
18
19
    private int $translatorTokenCount = 0;
20
21
    private string $defaultCategory = '';
22
23
    private static array $brackets = [
24
        ')' => '(',
25
        ']' => '[',
26
        '}' => '{',
27
    ];
28
29
    private array $skippedLines = [];
30
31 12
    public function __construct(?string $defaultCategory = null, ?string $translator = null)
32
    {
33 12
        $this->defaultCategory = $defaultCategory ?? $this->defaultCategory;
34 12
        $this->setTranslator($translator ?? $this->translator);
35 11
    }
36
37 12
    private function setTranslator(string $translator): void
38
    {
39 12
        $this->translator = $translator;
40 12
        $translatorTokens = token_get_all('<?php ' . $this->translator);
41 12
        array_shift($translatorTokens);
42 12
        $this->translatorTokens = $translatorTokens;
43 12
        $this->translatorTokenCount = count($this->translatorTokens);
44
45 12
        if ($this->translatorTokenCount < 2) {
46 1
            throw new RuntimeException('Translator cannot contain less than 2 tokens.');
47
        }
48 11
    }
49
50
    /**
51
     * @param string $content
52
     *
53
     * @psalm-return array<array-key|string, mixed|non-empty-list<string>>
54
     *
55
     * @return array[]
56
     */
57 11
    public function extract(string $content): array
58
    {
59 11
        $this->skippedLines = [];
60 11
        $tokens = token_get_all($content);
61
62 11
        return $this->extractMessagesFromTokens($tokens);
63
    }
64
65
    /**
66
     * @psalm-param array<integer, string|array{0: int, 1: string, 2: int}> $tokens
67
     *
68
     * @param array $tokens
69
     *
70
     * @psalm-return array<array-key|string, mixed|non-empty-list<string>>
71
     *
72
     * @return array
73
     */
74 11
    private function extractMessagesFromTokens(array $tokens): array
75
    {
76 11
        $messages = $buffer = [];
77 11
        $matchedTokensCount = $pendingParenthesisCount = 0;
78 11
        $startTranslatorTokenIndex = 0;
79
80 11
        foreach ($tokens as $indexToken => $token) {
81 11
            if (in_array($token[0], [T_WHITESPACE, T_COMMENT], true)) {
82 11
                continue;
83
            }
84
85 11
            if ($startTranslatorTokenIndex) {
86 11
                if ($this->tokensEqual($token, ')')) {
87 11
                    if ($pendingParenthesisCount === 0) {
88 11
                        $result = $this->extractParametersFromTokens($buffer);
89 11
                        if ($result === null) {
90 6
                            $skippedTokens = array_slice($tokens, $startTranslatorTokenIndex, $indexToken - $startTranslatorTokenIndex + 1);
91 6
                            $this->skippedLines[] = $this->getLinesData($skippedTokens);
92
                        } else {
93 8
                            $messages = array_merge_recursive($messages, $result);
94
                        }
95 11
                        $startTranslatorTokenIndex = 0;
96 11
                        $pendingParenthesisCount = 0;
97 11
                        $buffer = [];
98 11
                        continue;
99
                    }
100 11
                    $pendingParenthesisCount--;
101 11
                } elseif ($this->tokensEqual($token, '(')) {
102 11
                    $pendingParenthesisCount++;
103
                }
104 11
                $buffer[] = $token;
105
            } else {
106 11
                if ($matchedTokensCount === $this->translatorTokenCount) {
107 11
                    if ($this->tokensEqual($token, '(')) {
108 11
                        $startTranslatorTokenIndex = $indexToken - $this->translatorTokenCount;
109 11
                        continue;
110
                    }
111 11
                    $matchedTokensCount = 0;
112
                }
113
114 11
                if ($this->tokensEqual($token, $this->translatorTokens[$matchedTokensCount])) {
115 11
                    $matchedTokensCount++;
116
                } else {
117 11
                    $matchedTokensCount = 0;
118
                }
119
            }
120
        }
121
122 11
        return $messages;
123
    }
124
125
    /**
126
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
127
     *
128
     * @param array $tokens
129
     *
130
     * @psalm-return null|array<array-key|string, mixed|non-empty-list<string>>
131
     *
132
     * @return array|null
133
     */
134 11
    private function extractParametersFromTokens(array $tokens): ?array
135
    {
136 11
        $parameters = $this->splitTokensAsParams($tokens);
137
138 11
        if (!isset($parameters['id'])) {
139 6
            return null;
140
        }
141
142 8
        $messages = [$parameters['category'] ?? $this->defaultCategory => [$parameters['id']]];
143
144
        // Get translation messages from parameters
145 8
        if (isset($parameters['parameters'])) {
146 6
            $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($parameters['parameters']));
147
        }
148
149 8
        return $messages;
150
    }
151
152
    /**
153
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
154
     *
155
     * @psalm-return array{category?: null|string, id?: null|string, parameters?: null|list<array{0: int, 1: string, 2: int}|string>}
156
     */
157 11
    private function splitTokensAsParams(array $tokens): array
158
    {
159 11
        $parameters = [];
160 11
        $parameterIndex = 0;
161 11
        $commaStack = [];
162
163 11
        foreach ($tokens as $token) {
164 11
            if (empty($commaStack) && $token === ',') {
165 9
                $parameterIndex++;
166 9
                continue;
167
            }
168 11
            if (is_string($token)) {
169 9
                if (in_array($token, self::$brackets, true)) {
170 9
                    $commaStack[] = $token;
171 9
                } elseif (isset(self::$brackets[$token]) && array_pop($commaStack) !== self::$brackets[$token]) {
172 4
                    return [];
173
                }
174
            }
175 11
            $parameters[$parameterIndex][] = $token;
176
        }
177
178
        return [
179 11
            'id' => $this->getMessageStringFromTokens($parameters[0] ?? []),
180 11
            'parameters' => $parameters[1] ?? null,
181 11
            'category' => $this->getMessageStringFromTokens($parameters[2] ?? []),
182
        ];
183
    }
184
185
    /**
186
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
187
     *
188
     * @return string|null
189
     */
190 11
    private function getMessageStringFromTokens(array $tokens): ?string
191
    {
192 11
        if (empty($tokens) || $tokens[0][0] !== T_CONSTANT_ENCAPSED_STRING) {
193 10
            return null;
194
        }
195
196 11
        $fullMessage = substr($tokens[0][1], 1, -1);
197
198 11
        $i = 1;
199 11
        $countTokens = count($tokens);
200 11
        while ($i < $countTokens && $tokens[$i] === '.') {
201 9
            if ($tokens[$i + 1][0] === T_CONSTANT_ENCAPSED_STRING) {
202 9
                $fullMessage .= substr($tokens[$i + 1][1], 1, -1);
203 6
            } elseif (in_array($tokens[$i + 1][0], [T_LNUMBER, T_DNUMBER], true)) {
204 6
                $fullMessage .= $tokens[$i + 1][1];
205
            } else {
206 4
                return null;
207
            }
208
209 9
            $i += 2;
210
        }
211
212 8
        return stripcslashes($fullMessage);
213
    }
214
215
    /**
216
     * Finds out if two PHP tokens are equal.
217
     *
218
     * @param array{0: int, 1: string, 2: int}|string $a
219
     * @param array{0: int, 1: string, 2: int}|string $b
220
     *
221
     * @return bool
222
     */
223 11
    private function tokensEqual($a, $b): bool
224
    {
225 11
        if (is_string($a)) {
226 11
            return $a === $b;
227
        }
228
229 11
        return $a[0] === $b[0] && $a[1] === $b[1];
230
    }
231
232
    /**
233
     * @param array $tokens
234
     *
235
     * @psalm-param array<string|array{0: int, 1: string, 2: int}> $tokens
236
     *
237
     * @return array
238
     */
239 6
    private function getLinesData(array $tokens): array
240
    {
241 6
        $startLine = null;
242 6
        $codeLines = '';
243 6
        foreach ($tokens as $token) {
244 6
            if (is_array($token)) {
245 6
                if ($startLine === null) {
246 6
                    $startLine = $token[2];
247
                }
248 6
                $codeLines .= $token[1];
249
            } else {
250 6
                $codeLines .= $token;
251
            }
252
        }
253 6
        return [$startLine, $codeLines];
254
    }
255
256 2
    public function setDefaultCategory(string $defaultCategory): void
257
    {
258 2
        $this->defaultCategory = $defaultCategory;
259 2
    }
260
261 10
    public function hasSkippedLines(): bool
262
    {
263 10
        return !empty($this->skippedLines);
264
    }
265
266 7
    public function getSkippedLines(): array
267
    {
268 7
        return $this->skippedLines;
269
    }
270
}
271