TokenMatcherSpecParser   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 173
dl 0
loc 355
rs 3.28
c 2
b 0
f 0
wmc 64

21 Methods

Rating   Name   Duplication   Size   Complexity  
A loadFromFile() 0 8 2
A detectTokenRegExp() 0 24 5
B buildMatcherSpec() 0 27 7
A resetCodeBlock() 0 5 2
A afterProcessingSource() 0 3 1
A codeBlockExists() 0 3 1
A isCurrentCodeBlock() 0 3 1
A replaceCurrentCodeBlock() 0 4 1
A getDocBlockFactory() 0 7 2
C processPhpToken() 0 38 14
A getCodeBlock() 0 7 2
A detectTemplateClassName() 0 12 4
A detectCodeBlock() 0 25 6
A restoreCurrentCodeBlock() 0 3 1
A parseUsedClass() 0 10 2
A detectUnprocessedTokenBlock() 0 17 3
A appendCodeBlock() 0 5 2
A __construct() 0 3 1
A detectTargetClassName() 0 12 4
A getMatcherSpec() 0 7 2
A switchCurrentCodeBlock() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\UniLex\Lexer;
6
7
use phpDocumentor\Reflection\DocBlock;
8
use phpDocumentor\Reflection\DocBlockFactory;
9
use phpDocumentor\Reflection\DocBlockFactoryInterface;
10
use ReflectionException;
11
use Remorhaz\UniLex\Exception;
12
13
class TokenMatcherSpecParser
14
{
15
    private const TAG_LEX_TARGET_CLASS = 'lexTargetClass';
16
    private const TAG_LEX_TEMPLATE_CLASS = 'lexTemplateClass';
17
    private const TAG_LEX_HEADER = 'lexHeader';
18
    private const TAG_LEX_BEFORE_MATCH = 'lexBeforeMatch';
19
    private const TAG_LEX_ON_TRANSITION = 'lexOnTransition';
20
    private const TAG_LEX_ON_ERROR = 'lexOnError';
21
    private const TAG_LEX_TOKEN = 'lexToken';
22
    private const TAG_LEX_MODE = 'lexMode';
23
    private const LEX_NAMESPACE = 'namespace';
24
    private const LEX_USE = 'use';
25
    private const LEX_TOKEN_REGEXP = 'token_regexp';
26
    private const LEX_TOKEN_CONTEXT = 'token_context';
27
28
    private $source;
29
30
    private $matcherSpec;
31
32
    private $targetClassName;
33
34
    private $templateClassName;
35
36
    private $docBlockFactory;
37
38
    private $codeBlockList = [];
39
40
    private $codeBlockKey;
41
42
    private $codeBlockStack = [];
43
44
    private $skipToken = false;
45
46
    private $usedClassList = [];
47
48
    private $tokenSpecList = [];
49
50
    public function __construct(string $source)
51
    {
52
        $this->source = $source;
53
    }
54
55
    /**
56
     * @param string $fileName
57
     * @return TokenMatcherSpecParser
58
     * @throws Exception
59
     */
60
    public static function loadFromFile(string $fileName): self
61
    {
62
        $source = @file_get_contents($fileName);
63
        if (false === $source) {
64
            throw new Exception("Failed to read lexer specification from file {$fileName}");
65
        }
66
67
        return new self($source);
68
    }
69
70
    /**
71
     * @return TokenMatcherSpec
72
     * @throws Exception
73
     * @throws ReflectionException
74
     */
75
    public function getMatcherSpec(): TokenMatcherSpec
76
    {
77
        if (!isset($this->matcherSpec)) {
78
            $this->matcherSpec = $this->buildMatcherSpec();
79
        }
80
81
        return $this->matcherSpec;
82
    }
83
84
    /**
85
     * @return TokenMatcherSpec
86
     * @throws Exception
87
     * @throws ReflectionException
88
     */
89
    private function buildMatcherSpec(): TokenMatcherSpec
90
    {
91
        $phpTokenList = token_get_all($this->source);
92
        foreach ($phpTokenList as $phpToken) {
93
            $argList = is_array($phpToken) ? $phpToken : [null, $phpToken];
94
            $this->processPhpToken(...$argList);
0 ignored issues
show
Bug introduced by
The call to Remorhaz\UniLex\Lexer\To...rser::processPhpToken() has too few arguments starting with code. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

94
            $this->/** @scrutinizer ignore-call */ 
95
                   processPhpToken(...$argList);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
95
        }
96
        $this->afterProcessingSource();
97
        if (!isset($this->targetClassName)) {
98
            throw new Exception("Invalid lexer specification: target class is not defined");
99
        }
100
        $targetClassName = $this->getCodeBlock(self::LEX_NAMESPACE) . $this->targetClassName;
101
        $templateClassName = $this->templateClassName ?? TokenMatcherTemplate::class;
102
        $matcherSpec = new TokenMatcherSpec($targetClassName, $templateClassName);
103
        $matcherSpec
104
            ->setHeader($this->getCodeBlock(self::TAG_LEX_HEADER))
105
            ->setBeforeMatch($this->getCodeBlock(self::TAG_LEX_BEFORE_MATCH))
106
            ->setOnTransition($this->getCodeBlock(self::TAG_LEX_ON_TRANSITION))
107
            ->setOnError($this->getCodeBlock(self::TAG_LEX_ON_ERROR, "return false;"));
108
        foreach ($this->tokenSpecList as $mode => $tokenSpecList) {
109
            $matcherSpec->addTokenSpec($mode, ...array_values($tokenSpecList));
110
        }
111
        foreach ($this->usedClassList as $usedClassAlias => $usedClassName) {
112
            $matcherSpec->addUsedClass($usedClassName, is_string($usedClassAlias) ? $usedClassAlias : null);
113
        }
114
115
        return $matcherSpec;
116
    }
117
118
    /**
119
     * @param int|null $tokenId
120
     * @param string   $code
121
     * @throws Exception
122
     */
123
    private function processPhpToken(?int $tokenId, string $code): void
124
    {
125
        $this->skipToken = false;
126
        if ($this->isCurrentCodeBlock(self::TAG_LEX_HEADER)) {
127
            if (T_NAMESPACE === $tokenId && !$this->codeBlockExists(self::LEX_NAMESPACE)) {
128
                $this->replaceCurrentCodeBlock(self::LEX_NAMESPACE);
129
                $this->skipToken = true;
130
            }
131
            if (T_USE === $tokenId) {
132
                $this->replaceCurrentCodeBlock(self::LEX_USE);
133
                $this->skipToken = true;
134
            }
135
        }
136
        if ($this->isCurrentCodeBlock(self::LEX_NAMESPACE) && null === $tokenId && ';' == $code) {
137
            $this->appendCodeBlock("\\");
138
            $this->restoreCurrentCodeBlock();
139
            $this->skipToken = true;
140
        }
141
        if ($this->isCurrentCodeBlock(self::LEX_USE) && null === $tokenId && ';' == $code) {
142
            [$usedClass, $alias] = $this->parseUsedClass($this->getCodeBlock(self::LEX_USE));
143
            if (isset($alias)) {
144
                $this->usedClassList[$alias] = $usedClass;
145
            } else {
146
                $this->usedClassList[] = $usedClass;
147
            }
148
            $this->resetCodeBlock();
149
            $this->restoreCurrentCodeBlock();
150
            $this->skipToken = true;
151
        }
152
        if (T_DOC_COMMENT === $tokenId) {
153
            $docBlock = $this->getDocBlockFactory()->create($code);
154
            $this->detectTargetClassName($docBlock);
155
            $this->detectTemplateClassName($docBlock);
156
            $this->detectCodeBlock($docBlock);
157
            $this->detectTokenRegExp($docBlock);
158
        }
159
        if (!$this->skipToken) {
160
            $this->appendCodeBlock($code);
161
        }
162
    }
163
164
    /**
165
     * @throws Exception
166
     */
167
    private function afterProcessingSource(): void
168
    {
169
        $this->detectUnprocessedTokenBlock();
170
    }
171
172
    /**
173
     * @param string $usedClass
174
     * @return array
175
     * @throws Exception
176
     */
177
    private function parseUsedClass(string $usedClass): array
178
    {
179
        /** @noinspection HtmlUnknownTag */
180
        $pattern = '#^(?P<className>\S+)(?:\s+(?:as\s+)?(?P<alias>\S+))?$#i';
181
        $pregResult = preg_match($pattern, $usedClass, $matches);
182
        if (1 !== $pregResult) {
183
            throw new Exception("Invalid lexer specification: invalid used class expression");
184
        }
185
186
        return [$matches['className'], $matches['alias'] ?? null];
187
    }
188
189
    /**
190
     * @param DocBlock $docBlock
191
     * @throws Exception
192
     */
193
    private function detectTargetClassName(DocBlock $docBlock): void
194
    {
195
        $tagName = self::TAG_LEX_TARGET_CLASS;
196
        if (!$docBlock->hasTag($tagName)) {
197
            return;
198
        }
199
        $this->skipToken = true;
200
        $tagList = $docBlock->getTagsByName($tagName);
201
        if (isset($this->targetClassName) || count($tagList) != 1) {
202
            throw new Exception("Invalid lexer specification: duplicated @{$tagName} tag");
203
        }
204
        $this->targetClassName = (string) array_pop($tagList);
205
    }
206
207
    /**
208
     * @param DocBlock $docBlock
209
     * @throws Exception
210
     */
211
    private function detectTemplateClassName(DocBlock $docBlock): void
212
    {
213
        $tagName = self::TAG_LEX_TEMPLATE_CLASS;
214
        if (!$docBlock->hasTag($tagName)) {
215
            return;
216
        }
217
        $this->skipToken = true;
218
        $tagList = $docBlock->getTagsByName($tagName);
219
        if (isset($this->templateClassName) || count($tagList) != 1) {
220
            throw new Exception("Invalid lexer specification: duplicated @{$tagName} tag");
221
        }
222
        $this->templateClassName = (string) array_pop($tagList);
223
    }
224
225
    /**
226
     * @param DocBlock $docBlock
227
     * @throws Exception
228
     */
229
    private function detectCodeBlock(DocBlock $docBlock): void
230
    {
231
        $codeBlockKeyList = [
232
            self::TAG_LEX_HEADER,
233
            self::TAG_LEX_BEFORE_MATCH,
234
            self::TAG_LEX_ON_TRANSITION,
235
            self::TAG_LEX_ON_ERROR,
236
            self::TAG_LEX_TOKEN,
237
        ];
238
        $codeBlockKey = null;
239
        foreach ($codeBlockKeyList as $tagName) {
240
            if ($docBlock->hasTag($tagName)) {
241
                if (isset($codeBlockKey)) {
242
                    throw new Exception("Invalid lexer specification: @{$tagName} tag conflicts with @{$codeBlockKey}");
243
                }
244
                $codeBlockKey = $tagName;
245
            }
246
        }
247
        if (isset($codeBlockKey)) {
248
            $this->detectUnprocessedTokenBlock();
249
            if ($this->codeBlockExists($codeBlockKey)) {
250
                throw new Exception("Invalid lexer specification: duplicated @{$codeBlockKey} tag");
251
            }
252
            $this->skipToken = true;
253
            $this->switchCurrentCodeBlock($codeBlockKey);
254
        }
255
    }
256
257
    /**
258
     * @throws Exception
259
     */
260
    private function detectUnprocessedTokenBlock(): void
261
    {
262
        $codeBlockKey = self::TAG_LEX_TOKEN;
263
        if (!$this->codeBlockExists($codeBlockKey)) {
264
            return;
265
        }
266
        $context = $this->getCodeBlock(self::LEX_TOKEN_CONTEXT);
267
        $this->resetCodeBlock(self::LEX_TOKEN_CONTEXT);
268
        $tokenRegExp = $this->getCodeBlock(self::LEX_TOKEN_REGEXP);
269
        if (isset($this->tokenSpecList[$context][$tokenRegExp])) {
270
            throw new Exception("Invalid lexer specification: duplicated @{$codeBlockKey} /{$tokenRegExp}/ tag");
271
        }
272
        $this->resetCodeBlock(self::LEX_TOKEN_REGEXP);
273
        $tokenCode = $this->getCodeBlock($codeBlockKey);
274
        $this->resetCodeBlock($codeBlockKey);
275
        $tokenSpec = new TokenSpec($tokenRegExp, $tokenCode);
276
        $this->tokenSpecList[$context][$tokenSpec->getRegExp()] = $tokenSpec;
277
    }
278
279
    /**
280
     * @param DocBlock $docBlock
281
     * @throws Exception
282
     */
283
    private function detectTokenRegExp(DocBlock $docBlock): void
284
    {
285
        if (!$this->isCurrentCodeBlock(self::TAG_LEX_TOKEN)) {
286
            return;
287
        }
288
        $tagValue = (string) $docBlock->getTagsByName(self::TAG_LEX_TOKEN)[0];
289
        $matchResult = preg_match('#^/(?P<regexp>.*)/$#', $tagValue, $matches);
290
        if (1 !== $matchResult) {
291
            throw new Exception("Invalid lexer specification: regular expression is not framed by \"/\"");
292
        }
293
        $regExp = $matches['regexp'];
294
        $this->replaceCurrentCodeBlock(self::LEX_TOKEN_REGEXP);
295
        $this->appendCodeBlock($regExp);
296
        $this->restoreCurrentCodeBlock();
297
        $context = $docBlock->hasTag(self::TAG_LEX_MODE)
298
            ? (string) $docBlock->getTagsByName(self::TAG_LEX_MODE)[0]
299
            : TokenMatcherInterface::DEFAULT_MODE;
300
        $matchResult = preg_match('#^[a-zA-Z][0-9a-zA-Z]*$#i', $context);
301
        if (1 !== $matchResult) {
302
            throw new Exception("Invalid lexer specification: invalid context name {$context}");
303
        }
304
        $this->replaceCurrentCodeBlock(self::LEX_TOKEN_CONTEXT);
305
        $this->appendCodeBlock($context);
306
        $this->restoreCurrentCodeBlock();
307
    }
308
309
    private function getDocBlockFactory(): DocBlockFactoryInterface
310
    {
311
        if (!isset($this->docBlockFactory)) {
312
            $this->docBlockFactory = DocBlockFactory::createInstance();
313
        }
314
315
        return $this->docBlockFactory;
316
    }
317
318
    private function switchCurrentCodeBlock(string $key): void
319
    {
320
        $this->codeBlockKey = $key;
321
        $this->codeBlockList[$this->codeBlockKey] = '';
322
    }
323
324
    private function replaceCurrentCodeBlock(string $key): void
325
    {
326
        $this->codeBlockStack[] = $this->codeBlockKey;
327
        $this->switchCurrentCodeBlock($key);
328
    }
329
330
    private function restoreCurrentCodeBlock(): void
331
    {
332
        $this->codeBlockKey = array_pop($this->codeBlockStack);
333
    }
334
335
    private function codeBlockExists(string $key): bool
336
    {
337
        return isset($this->codeBlockList[$key]);
338
    }
339
340
    private function getCodeBlock(string $key = null, string $defaultValue = ''): string
341
    {
342
        $effectiveKey = $key ?? $this->codeBlockKey;
343
344
        return isset($effectiveKey)
345
            ? trim($this->codeBlockList[$effectiveKey] ?? $defaultValue)
346
            : trim($defaultValue);
347
    }
348
349
    private function resetCodeBlock(string $key = null): void
350
    {
351
        $effectiveKey = $key ?? $this->codeBlockKey;
352
        if (isset($effectiveKey)) {
353
            unset($this->codeBlockList[$key]);
354
        }
355
    }
356
357
    private function appendCodeBlock(string $code, string $key = null): void
358
    {
359
        $effectiveKey = $key ?? $this->codeBlockKey;
360
        if (isset($effectiveKey)) {
361
            $this->codeBlockList[$effectiveKey] .= $code;
362
        }
363
    }
364
365
    private function isCurrentCodeBlock(string $key): bool
366
    {
367
        return $key === $this->codeBlockKey;
368
    }
369
}
370