Completed
Push — master ( 631929...5231d5 )
by Alexandr
02:39
created

Tokenizer::scanString()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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