Passed
Push — master ( 0f10a6...8f1e95 )
by Koen
11:27
created

TokenStream::readNext()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8.512

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
ccs 16
cts 20
cp 0.8
rs 5.3846
cc 8
eloc 19
nc 8
nop 0
crap 8.512
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 21
    public function __construct(InputStream $input) {
26 21
        $this->input = $input;
27 21
    }
28
29
    /**
30
     * @return Token
31
     */
32 21
    public function next() {
33 21
        $token         = $this->current;
34 21
        $this->current = null;
35 21
        if ($token) {
36 13
            return $token;
37
        }
38 8
        return $this->readNext();
39
    }
40
41
    /**
42
     * @return bool
43
     */
44 6
    public function eof() {
45 6
        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 13
    public function peek() {
52 13
        if ($this->current) {
53 13
            return $this->current;
54
        }
55 13
        $this->current = $this->readNext();
56 13
        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 21
    protected function readNext() {
73 21
        $this->readWhile([$this, 'isWhitespace']);
74 21
        if ($this->input->eof()) {
75
            return null;
76
        }
77 21
        $char = $this->input->peek();
78 21
        if ($char == "--") {
79
            $this->skipComment();
80
            return $this->readNext();
81
        }
82 21
        if ($char == '"') {
83 8
            return $this->readDoubleQuotedString();
84
        }
85 18
        if ($char == '\'') {
86 1
            return $this->readSingleQuotedString();
87
        }
88 17
        if ($this->isDigit($char)) {
89 6
            return $this->readNumber();
90
        }
91 13
        if ($this->isStartIdentifierCharacter($char)) {
92 9
            return $this->readIdentifier();
93
        }
94 9
        if ($this->isPunctuation($char)) {
95 8
            return $this->readPunctuation();
96
        }
97 1
        $this->input->error('Cannot handle character: ' . $char . ' (ord: ' . ord($char) . ')');
98
    }
99
100
    protected function skipComment() {
101
        $this->readWhile(
102
            function ($char) {
103
                return $char != "\n";
104
            }
105
        );
106
        $this->input->next();
107
    }
108
109
    /**
110
     * @return Token
111
     */
112 8
    protected function readDoubleQuotedString() {
113 8
        return new Token(Token::TYPE_STRING, $this->readEscaped('"'));
114
    }
115
116
    /**
117
     * @return Token
118
     */
119 1
    protected function readSingleQuotedString() {
120 1
        return new Token(Token::TYPE_STRING, $this->readEscaped('\''));
121
    }
122
123
    /**
124
     * @param string $end
125
     *
126
     * @return string
127
     */
128 9
    protected function readEscaped($end) {
129 9
        $escaped = false;
130 9
        $str     = "";
131 9
        $this->input->next();
132 9
        while (!$this->input->eof()) {
133 9
            $char = $this->input->next();
134 9
            if ($escaped) {
135
                $str .= $char;
136
                $escaped = false;
137
            } else {
138 9
                if ($char == "\\") {
139
                    $escaped = true;
140
                } else {
141 9
                    if ($char == $end) {
142 9
                        break;
143
                    } else {
144 9
                        $str .= $char;
145
                    }
146
                }
147
            }
148 9
        }
149 9
        return $str;
150
    }
151
152
    /**
153
     * @return Token
154
     */
155 6
    protected function readNumber() {
156 6
        $hasDot = false;
157 6
        $number = $this->readWhile(
158
            function ($char) use (&$hasDot) {
159 6
                if ($char == '.') {
160 1
                    if ($hasDot) {
161
                        return false;
162
                    }
163 1
                    $hasDot = true;
164 1
                    return true;
165
                }
166 6
                return $this->isDigit($char);
167
            }
168 6
        );
169 6
        return new Token(Token::TYPE_NUMBER, $hasDot ? floatval($number) : intval($number));
170
    }
171
172
    /**
173
     * @return Token
174
     */
175 9
    protected function readIdentifier() {
176 9
        $first      = false;
177 9
        $identifier = $this->readWhile(
178 9
            function ($char) use (&$first) {
179 9
                if ($first) {
180
                    $first = false;
181
                    return $this->isStartIdentifierCharacter($char);
182
                }
183 9
                return $this->isIdentifierCharacter($char);
184
            }
185 9
        );
186 9
        if ($this->isKeyword($identifier)) {
187 3
            return new Token(Token::TYPE_KEYWORD, $identifier);
188
        }
189 6
        return new Token(Token::TYPE_IDENTIFIER, $identifier);
190
    }
191
192
    /**
193
     * @return Token
194
     */
195 8
    protected function readPunctuation() {
196 8
        return new Token(Token::TYPE_PUNCTUATION, $this->input->next());
197
    }
198
199
    /**
200
     * @param callable $predicate
201
     *
202
     * @return string
203
     */
204 21
    protected function readWhile(callable $predicate) {
205 21
        $str = "";
206 21
        while (!$this->input->eof() && call_user_func($predicate, $this->input->peek())) {
207 13
            $str .= $this->input->next();
208 13
        }
209 21
        return $str;
210
    }
211
212 21
    protected function isWhitespace($char) {
213 21
        return strpos(" \t\n\r", $char) !== false;
214
    }
215
216
    /**
217
     * @param string $char
218
     *
219
     * @return bool
220
     */
221 17
    protected function isDigit($char) {
222 17
        return is_numeric($char);
223
    }
224
225
    /**
226
     * @param string $char
227
     *
228
     * @return bool
229
     */
230 13
    protected function isStartIdentifierCharacter($char) {
231 13
        return preg_match('/[a-zA-Z_]/', $char) === 1;
232
    }
233
234
    /**
235
     * @param string $char
236
     *
237
     * @return bool
238
     */
239 9
    protected function isIdentifierCharacter($char) {
240 9
        return preg_match('/[a-zA-Z0-9_]/', $char) === 1;
241
    }
242
243
    /**
244
     * @param string $char
245
     *
246
     * @return bool
247
     */
248 9
    protected function isPunctuation($char) {
249 9
        return strpos(",{}=[]", $char) !== false;
250
    }
251
252
    /**
253
     * @param string $text
254
     *
255
     * @return bool
256
     */
257 9
    protected function isKeyword($text) {
258 9
        return in_array($text, Lua::$luaKeywords);
259
    }
260
}