Passed
Push — master ( 1a0a87...a70f7d )
by Koen
05:46 queued 03:06
created

TokenStream::isComment()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 3
eloc 2
nc 3
nop 0
crap 3
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 27
    public function __construct(InputStream $input) {
26 27
        $this->input = $input;
27 27
    }
28
29
    /**
30
     * @return Token
31
     */
32 27
    public function next() {
33 27
        $token         = $this->current;
34 27
        $this->current = null;
35 27
        if ($token) {
36 19
            return $token;
37
        }
38 8
        return $this->readNext();
39
    }
40
41
    /**
42
     * @return bool
43
     */
44 9
    public function eof() {
45 9
        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 19
    public function peek() {
52 19
        if ($this->current) {
53 19
            return $this->current;
54
        }
55 19
        $this->current = $this->readNext();
56 19
        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 27
    protected function readNext() {
73 27
        $this->readWhile([$this, 'isWhitespace']);
74 27
        if ($this->input->eof()) {
75
            return null;
76
        }
77 27
        $char = $this->input->peek();
78 27
        if ($this->isComment()) {
79 3
            $this->skipComment();
80 3
            return $this->readNext();
81
        }
82 27
        if ($char == '"') {
83 10
            return $this->readDoubleQuotedString();
84
        }
85 23
        if ($char == '\'') {
86 1
            return $this->readSingleQuotedString();
87
        }
88 22
        if ($this->isDoubleBracketString()) {
89 2
            return $this->readDoubleBracketString();
90
        }
91 20
        if ($this->isDigit($char)) {
92 8
            return $this->readNumber();
93
        }
94 16
        if ($this->isStartIdentifierCharacter($char)) {
95 12
            return $this->readIdentifier();
96
        }
97 12
        if ($this->isPunctuation($char)) {
98 11
            return $this->readPunctuation();
99
        }
100 1
        $this->input->error('Cannot handle character: ' . $char . ' (ord: ' . ord($char) . ')');
101
    }
102
103 3
    protected function skipComment() {
104 3
        $this->readWhile(
105
            function ($char) {
106 3
                return $char != "\n";
107
            }
108 3
        );
109 3
        $this->input->next();
110 3
    }
111
112
    /**
113
     * @return Token
114
     */
115 10
    protected function readDoubleQuotedString() {
116 10
        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 2
    protected function readDoubleBracketString() {
130
        // we cannot use readEscaped because it only supports a single char as $end
131 2
        $escaped = false;
132 2
        $str     = "";
133
        // skip both
134 2
        $this->input->next();
135 2
        $this->input->next();
136 2
        while (!$this->input->eof()) {
137 2
            $char = $this->input->next();
138 2
            if ($escaped) {
139
                $str .= $char;
140
                $escaped = false;
141
            } else {
142 2
                if ($char == "\\") {
143
                    $escaped = true;
144
                } else {
145 2
                    if ($char == ']' && $this->input->peek() == ']') { // we reached the end
146 2
                        break;
147
                    } else {
148 2
                        $str .= $char;
149
                    }
150
                }
151
            }
152 2
        }
153 2
        return new Token(Token::TYPE_STRING, $str);
154
    }
155
156
    /**
157
     * @param string $end
158
     *
159
     * @return string
160
     */
161 11
    protected function readEscaped($end) {
162 11
        $escaped = false;
163 11
        $str     = "";
164 11
        $this->input->next();
165 11
        while (!$this->input->eof()) {
166 11
            $char = $this->input->next();
167 11
            if ($escaped) {
168
                $str .= $char;
169
                $escaped = false;
170
            } else {
171 11
                if ($char == "\\") {
172
                    $escaped = true;
173
                } else {
174 11
                    if ($char == $end) {
175 11
                        break;
176
                    } else {
177 11
                        $str .= $char;
178
                    }
179
                }
180
            }
181 11
        }
182 11
        return $str;
183
    }
184
185
    /**
186
     * @return Token
187
     */
188 8
    protected function readNumber() {
189 8
        $hasDot = false;
190 8
        $number = $this->readWhile(
191
            function ($char) use (&$hasDot) {
192 8
                if ($char == '.') {
193 1
                    if ($hasDot) {
194
                        return false;
195
                    }
196 1
                    $hasDot = true;
197 1
                    return true;
198
                }
199 8
                return $this->isDigit($char);
200
            }
201 8
        );
202 8
        return new Token(Token::TYPE_NUMBER, $hasDot ? floatval($number) : intval($number));
203
    }
204
205
    /**
206
     * @return Token
207
     */
208 12
    protected function readIdentifier() {
209 12
        $first      = false;
210 12
        $identifier = $this->readWhile(
211 12
            function ($char) use (&$first) {
212 12
                if ($first) {
213
                    $first = false;
214
                    return $this->isStartIdentifierCharacter($char);
215
                }
216 12
                return $this->isIdentifierCharacter($char);
217
            }
218 12
        );
219 12
        if ($this->isKeyword($identifier)) {
220 3
            return new Token(Token::TYPE_KEYWORD, $identifier);
221
        }
222 9
        return new Token(Token::TYPE_IDENTIFIER, $identifier);
223
    }
224
225
    /**
226
     * @return Token
227
     */
228 11
    protected function readPunctuation() {
229 11
        return new Token(Token::TYPE_PUNCTUATION, $this->input->next());
230
    }
231
232
    /**
233
     * @param callable $predicate
234
     *
235
     * @return string
236
     */
237 27
    protected function readWhile(callable $predicate) {
238 27
        $str = "";
239 27
        while (!$this->input->eof() && call_user_func($predicate, $this->input->peek())) {
240 16
            $str .= $this->input->next();
241 16
        }
242 27
        return $str;
243
    }
244
245
    /**
246
     * @param string $char
247
     *
248
     * @return bool
249
     */
250 27
    protected function isWhitespace($char) {
251 27
        return strpos(" \t\n\r", $char) !== false;
252
    }
253
254
    /**
255
     * @param string $char
256
     *
257
     * @return bool
258
     */
259 20
    protected function isDigit($char) {
260 20
        return is_numeric($char);
261
    }
262
263
    /**
264
     * @return bool
265
     */
266 22
    protected function isDoubleBracketString() {
267 22
        return $this->input->peek() == '[' && !$this->input->eof(1) && $this->input->peek(1) == '[';
268
    }
269
270
    /**
271
     * @return bool
272
     */
273 27
    protected function isComment() {
274 27
        return $this->input->peek() == '-' && !$this->input->eof(1) && $this->input->peek(1) == '-';
275
    }
276
277
    /**
278
     * @param string $char
279
     *
280
     * @return bool
281
     */
282 16
    protected function isStartIdentifierCharacter($char) {
283 16
        return preg_match('/[a-zA-Z_]/', $char) === 1;
284
    }
285
286
    /**
287
     * @param string $char
288
     *
289
     * @return bool
290
     */
291 12
    protected function isIdentifierCharacter($char) {
292 12
        return preg_match('/[a-zA-Z0-9_]/', $char) === 1;
293
    }
294
295
    /**
296
     * @param string $char
297
     *
298
     * @return bool
299
     */
300 12
    protected function isPunctuation($char) {
301 12
        return strpos(",{}=[]", $char) !== false;
302
    }
303
304
    /**
305
     * @param string $text
306
     *
307
     * @return bool
308
     */
309 12
    protected function isKeyword($text) {
310 12
        return in_array($text, Lua::$luaKeywords);
311
    }
312
}