Passed
Pull Request — master (#806)
by Maxim
19:17
created

DirectiveGrammar::parse()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 9.0027

Importance

Changes 0
Metric Value
cc 9
eloc 31
nc 6
nop 2
dl 0
loc 54
ccs 30
cts 31
cp 0.9677
crap 9.0027
rs 8.0555
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

187
                    $n = \array_pop(/** @scrutinizer ignore-type */ $this->body);
Loading history...
188
189 40
                    $this->flushBody();
190 40
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_CLOSE, $n->offset, $n->char);
191
192 40
                    return $this->finalize();
193
                }
194
            }
195
        }
196
197 1
        return $this->finalize();
198
    }
199
200
    /**
201
     * Pack name token.
202
     */
203 40
    private function flushBody(): void
204
    {
205 40
        if ($this->body === []) {
206
            return;
207
        }
208
209 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

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