Completed
Push — master ( bf4763...20c688 )
by Hans
02:07
created

Lexer::getLocation()   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 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 1
1
<?php
2
3
namespace HansOtt\GraphQL\Query;
4
5
final class Lexer
6
{
7
    /**
8
     * @var ScannerWithLocation
9
     */
10
    private $scanner;
11
    private $tokens;
12
13 63
    private function emit($type, $value, $location)
14
    {
15 63
        $this->tokens[] = new Token($type, $value, $location);
16 63
    }
17
18 90
    private function getLocation()
19
    {
20 90
        $line = $this->scanner->getLine();
21 90
        $column = $this->scanner->getColumn();
22
23 90
        return new Location($line, $column);
24
    }
25
26 27
    private function getError($message)
27
    {
28 27
        $line = $this->scanner->getLine();
29 27
        $column = $this->scanner->getColumn();
30
31 27
        return new SyntaxError($message . " (line {$line}, column {$column})");
32
    }
33
34 60
    private function name()
35
    {
36 60
        $name = $this->scanner->next();
37 60
        $location = $this->getLocation();
38
39 60
        if ($this->scanner->eof()) {
40
            $this->emit(Token::T_NAME, $name, $location);
41
            return;
42
        }
43
44 60
        $next = $this->scanner->peek();
45 60
        while ($next === '_' || ctype_alpha($next) || ctype_digit($next)) {
46 60
            $name .= $this->scanner->next();
47 60
            if ($this->scanner->eof()) {
48 3
                break;
49
            }
50 60
            $next = $this->scanner->peek();
51 60
        }
52
53 60
        $type = Token::T_NAME;
54 60
        if ($name === 'query') {
55 15
            $type = Token::T_QUERY;
56 60
        } elseif ($name === 'mutation') {
57
            $type = Token::T_MUTATION;
58 60
        } elseif ($name === 'subscription') {
59
            $type = Token::T_SUBSCRIPTION;
60 60
        } elseif ($name === 'fragment') {
61 6
            $type = Token::T_FRAGMENT;
62 60
        } elseif ($name === 'true') {
63 3
            $type = Token::T_TRUE;
64 60
        } elseif ($name === 'false') {
65 3
            $type = Token::T_FALSE;
66 60
        } elseif ($name === 'null') {
67 3
            $type = Token::T_NULL;
68 3
        }
69
70 60
        $this->emit($type, $name, $location);
71 60
    }
72
73
    private function comment()
74
    {
75
        $this->scanner->next();
76
        $next = $this->scanner->peek();
77
        while ($this->scanner->eof() === false && $next !== "\n" && $next !== "\r") {
78
            $this->scanner->next();
79
            $next = $this->scanner->peek();
80
        }
81
    }
82
83 6
    private function spread()
84
    {
85 6
        $points = $this->scanner->next();
86 6
        $location = $this->getLocation();
87 6
        $next = $this->scanner->peek();
88
89 6
        if ($next !== '.') {
90
            throw $this->getError("Expected \".\" but instead found \"{$next}\"");
91
        }
92
93 6
        $points .= $this->scanner->next();
94 6
        $next = $this->scanner->peek();
95
96 6
        if ($next !== '.') {
97
            throw $this->getError("Expected \".\" but instead found \"{$next}\"");
98
        }
99
100 6
        $points .= $this->scanner->next();
101 6
        $this->emit(Token::T_SPREAD, $points, $location);
102 6
    }
103
104 24
    private function str()
105
    {
106 24
        $this->scanner->next();
107 24
        $location = $this->getLocation();
108 24
        $string = '';
109 24
        $previousChar = false;
110
111 24
        while (true) {
112 24
            if ($this->scanner->eof()) {
113 3
                throw $this->getError('Unclosed quote');
114
            }
115 21
            $next = $this->scanner->peek();
116 21
            if ($previousChar !== '\\' && $next === '"') {
117 21
                $this->scanner->next();
118 21
                break;
119
            }
120 21
            $previousChar = $this->scanner->next();
121 21
            $string .= $previousChar;
122 21
        }
123
124 21
        $string = json_decode('"' . $string . '"');
125 21
        $this->emit(Token::T_STRING, $string, $location);
126 21
    }
127
128 48
    private function integerPart()
129
    {
130 48
        $number = $this->scanner->next();
131 48
        $location = $this->getLocation();
132 48
        if ($number === '-') {
133 12
            if ($this->scanner->eof()) {
134 3
                throw $this->getError('Expected a digit but instead reached end');
135
            }
136 9
            $next = $this->scanner->peek();
137 9
            if (ctype_digit($next) === false) {
138
                throw $this->getError("Expected a digit but instead found \"{$next}\"");
139
            }
140 9
        }
141
142 45
        $next = $this->scanner->peek();
143 45
        if ($next === '0') {
144 9
            $number .= $this->scanner->next();
145 9
            return array($number, $location);
146
        }
147
148 36
        $next = $this->scanner->peek();
149 36
        while ($this->scanner->eof() === false && ctype_digit($next)) {
150
            $number .= $this->scanner->next();
151
            $next = $this->scanner->peek();
152
        }
153
154 36
        return array($number, $location);
155
    }
156
157 21
    private function fractionalPart()
158
    {
159 21
        $part = $this->scanner->next();
160
161 21
        if ($this->scanner->eof()) {
162 6
            throw $this->getError('Expected a digit but instead reached end');
163
        }
164
165 15
        $next = $this->scanner->peek();
166 15
        if (ctype_digit($next) === false) {
167
            throw $this->getError("Expected a digit but instead found \"{$next}\"");
168
        }
169
170 15
        $next = $this->scanner->peek();
171 15
        while ($this->scanner->eof() === false && ctype_digit($next)) {
172 15
            $part .= $this->scanner->next();
173 15
            $next = $this->scanner->peek();
174 15
        }
175
176 15
        return $part;
177
    }
178
179 15
    private function exponentPart()
180
    {
181 15
        $part = $this->scanner->next();
182
183 15
        if ($this->scanner->eof()) {
184 9
            throw $this->getError('Expected a digit but instead reached end');
185
        }
186
187 6
        $next = $this->scanner->peek();
188 6
        if ($next === '+' || $next === '-') {
189
            $part .= $this->scanner->next();
190
        }
191
192 6
        $next = $this->scanner->peek();
193 6
        if (ctype_digit($next) === false) {
194 6
            throw $this->getError("Expected a digit but instead found \"{$next}\"");
195
        }
196
197
        $next = $this->scanner->peek();
198
        while ($this->scanner->eof() === false && ctype_digit($next)) {
199
            $part .= $this->scanner->next();
200
            $next = $this->scanner->peek();
201
        }
202
203
        return $part;
204
    }
205
206 48
    private function number()
207
    {
208 48
        list ($integerPart, $location) = $this->integerPart();
209 45
        if ($this->scanner->eof()) {
210
            $this->emit(Token::T_INT, $integerPart, $location);
211
            return;
212
        }
213
214 45
        $next = $this->scanner->peek();
215 45
        if ($next !== '.' && $next !== 'e' && $next !== 'E') {
216 24
            $this->emit(Token::T_INT, $integerPart, $location);
217 24
            return;
218
        }
219
220 24
        $number = $integerPart;
221 24
        if ($next === '.') {
222 21
            $number .= $this->fractionalPart();
223 15
        }
224
225 18
        $next = $this->scanner->peek();
226 18
        if ($next === 'e' || $next === 'E') {
227 15
            $number .= $this->exponentPart();
228
        }
229
230 3
        $this->emit(Token::T_FLOAT, $number, $location);
231 3
    }
232
233
    /**
234
     * @param string $query
235
     *
236
     * @throws SyntaxError
237
     *
238
     * @return Token[]
239
     */
240 93
    public function lex($query)
241
    {
242 93
        $flags = PREG_SPLIT_NO_EMPTY;
243 93
        $chars = preg_split('//u', $query, -1, $flags);
244 93
        $scanner = new ScannerGeneric($chars);
245 93
        $this->scanner = new ScannerWithLocation($scanner);
246 93
        $this->tokens = array();
247
        $punctuators = array(
248 93
            '!' => Token::T_EXCLAMATION,
249 93
            '$' => Token::T_DOLLAR,
250 93
            '(' => Token::T_PAREN_LEFT,
251 93
            ')' => Token::T_PAREN_RIGHT,
252 93
            '{' => Token::T_BRACE_LEFT,
253 93
            '}' => Token::T_BRACE_RIGHT,
254 93
            ':' => Token::T_COLON,
255 93
            ',' => Token::T_COMMA,
256 93
            '[' => Token::T_BRACKET_LEFT,
257 93
            ']' => Token::T_BRACKET_RIGHT,
258 93
            '=' => Token::T_EQUAL,
259 93
            '@' => Token::T_AT,
260 93
        );
261
262 93
        while ($this->scanner->eof() === false) {
263 90
            $next = $this->scanner->peek();
264
265 90
            if (ctype_space($next)) {
266 60
                $this->scanner->next();
267 60
                continue;
268
            }
269
270 90
            if ($next === '#') {
271
                $this->comment();
272
                continue;
273
            }
274
275 90
            if ($next === '_' || ctype_alpha($next)) {
276 60
                $this->name();
277 60
                continue;
278
            }
279
280 90
            if ($next === '.') {
281 6
                $this->spread();
282 6
                continue;
283
            }
284
285 90
            if ($next === '"') {
286 24
                $this->str();
287 21
                continue;
288
            }
289
290 87
            if ($next === '-' || ctype_digit($next)) {
291 48
                $this->number();
292 24
                continue;
293
            }
294
295 63
            if (isset($punctuators[$next])) {
296 63
                $this->emit($punctuators[$next], $this->scanner->next(), $this->getLocation());
297 63
                continue;
298
            }
299
300
            throw $this->getError("Unknown character: \"{$next}\"");
301
        }
302
303 66
        return $this->tokens;
304
    }
305
}
306