Completed
Push — master ( f8702a...461e07 )
by Alexandr
04:08
created

Tokenizer::lex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * Date: 23.11.15
4
 *
5
 * @author Portey Vasil <[email protected]>
6
 */
7
8
namespace Youshido\GraphQL\Parser;
9
10
use Youshido\GraphQL\Parser\Exception\SyntaxErrorException;
11
12
class Tokenizer
13
{
14
    protected $source;
15
    protected $pos       = 0;
16
    protected $line      = 1;
17
    protected $lineStart = 0;
18
19
    /** @var  Token */
20
    protected $lookAhead;
21
22 49
    public function setSource($source)
23
    {
24 49
        $this->source    = $source;
25 49
        $this->lookAhead = $this->next();
26 49
    }
27
28 49
    protected function next()
29
    {
30 49
        $this->skipWhitespace();
31
32
        /** @var Token $token */
33 49
        $token = $this->scan();
34
35 49
        return $token;
36
    }
37
38 49
    protected function skipWhitespace()
39
    {
40 49
        while ($this->pos < strlen($this->source)) {
41 49
            $ch = $this->source[$this->pos];
42 49
            if ($ch === ' ' || $ch === "\t") {
43 47
                $this->pos++;
44 49
            } elseif ($ch === '#') {
45 1
                $this->pos++;
46
                while (
47 1
                    $this->pos < strlen($this->source) &&
48 1
                    ($code = ord($this->source[$this->pos])) &&
49 1
                    $code !== 10 && $code !== 13 && $code !== 0x2028 && $code !== 0x2029
50
                ) {
51 1
                    $this->pos++;
52
                }
53 49
            } elseif ($ch === "\r") {
54
                $this->pos++;
55
                if ($this->source[$this->pos] === "\n") {
56
                    $this->pos++;
57
                }
58
                $this->line++;
59
                $this->lineStart = $this->pos;
60 49
            } elseif ($ch === "\n") {
61 21
                $this->pos++;
62 21
                $this->line++;
63 21
                $this->lineStart = $this->pos;
64
            } else {
65 49
                break;
66
            }
67
        }
68 49
    }
69
70
    /**
71
     * @return Token
72
     */
73 49
    protected function scan()
74
    {
75 49
        if ($this->pos >= strlen($this->source)) {
76 39
            return new Token(Token::TYPE_END);
77
        }
78
79 49
        $ch = $this->source[$this->pos];
80
        switch ($ch) {
81 49
            case Token::TYPE_LPAREN:
82 24
                ++$this->pos;
83
84 24
                return new Token(Token::TYPE_LPAREN);
85 49
            case Token::TYPE_RPAREN:
86 19
                ++$this->pos;
87
88 19
                return new Token(Token::TYPE_RPAREN);
89 49
            case Token::TYPE_LBRACE:
90 47
                ++$this->pos;
91
92 47
                return new Token(Token::TYPE_LBRACE);
93 49
            case Token::TYPE_RBRACE:
94 39
                ++$this->pos;
95
96 39
                return new Token(Token::TYPE_RBRACE);
97 48
            case Token::TYPE_LT:
98 1
                ++$this->pos;
99
100 1
                return new Token(Token::TYPE_LT);
101 48
            case Token::TYPE_GT:
102
                ++$this->pos;
103
104
                return new Token(Token::TYPE_GT);
105 48
            case Token::TYPE_AMP:
106
                ++$this->pos;
107
108
                return new Token(Token::TYPE_AMP);
109 48
            case Token::TYPE_COMMA:
110 26
                ++$this->pos;
111
112 26
                return new Token(Token::TYPE_COMMA);
113 48
            case Token::TYPE_LSQUARE_BRACE:
114 4
                ++$this->pos;
115
116 4
                return new Token(Token::TYPE_LSQUARE_BRACE);
117 48
            case Token::TYPE_RSQUARE_BRACE:
118 3
                ++$this->pos;
119
120 3
                return new Token(Token::TYPE_RSQUARE_BRACE);
121 48
            case Token::TYPE_COLON:
122 29
                ++$this->pos;
123
124 29
                return new Token(Token::TYPE_COLON);
125
126 48
            case Token::TYPE_POINT:
127 8
                if ($this->checkFragment()) {
128 8
                    return new Token(Token::TYPE_FRAGMENT_REFERENCE);
129
                }
130
131
                break;
132
133 48
            case Token::TYPE_VARIABLE:
134 7
                ++$this->pos;
135
136 7
                return new Token(Token::TYPE_VARIABLE);
137
        }
138
139 48
        if ($ch === '_' || 'a' <= $ch && $ch <= 'z' || 'A' <= $ch && $ch <= 'Z') {
140 48
            return $this->scanWord();
141
        }
142
143 15
        if ($ch === '-' || '0' <= $ch && $ch <= '9') {
144 10
            return $this->scanNumber();
145
        }
146
147 9
        if ($ch === '"') {
148 9
            return $this->scanString();
149
        }
150
151
        throw $this->createIllegal();
152
    }
153
154 8
    protected function checkFragment()
155
    {
156 8
        $this->pos++;
157 8
        $ch = $this->source[$this->pos];
158
159 8
        $this->pos++;
160 8
        $nextCh = $this->source[$this->pos];
161
162 8
        $isset = $ch == Token::TYPE_POINT && $nextCh == Token::TYPE_POINT;
163
164 8
        if ($isset) {
165 8
            $this->pos++;
166
167 8
            return true;
168
        }
169
170
        return false;
171
    }
172
173 48
    protected function scanWord()
174
    {
175 48
        $start = $this->pos;
176 48
        $this->pos++;
177
178 48
        while ($this->pos < strlen($this->source)) {
179 48
            $ch = $this->source[$this->pos];
180
181 48
            if ($ch === '_' || $ch === '$' || 'a' <= $ch && $ch <= ('z') || 'A' <= $ch && $ch <= 'Z' || '0' <= $ch && $ch <= '9') {
182 48
                $this->pos++;
183
            } else {
184 47
                break;
185
            }
186
        }
187
188 48
        $value = substr($this->source, $start, $this->pos - $start);
189
190 48
        return new Token($this->getKeyword($value), $value);
191
    }
192
193 48
    protected function getKeyword($name)
194
    {
195
        switch ($name) {
196 48
            case 'null':
197 2
                return Token::TYPE_NULL;
198
199 48
            case 'true':
200 2
                return Token::TYPE_TRUE;
201
202 48
            case 'false':
203 1
                return Token::TYPE_FALSE;
204
205 48
            case 'query':
206 16
                return Token::TYPE_QUERY;
207
208 47
            case 'fragment':
209 3
                return Token::TYPE_FRAGMENT;
210
211 47
            case 'mutation':
212 5
                return Token::TYPE_MUTATION;
213
214 47
            case 'on':
215 6
                return Token::TYPE_ON;
216
        }
217
218 47
        return Token::TYPE_IDENTIFIER;
219
    }
220
221 10
    protected function scanNumber()
222
    {
223 10
        $start = $this->pos;
224 10
        if ($this->source[$this->pos] === '-') {
225 1
            ++$this->pos;
226
        }
227
228 10
        $this->skipInteger();
229
230 10
        if ($this->source[$this->pos] === '.') {
231 1
            $this->pos++;
232 1
            $this->skipInteger();
233
        }
234
235 10
        $value = substr($this->source, $start, $this->pos - $start);
236
237 10
        if (strpos($value, '.') === false) {
238 10
            $value = (int)$value;
239
        } else {
240 1
            $value = (float)$value;
241
        }
242
243 10
        return new Token(Token::TYPE_NUMBER, $value);
244
    }
245
246 10
    protected function skipInteger()
247
    {
248 10
        $start = $this->pos;
249
250 10
        while ($this->pos < strlen($this->source)) {
251 10
            $ch = $this->source[$this->pos];
252 10
            if ('0' <= $ch && $ch <= '9') {
253 10
                $this->pos++;
254
            } else {
255 10
                break;
256
            }
257
        }
258
259 10
        if ($this->pos - $start === 0) {
260
            throw $this->createIllegal();
261
        }
262 10
    }
263
264
    protected function createIllegal()
265
    {
266
        return $this->pos < strlen($this->source)
267
            ? $this->createError("Unexpected {$this->source[$this->pos]}")
268
            : $this->createError('Unexpected end of input');
269
    }
270
271 1
    protected function createError($message)
272
    {
273 1
        return new SyntaxErrorException($message . " ({$this->line}:{$this->getColumn()})");
274
    }
275
276 1
    protected function getColumn()
277
    {
278 1
        return $this->pos - $this->lineStart;
279
    }
280
281 9
    protected function scanString()
282
    {
283 9
        $this->pos++;
284
285 9
        $value = '';
286 9
        while ($this->pos < strlen($this->source)) {
287 9
            $ch = $this->source[$this->pos];
288 9
            if ($ch === '"' && $this->source[$this->pos - 1] != '\\') {
289 9
                $this->pos++;
290
291 9
                return new Token(Token::TYPE_STRING, $value);
292
            }
293
294 9
            $value .= $ch;
295 9
            $this->pos++;
296
        }
297
298
        throw $this->createIllegal();
299
    }
300
301 49
    protected function end()
302
    {
303 49
        return $this->lookAhead->getType() === Token::TYPE_END;
304
    }
305
306 49
    protected function peek()
307
    {
308 49
        return $this->lookAhead;
309
    }
310
311 48
    protected function lex()
312
    {
313 48
        $prev            = $this->lookAhead;
314 48
        $this->lookAhead = $this->next();
315
316 48
        return $prev;
317
    }
318
319 6
    protected function createUnexpected(Token $token)
320
    {
321 6
        switch ($token->getType()) {
322 6
            case Token::TYPE_END:
323
                return $this->createError('Unexpected end of input');
324 6
            case Token::TYPE_NUMBER:
325
                return $this->createError('Unexpected number');
326 6
            case Token::TYPE_STRING:
327 1
                return $this->createError('Unexpected string');
328 5
            case Token::TYPE_IDENTIFIER:
329
                return $this->createError('Unexpected identifier');
330
        }
331
332 5
        return new SyntaxErrorException('Unexpected token');
333
    }
334
}
335