Passed
Push — master ( 23b91d...70495b )
by Koen
09:19
created

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