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