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