Passed
Pull Request — master (#41)
by Def
02:44
created

ContentParser::getLinesData()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

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