Passed
Push — master ( b8c5dd...52423a )
by Koen
12:09
created

TokenStream   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 88.34%

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 3
dl 0
loc 334
c 0
b 0
f 0
ccs 144
cts 163
cp 0.8834
rs 6.018

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A next() 0 8 2
A eof() 0 3 1
A peek() 0 7 2
A error() 0 3 1
D readNext() 0 30 9
A skipComment() 0 8 1
A readDoubleQuotedString() 0 3 1
A readSingleQuotedString() 0 3 1
C readDoubleBracketString() 0 60 13
B readEscaped() 0 23 5
A readNumber() 0 16 4
A readIdentifier() 0 16 3
A readPunctuation() 0 3 1
A readWhile() 0 7 3
A isWhitespace() 0 3 1
A isDigit() 0 3 1
A isDoubleBracketString() 0 3 4
A isComment() 0 3 3
A isStartIdentifierCharacter() 0 3 1
A isIdentifierCharacter() 0 3 1
A isPunctuation() 0 3 1
A isKeyword() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TokenStream 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 TokenStream, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Vlaswinkel\Lua;
4
5
/**
6
 * Class TokenStream
7
 *
8
 * @see     http://lisperator.net/pltut/parser/token-stream
9
 *
10
 * @author  Koen Vlaswinkel <[email protected]>
11
 * @package Vlaswinkel\Lua
12
 */
