Completed
Push — master ( e40fed...ed70fa )
by Portey
02:55
created

Tokenizer::getKeyword()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8

Importance

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