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

ContentParser::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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