13
class TokenStream {
14
    private $current = null;
15
    /**
16
     * @var InputStream
17
     */
18
    private $input;
19
20
    /**
21
     * TokenStream constructor.
22
     *
23
     * @param InputStream $input
24
     */
25 35
    public function __construct(InputStream $input) {
26 35
        $this->input = $input;
27 35
    }
28
29
    /**
30
     * @return Token
31
     */
32 35
    public function next() {
33 35
        $token         = $this->current;
34 35
        $this->current = null;
35 35
        if ($token) {
36 22
            return $token;
37
        }
38 13
        return $this->readNext();
39
    }
40
41
    /**
42
     * @return bool
43
     */
44 22
    public function eof() {
45 22
        return $this->peek() == null;
46
    }
47
48
    /**
49
     * @return Token
0 ignored issues
show
Documentation introduced by
Should the return type not be Token|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
50
     */
51 22
    public function peek() {
52 22
        if ($this->current) {
53 22
            return $this->current;
54
        }
55 22
        $this->current = $this->readNext();
56 22
        return $this->current;
57
    }
58
59
    /**
60
     * @param string $msg
61
     *
62
     * @throws ParseException
63
     */
64 1
    public function error($msg) {
65 1
        $this->input->error($msg);
66
    }
67
68
    /**
69
     * @return Token
0 ignored issues
show
Documentation introduced by
Should the return type not be Token|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
70
     * @throws ParseException
71
     */
72 35
    protected function readNext() {
73 35
        $this->readWhile([$this, 'isWhitespace']);
74 35
        if ($this->input->eof()) {
75 19
            return null;
76
        }
77 35
        $char = $this->input->peek();
78 35
        if ($this->isComment()) {
79 5
            $this->skipComment();
80 5
            return $this->readNext();
81
        }
82 35
        if ($char == '"') {
83 12
            return $this->readDoubleQuotedString();
84
        }
85 31
        if ($char == '\'') {
86 1
            return $this->readSingleQuotedString();
87
        }
88 30
        if ($this->isDoubleBracketString()) {
89 10
            return $this->readDoubleBracketString();
90
        }
91 23
        if ($this->isDigit($char)) {
92 10
            return $this->readNumber();
93
        }
94 19
        if ($this->isStartIdentifierCharacter($char)) {
95 15
            return $this->readIdentifier();
96
        }
97 15
        if ($this->isPunctuation($char)) {
98 14
            return $this->readPunctuation();
99
        }
100 1
        $this->input->error('Cannot handle character: ' . $char . ' (ord: ' . ord($char) . ')');
101
    }
102
103 5
    protected function skipComment() {
104 5
        $this->readWhile(
105
            function ($char) {
106 5
                return $char != "\n";
107
            }
108 5
        );
109 5
        $this->input->next();
110 5
    }
111
112
    /**
113
     * @return Token
114
     */
115 12
    protected function readDoubleQuotedString() {
116 12
        return new Token(Token::TYPE_STRING, $this->readEscaped('"'));
117
    }
118
119
    /**
120
     * @return Token
121
     */
122 1
    protected function readSingleQuotedString() {
123 1
        return new Token(Token::TYPE_STRING, $this->readEscaped('\''));
124
    }
125
126
    /**
127
     * @return Token
128
     */
129 10
    protected function readDoubleBracketString() {
130
        // we cannot use readEscaped because it only supports a single char as $end
131 10
        $escaped                  = false;
132 10
        $str                      = "";
133 10
        $startNumberOfEqualsSigns = 0;
134
        // skip both
135 10
        $this->input->next();
136 10
        while ($this->input->peek() == '=') {
137 5
            $startNumberOfEqualsSigns++;
138 5
            $this->input->next();
139 5
        }
140 10
        if ($this->input->peek() != '[') {
141
            $this->error('Unexpected character ' . $this->input->peek() . ', expected [');
142
        }
143 10
        $this->input->next();
144 10
        while (!$this->input->eof()) {
145 10
            $char = $this->input->next();
146 10
            if ($escaped) {
147
                $str .= $char;
148
                $escaped = false;
149
            } else {
150 10
                if ($char == "\\") {
151
                    $escaped = true;
152
                } else {
153 10
                    if ($char == ']') { // we might have reached the end
154 10
                        if ($startNumberOfEqualsSigns != 0) {
155 5
                            if ($this->input->peek() == '=') {
156 4
                                $endNumberOfEqualsSigns = 0;
157 4
                                while ($this->input->peek() == '=') {
158 4
                                    $endNumberOfEqualsSigns++;
159 4
                                    $this->input->next();
160 4
                                }
161
162
                                // we have an equal number of equal signs
163 4
                                if ($endNumberOfEqualsSigns == $startNumberOfEqualsSigns) {
164 4
                                    if ($this->input->peek() != ']') {
165
                                        $this->error('Unexpected character ' . $this->input->peek() . ', expected [');
166
                                    }
167 4
                                    $this->input->next();
168 4
                                    break;
169
                                } else {
170 1
                                    $str .= $char . str_repeat('=', $endNumberOfEqualsSigns);
171
                                }
172 1
                            } else {
173 3
                                $str .= $char;
174
                            }
175 3
                        } else {
176 5
                            if ($this->input->peek() == ']') {
177 5
                                $this->input->next();
178 5
                                break;
179
                            }
180
                        }
181 3
                    } else {
182 10
                        $str .= $char;
183
                    }
184
                }
185
            }
186 10
        }
187 9
        return new Token(Token::TYPE_STRING, $str);
188
    }
189
190
    /**
191
     * @param string $end
192
     *
193
     * @return string
194
     */
195 13
    protected function readEscaped($end) {
196 13
        $escaped = false;
197 13
        $str     = "";
198 13
        $this->input->next();
199 13
        while (!$this->input->eof()) {
200 13
            $char = $this->input->next();
201 13
            if ($escaped) {
202
                $str .= $char;
203
                $escaped = false;
204
            } else {
205 13
                if ($char == "\\") {
206
                    $escaped = true;
207
                } else {
208 13
                    if ($char == $end) {
209 13
                        break;
210
                    } else {
211 13
                        $str .= $char;
212
                    }
213
                }
214
            }
215 13
        }
216 13
        return $str;
217
    }
218
219
    /**
220
     * @return Token
221
     */
222 10
    protected function readNumber() {
223 10
        $hasDot = false;
224 10
        $number = $this->readWhile(
225
            function ($char) use (&$hasDot) {
226 10
                if ($char == '.') {
227 1
                    if ($hasDot) {
228
                        return false;
229
                    }
230 1
                    $hasDot = true;
231 1
                    return true;
232
                }
233 10
                return $this->isDigit($char);
234
            }
235 10
        );
236 10
        return new Token(Token::TYPE_NUMBER, $hasDot ? floatval($number) : intval($number));
237
    }
238
239
    /**
240
     * @return Token
241
     */
242 15
    protected function readIdentifier() {
243 15
        $first      = false;
244 15
        $identifier = $this->readWhile(
245 15
            function ($char) use (&$first) {
246 15
                if ($first) {
247
                    $first = false;
248
                    return $this->isStartIdentifierCharacter($char);
249
                }
250 15
                return $this->isIdentifierCharacter($char);
251
            }
252 15
        );
253 15
        if ($this->isKeyword($identifier)) {
254 5
            return new Token(Token::TYPE_KEYWORD, $identifier);
255
        }
256 12
        return new Token(Token::TYPE_IDENTIFIER, $identifier);
257
    }
258
259
    /**
260
     * @return Token
261
     */
262 14
    protected function readPunctuation() {
263 14
        return new Token(Token::TYPE_PUNCTUATION, $this->input->next());
264
    }
265
266
    /**
267
     * @param callable $predicate
268
     *
269
     * @return string
270
     */
271 35
    protected function readWhile(callable $predicate) {
272 35
        $str = "";
273 35
        while (!$this->input->eof() && call_user_func($predicate, $this->input->peek())) {
274 19
            $str .= $this->input->next();
275 19
        }
276 35
        return $str;
277
    }
278
279
    /**
280
     * @param string $char
281
     *
282
     * @return bool
283
     */
284 35
    protected function isWhitespace($char) {
285 35
        return strpos(" \t\n\r", $char) !== false;
286
    }
287
288
    /**
289
     * @param string $char
290
     *
291
     * @return bool
292
     */
293 23
    protected function isDigit($char) {
294 23
        return is_numeric($char);
295
    }
296
297
    /**
298
     * @return bool
299
     */
300 30
    protected function isDoubleBracketString() {
301 30
        return $this->input->peek() == '[' && !$this->input->eof(1) && ($this->input->peek(1) == '[' || $this->input->peek(1) == '=');
302
    }
303
304
    /**
305
     * @return bool
306
     */
307 35
    protected function isComment() {
308 35
        return $this->input->peek() == '-' && !$this->input->eof(1) && $this->input->peek(1) == '-';
309
    }
310
311
    /**
312
     * @param string $char
313
     *
314
     * @return bool
315
     */
316 19
    protected function isStartIdentifierCharacter($char) {
317 19
        return preg_match('/[a-zA-Z_]/', $char) === 1;
318
    }
319
320
    /**
321
     * @param string $char
322
     *
323
     * @return bool
324
     */
325 15
    protected function isIdentifierCharacter($char) {
326 15
        return preg_match('/[a-zA-Z0-9_]/', $char) === 1;
327
    }
328
329
    /**
330
     * @param string $char
331
     *
332
     * @return bool
333
     */
334 15
    protected function isPunctuation($char) {
335 15
        return strpos(",{}=[]", $char) !== false;
336
    }
337
338
    /**
339
     * @param string $text
340
     *
341
     * @return bool
342
     */
343 15
    protected function isKeyword($text) {
344 15
        return in_array($text, Lua::$luaKeywords);
345
    }
346
}