Passed
Push — master ( 52423a...23b91d )
by Koen
03:18
created

TokenStream::isWhitespace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
nc 1
cc 1
eloc 2
nop 1
crap 1
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 39
    public function __construct(InputStream $input) {
26 39
        $this->input = $input;
27 39
    }
28
29
    /**
30
     * @return Token
31
     */
32 39
    public function next() {
33 39
        $token         = $this->current;
34 39
        $this->current = null;
35 39
        if ($token) {
36 23
            return $token;
37
        }
38 16
        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 23
    public function peek() {
52 23
        if ($this->current) {
53 23
            return $this->current;
54
        }
55 23
        $this->current = $this->readNext();
56 23
        return $this->current;
57
    }
58
59
    /**
60
     * @param string $msg
61
     *
62
     * @throws ParseException
63
     */
64 4
    public function error($msg) {
65 4
        $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 39
    protected function readNext() {
73 39
        $this->readWhile([$this, 'isWhitespace']);
74 39
        if ($this->input->eof()) {
75 19
            return null;
76
        }
77 39
        $char = $this->input->peek();
78 39
        if ($this->isComment()) {
79 5
            $this->skipComment();
80 5
            return $this->readNext();
81
        }
82 39
        if ($char == '"') {
83 13
            return $this->readDoubleQuotedString();
84
        }
85 34
        if ($char == '\'') {
86 1
            return $this->readSingleQuotedString();
87
        }
88 33
        if ($this->isDoubleBracketString()) {
89 12
            return $this->readDoubleBracketString();
90
        }
91 24
        if ($this->isDigit($char)) {
92 10
            return $this->readNumber();
93
        }
94 20
        if ($this->isStartIdentifierCharacter($char)) {
95 16
            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 13
    protected function readDoubleQuotedString() {
116 13
        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 12
    protected function readDoubleBracketString() {
130
        // we cannot use readEscaped because it only supports a single char as $end
131
        // and we do not support escaping in double bracke strings
132 12
        $str                      = "";
133 12
        $startNumberOfEqualsSigns = 0;
134
        // skip both
135 12
        $this->input->next();
136 12
        while ($this->input->peek() == '=') {
137 7
            $startNumberOfEqualsSigns++;
138 7
            $this->input->next();
139 7
        }
140 12
        if ($this->input->peek() != '[') {
141 1
            $this->error('Unexpected character \'' . $this->input->peek() . '\', expected \'[\'');
142
        }
143 11
        $this->input->next();
144 11
        while (!$this->input->eof()) {
145 11
            $char = $this->input->next();
146 11
            if ($char == ']') { // we might have reached the end
147 11
                if ($startNumberOfEqualsSigns != 0) {
148 6
                    if ($this->input->peek() == '=') {
149 5
                        $endNumberOfEqualsSigns = 0;
150 5
                        while ($this->input->peek() == '=') {
151 5
                            $endNumberOfEqualsSigns++;
152 5
                            $this->input->next();
153 5
                        }
154
155
                        // we have an equal number of equal signs
156 5
                        if ($endNumberOfEqualsSigns == $startNumberOfEqualsSigns) {
157 5
                            if ($this->input->peek() != ']') {
158 1
                                $this->error('Unexpected character \'' . $this->input->peek() . '\', expected \'[\'');
159
                            }
160 4
                            $this->input->next();
161 4
                            break;
162
                        } else {
163 1
                            $str .= $char . str_repeat('=', $endNumberOfEqualsSigns);
164
                        }
165 1
                    } else {
166 3
                        $str .= $char;
167
                    }
168 3
                } else {
169 5
                    if ($this->input->peek() == ']') {
170 5
                        $this->input->next();
171 5
                        break;
172
                    }
173
                }
174 3
            } else {
175 11
                $str .= $char;
176
            }
177 11
        }
178 9
        return new Token(Token::TYPE_STRING, $str);
179
    }
180
181
    /**
182
     * @param string $end
183
     *
184
     * @return string
185
     */
186 14
    protected function readEscaped($end) {
187 14
        $escaped = false;
188 14
        $str     = "";
189 14
        $this->input->next();
190 14
        while (!$this->input->eof()) {
191 14
            $char = $this->input->next();
192 14
            if ($escaped) {
193
                switch ($char) {
194 1
                    case 'n':
195 1
                        $str .= "\n";
196 1
                        break;
197 1
                    case 'r':
198 1
                        $str .= "\r";
199 1
                        break;
200 1
                    case 't':
201 1
                        $str .= "\t";
202 1
                        break;
203 1
                    case 'v':
204 1
                        $str .= "\v";
205 1
                        break;
206 1
                    default:
207 1
                        $str .= $char;
208 1
                        break;
209 1
                }
210 1
                $escaped = false;
211 1
            } else {
212 14
                if ($char == "\\") {
213 1
                    $escaped = true;
214 1
                } else {
215 14
                    if ($char == $end) {
216 14
                        break;
217
                    } else {
218 14
                        $str .= $char;
219
                    }
220
                }
221
            }
222 14
        }
223 14
        return $str;
224
    }
225
226
    /**
227
     * @return Token
228
     */
229 10
    protected function readNumber() {
230 10
        $hasDot = false;
231 10
        $number = $this->readWhile(
232
            function ($char) use (&$hasDot) {
233 10
                if ($char == '.') {
234 1
                    if ($hasDot) {
235
                        return false;
236
                    }
237 1
                    $hasDot = true;
238 1
                    return true;
239
                }
240 10
                return $this->isDigit($char);
241
            }
242 10
        );
243 10
        return new Token(Token::TYPE_NUMBER, $hasDot ? floatval($number) : intval($number));
244
    }
245
246
    /**
247
     * @return Token
248
     */
249 16
    protected function readIdentifier() {
250 16
        $first      = false;
251 16
        $identifier = $this->readWhile(
252 16
            function ($char) use (&$first) {
253 16
                if ($first) {
254
                    $first = false;
255
                    return $this->isStartIdentifierCharacter($char);
256
                }
257 16
                return $this->isIdentifierCharacter($char);
258
            }
259 16
        );
260 16
        if ($this->isKeyword($identifier)) {
261 6
            return new Token(Token::TYPE_KEYWORD, $identifier);
262
        }
263 12
        return new Token(Token::TYPE_IDENTIFIER, $identifier);
264
    }
265
266
    /**
267
     * @return Token
268
     */
269 14
    protected function readPunctuation() {
270 14
        return new Token(Token::TYPE_PUNCTUATION, $this->input->next());
271
    }
272
273
    /**
274
     * @param callable $predicate
275
     *
276
     * @return string
277
     */
278 39
    protected function readWhile(callable $predicate) {
279 39
        $str = "";
280 39
        while (!$this->input->eof() && call_user_func($predicate, $this->input->peek())) {
281 20
            $str .= $this->input->next();
282 20
        }
283 39
        return $str;
284
    }
285
286
    /**
287
     * @param string $char
288
     *
289
     * @return bool
290
     */
291 39
    protected function isWhitespace($char) {
292 39
        return strpos(" \t\n\r", $char) !== false;
293
    }
294
295
    /**
296
     * @param string $char
297
     *
298
     * @return bool
299
     */
300 24
    protected function isDigit($char) {
301 24
        return is_numeric($char);
302
    }
303
304
    /**
305
     * @return bool
306
     */
307 33
    protected function isDoubleBracketString() {
308 33
        return $this->input->peek() == '[' && !$this->input->eof(1) && ($this->input->peek(1) == '[' || $this->input->peek(1) == '=');
309
    }
310
311
    /**
312
     * @return bool
313
     */
314 39
    protected function isComment() {
315 39
        return $this->input->peek() == '-' && !$this->input->eof(1) && $this->input->peek(1) == '-';
316
    }
317
318
    /**
319
     * @param string $char
320
     *
321
     * @return bool
322
     */
323 20
    protected function isStartIdentifierCharacter($char) {
324 20
        return preg_match('/[a-zA-Z_]/', $char) === 1;
325
    }
326
327
    /**
328
     * @param string $char
329
     *
330
     * @return bool
331
     */
332 16
    protected function isIdentifierCharacter($char) {
333 16
        return preg_match('/[a-zA-Z0-9_]/', $char) === 1;
334
    }
335
336
    /**
337
     * @param string $char
338
     *
339
     * @return bool
340
     */
341 15
    protected function isPunctuation($char) {
342 15
        return strpos(",{}=[]", $char) !== false;
343
    }
344
345
    /**
346
     * @param string $text
347
     *
348
     * @return bool
349
     */
350 16
    protected function isKeyword($text) {
351 16
        return in_array($text, Lua::$luaKeywords);
352
    }
353
}