Passed
Push — master ( 865ea9...f5a1ef )
by butschster
29:08 queued 21:20
created

DirectiveGrammar   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Test Coverage

Coverage 93.33%

Importance

Changes 0
Metric Value
wmc 41
eloc 102
dl 0
loc 242
ccs 98
cts 105
cp 0.9333
rs 9.1199
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
B parse() 0 54 9
A getLastToken() 0 7 2
B parseBody() 0 44 10
A flushBody() 0 8 2
A getKeyword() 0 9 3
A getLastOffset() 0 3 1
A flushName() 0 8 2
A getBody() 0 9 3
B finalize() 0 31 7
A getIterator() 0 7 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Stempler\Lexer\Grammar\Dynamic;
6
7
use Spiral\Stempler\Exception\SyntaxException;
8
use Spiral\Stempler\Lexer\Buffer;
9
use Spiral\Stempler\Lexer\Byte;
10
use Spiral\Stempler\Lexer\Grammar\DynamicGrammar;
11
use Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait;
12
use Spiral\Stempler\Lexer\Token;
13
14
/**
15
 * @implements \IteratorAggregate<int, Token>
16
 */
17
final class DirectiveGrammar implements \IteratorAggregate
18
{
19
    use TokenTrait;
0 ignored issues
show
introduced by
The trait Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait requires some properties which are not provided by Spiral\Stempler\Lexer\Gr...ynamic\DirectiveGrammar: $char, $content, $type
Loading history...
20
21
    // start directive
22
    public const DIRECTIVE_CHAR = '@';
23
24
    // whitespace
25
    private const REGEXP_WHITESPACE = '/\\s/';
26
27
    // Allowed keyword characters.
28
    private const REGEXP_KEYWORD = '/[a-z0-9_\\-:\\.]/ui';
29
30
    private array $name = [];
31
    private ?array $body = [];
32
33 51
    public function parse(Buffer $src, int $offset): bool
34
    {
35 51
        $this->tokens = [
36 51
            new Token(DynamicGrammar::TYPE_DIRECTIVE, $offset, self::DIRECTIVE_CHAR),
37 51
        ];
38
39 51
        $this->body = null;
40 51
        $hasWhitespace = false;
41
42 51
        while ($n = $src->next()) {
43 51
            if (!$n instanceof Byte) {
44
                // no other grammars are allowed
45 2
                break;
46
            }
47
48 51
            switch ($n->char) {
49 51
                case '(':
50 41
                    $this->flushName();
51 41
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_OPEN, $n->offset, $n->char);
52
53 41
                    return $this->parseBody($src);
54
                default:
55 51
                    if (\preg_match(self::REGEXP_WHITESPACE, $n->char)) {
56 15
                        $hasWhitespace = true;
57 15
                        if ($this->name !== []) {
58 15
                            $this->flushName();
59 15
                            $this->tokens[] = new Token(DynamicGrammar::TYPE_WHITESPACE, $n->offset, $n->char);
60 15
                            break;
61
                        }
62
63 1
                        if ($this->getLastToken()->type === DynamicGrammar::TYPE_WHITESPACE) {
64 1
                            $this->getLastToken()->content .= $n->char;
65 1
                            break;
66
                        }
67
68
                        // invalid directive
69
                        return false;
70 51
                    } elseif ($hasWhitespace) {
71 12
                        return $this->finalize();
72
                    }
73
74 51
                    if (!\preg_match(self::REGEXP_KEYWORD, $n->char)) {
75 2
                        $this->flushName();
76
77 2
                        return $this->finalize();
78
                    }
79
80 51
                    $this->name[] = $n;
81
            }
82
        }
83
84 26
        $this->flushName();
85
86 26
        return $this->finalize();
87
    }
88
89
    /**
90
     * Directive tokens.
91
     *
92
     * @return \Generator<int, Token>
93
     */
94 42
    public function getIterator(): \Generator
95
    {
96 42
        if ($this->tokens === []) {
97
            throw new \LogicException('Directive not parsed');
98
        }
99
100 42
        yield from $this->tokens;
101
    }
102
103
    /**
104
     * Return offset after last directive token.
105
     */
106 50
    public function getLastOffset(): int
107
    {
108 50
        return $this->getLastToken()->offset + \strlen($this->getLastToken()->content) - 1;
109
    }
110
111
    /**
112
     * Get directive keyword.
113
     */
114 50
    public function getKeyword(): string
115
    {
116 50
        foreach ($this->tokens as $token) {
117 50
            if ($token->type === DynamicGrammar::TYPE_KEYWORD) {
118 50
                return $token->content;
119
            }
120
        }
121
122
        throw new SyntaxException('Directive not parsed', $this->tokens[0]);
123
    }
124
125
    /**
126
     * Get directive body.
127
     */
128 8
    public function getBody(): ?string
129
    {
130 8
        foreach ($this->tokens as $token) {
131 8
            if ($token->type === DynamicGrammar::TYPE_BODY) {
132 6
                return $token->content;
133
            }
134
        }
135
136 2
        return null;
137
    }
138
139
    /**
140
     * Pack keyword token.
141
     */
142 51
    private function flushName(): void
143
    {
144 51
        if ($this->name === []) {
145 3
            return;
146
        }
147
148 51
        $this->tokens[] = $this->packToken($this->name, DynamicGrammar::TYPE_KEYWORD);
149 51
        $this->name = [];
150
    }
151
152
    /**
153
     * TODO issue #767
154
     * @link https://github.com/spiral/framework/issues/767
155
     * @psalm-suppress UndefinedPropertyFetch
156
     */
157 41
    private function parseBody(Buffer $src): bool
158
    {
159 41
        $this->body = [];
160 41
        $level = 1;
161
162 41
        while ($nn = $src->next()) {
163 41
            if (!$nn instanceof Byte) {
164
                $this->flushBody();
165
                return $this->finalize();
166
            }
167
168 41
            if (\in_array($nn->char, ['"', '"'])) {
169 9
                $this->body[] = $nn;
170 9
                while ($nnn = $src->next()) {
171 9
                    $this->body[] = $nnn;
172 9
                    if ($nnn instanceof Byte && $nnn->char === $nn->char) {
173 9
                        break;
174
                    }
175
                }
176 9
                continue;
177
            }
178
179 41
            $this->body[] = $nn;
180
181 41
            if ($nn->char === '(') {
182 6
                $level++;
183 6
                continue;
184
            }
185
186 41
            if ($nn->char === ')') {
187 40
                $level--;
188
189 40
                if ($level === 0) {
190 40
                    $n = \array_pop($this->body);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

190
                    $n = \array_pop(/** @scrutinizer ignore-type */ $this->body);
Loading history...
191
192 40
                    $this->flushBody();
193 40
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_CLOSE, $n->offset, $n->char);
194
195 40
                    return $this->finalize();
196
                }
197
            }
198
        }
199
200 1
        return $this->finalize();
201
    }
202
203
    /**
204
     * Pack name token.
205
     */
206 40
    private function flushBody(): void
207
    {
208 40
        if ($this->body === []) {
209
            return;
210
        }
211
212 40
        $this->tokens[] = $this->packToken($this->body, DynamicGrammar::TYPE_BODY);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $inner of Spiral\Stempler\Lexer\Gr...iveGrammar::packToken() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

212
        $this->tokens[] = $this->packToken(/** @scrutinizer ignore-type */ $this->body, DynamicGrammar::TYPE_BODY);
Loading history...
213 40
        $this->body = [];
214
    }
215
216 50
    private function getLastToken(): Token
217
    {
218 50
        if ($this->tokens === []) {
219
            throw new \LogicException('Directive not parsed');
220
        }
221
222 50
        return $this->tokens[\count($this->tokens) - 1];
223
    }
224
225
    /**
226
     * Flush directive and seek buffer before last non WHITESPACE token.
227
     */
228 51
    private function finalize(): bool
229
    {
230 51
        $tokens = $this->tokens;
231
232 51
        foreach (\array_reverse($tokens, true) as $i => $t) {
233 51
            if ($t->type !== DynamicGrammar::TYPE_WHITESPACE) {
234 51
                break;
235
            }
236
237 13
            unset($tokens[$i]);
238
        }
239
240 51
        $body = null;
241 51
        foreach ($tokens as $t) {
242 51
            if ($t->type === DynamicGrammar::TYPE_BODY_OPEN) {
243 41
                $body = false;
244 41
                continue;
245
            }
246
247 51
            if ($t->type === DynamicGrammar::TYPE_BODY_CLOSE) {
248 40
                $body = null;
249
            }
250
        }
251
252 51
        if ($body !== null) {
253 1
            return false;
254
        }
255
256 50
        $this->tokens = $tokens;
257
258 50
        return true;
259
    }
260
}
261