Completed
Branch master (b0b79e)
by Hans
01:27
created

Parser::parseType()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.0986

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 11
cts 16
cp 0.6875
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 12
nop 0
crap 7.0986
1
<?php
2
3
namespace HansOtt\GraphQL\Query;
4
5
final class Parser
6
{
7
    /**
8
     * @var ScannerTokens
9
     */
10
    private $scanner;
11
    private $lexer;
12
13 99
    public function __construct(Lexer $lexer)
14
    {
15 99
        $this->lexer = $lexer;
16 99
    }
17
18 21
    private function getParseError($message)
19
    {
20 21
        $token = $this->scanner->getLastToken();
21 21
        if ($this->scanner->eof() === false) {
22 12
            $token = $this->scanner->peek();
23 12
        }
24
25 21
        return new ParseError($message . " (line {$token->location->line}, column {$token->location->column})");
26
    }
27
28 66
    private function expect($tokenType)
29
    {
30 66
        $token = $this->scanner->next();
31
32 66
        if ($token->type !== $tokenType) {
33 6
            $expectedToken = Token::getNameFor($tokenType);
34 6
            throw $this->getParseError("Expected \"{$expectedToken}\" but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
35
        }
36
37 66
        return $token;
38
    }
39
40 63 View Code Duplication
    private function accept($tokenType)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
41
    {
42 63
        $token = $this->scanner->peek();
43
44 63
        if ($token->type !== $tokenType) {
45 63
            return false;
46
        }
47
48 45
        return $this->scanner->next();
49
    }
50
51 66 View Code Duplication
    private function is($tokenType)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
52
    {
53 66
        if ($this->scanner->eof()) {
54 3
            return false;
55
        }
56
57 66
        $token = $this->scanner->peek();
58
59 66
        return $token->type === $tokenType;
60
    }
61
62 3
    private function parseObject()
63
    {
64 3
        $location = $this->expect(Token::T_BRACE_LEFT)->location;
65 3
        $fields = array();
66
67 3
        while (true) {
68 3
            if ($this->scanner->eof()) {
69
                throw $this->getParseError('Unclosed brace of object value');
70
            }
71
72 3
            if ($this->accept(Token::T_BRACE_RIGHT)) {
73 3
                break;
74
            }
75
76 3
            $nameToken = $this->expect(Token::T_NAME);
77 3
            $this->expect(Token::T_COLON);
78 3
            $fields[] = new ValueObjectField($nameToken->value, $this->parseValue(), $nameToken->location);
79 3
            $this->accept(Token::T_COMMA);
80 3
        }
81
82 3
        return new ValueObject($fields, $location);
83
    }
84
85 6 View Code Duplication
    private function parseList()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
86
    {
87 6
        $location = $this->expect(Token::T_BRACKET_LEFT)->location;
88 6
        $items = array();
89
90 6
        while (true) {
91 6
            if ($this->scanner->eof()) {
92
                throw $this->getParseError('Unclosed bracket of list');
93
            }
94
95 6
            if ($this->accept(Token::T_BRACKET_RIGHT)) {
96 3
                break;
97
            }
98
99 6
            $items[] = $this->parseValue();
100 3
            $this->accept(Token::T_COMMA);
101 3
        }
102
103 3
        return new ValueList($items, $location);
104
    }
105
106 15
    private function parseVariable()
107
    {
108 15
        $location = $this->expect(Token::T_DOLLAR)->location;
109 15
        $name = $this->expect(Token::T_NAME)->value;
110
111 15
        return new ValueVariable($name, $location);
112
    }
113
114 30
    private function parseValue()
115
    {
116 30
        if ($this->is(Token::T_DOLLAR)) {
117 12
            return $this->parseVariable();
118
        }
119
120 24
        if ($string = $this->accept(Token::T_STRING)) {
121 18
            return new ValueString($string->value, $string->location);
122
        }
123
124 21
        if ($true = $this->accept(Token::T_TRUE)) {
125 3
            return new ValueBoolean(true, $true->location);
126
        }
127
128 21
        if ($false = $this->accept(Token::T_FALSE)) {
129 3
            return new ValueBoolean(false, $false->location);
130
        }
131
132 21
        if ($null = $this->accept(Token::T_NULL)) {
133 3
            return new ValueNull($null->location);
134
        }
135
136 21
        if ($int = $this->accept(Token::T_INT)) {
137 18
            return new ValueInt($int->value, $int->location);
138
        }
139
140 6
        if ($float = $this->accept(Token::T_FLOAT)) {
141 3
            return new ValueFloat($float->value, $float->location);
142
        }
143
144 6
        if ($name = $this->accept(Token::T_NAME)) {
145 3
            return new ValueEnum($name->value, $name->location);
146
        }
147
148 6
        if ($this->is(Token::T_BRACKET_LEFT)) {
149 6
            return $this->parseList();
150
        }
151
152 6
        if ($this->is(Token::T_BRACE_LEFT)) {
153 3
            return $this->parseObject();
154
        }
155
156 3
        $message = 'Expected a value';
157
158 3
        if ($this->scanner->eof()) {
159
            throw $this->getParseError($message . ' but instead reached end');
160
        }
161
162 3
        $token = $this->scanner->peek();
163 3
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
164
    }
165
166 36
    private function parseArgument()
167
    {
168 36
        $nameToken = $this->expect(Token::T_NAME);
169 30
        $this->expect(Token::T_COLON);
170 30
        $value = $this->parseValue();
171
172 27
        return new Argument($nameToken->value, $value, $nameToken->location);
173
    }
174
175 60 View Code Duplication
    private function parseArgumentList()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
    {
177 60
        $arguments = array();
178
179 60
        if ($this->is(Token::T_PAREN_LEFT) === false) {
180 30
            return $arguments;
181
        }
182
183 39
        $this->expect(Token::T_PAREN_LEFT);
184
185 39
        while (true) {
186 39
            if ($this->scanner->eof()) {
187
                throw $this->getParseError('Unclosed brace of argument list');
188
            }
189
190 39
            if ($this->accept(Token::T_PAREN_RIGHT)) {
191 30
                break;
192
            }
193
194 36
            $arguments[] = $this->parseArgument();
195 27
            $this->accept(Token::T_COMMA);
196 27
        }
197
198 30
        return $arguments;
199
    }
200
201 60
    private function parseField()
202
    {
203 60
        $nameToken = $this->expect(Token::T_NAME);
204 60
        $name = $nameToken->value;
205
206 60
        $alias = null;
207 60
        if ($this->is(Token::T_COLON)) {
208 3
            $this->expect(Token::T_COLON);
209 3
            $aliasToken = $this->expect(Token::T_NAME);
210 3
            $alias = $name;
211 3
            $name = $aliasToken->value;
212 3
        }
213
214 60
        $arguments = $this->parseArgumentList();
215 51
        $directives = $this->parseDirectiveList();
216
217 51
        $selectionSet = null;
218 51
        if ($this->is(Token::T_BRACE_LEFT)) {
219 21
            $selectionSet = $this->parseSelectionSet();
220 18
        }
221
222 48
        return new SelectionField($alias, $name, $arguments, $directives, $selectionSet, $nameToken->location);
223
    }
224
225 6
    private function parseFragment()
226
    {
227 6
        $location = $this->expect(Token::T_SPREAD)->location;
228 6
        $fragmentName = $this->expect(Token::T_NAME)->value;
229
230 6
        if ($fragmentName !== 'on') {
231 3
            return new SelectionFragmentSpread($fragmentName, $location);
232
        }
233
234 3
        $this->scanner->back();
235 3
        $typeCondition = $this->parseTypeCondition();
236 3
        $directives = $this->parseDirectiveList();
237 3
        $selectionSet = $this->parseSelectionSet();
238
239 3
        return new SelectionInlineFragment($typeCondition, $directives, $selectionSet, $location);
240
    }
241
242 66
    private function parseSelection()
243
    {
244 66
        if ($this->is(Token::T_SPREAD)) {
245 9
            return $this->parseFragment();
246
        }
247
248 66
        if ($this->is(Token::T_NAME)) {
249 60
            return $this->parseField();
250
        }
251
252 3
        $message = 'Expected a field, a fragment spread or an inline fragment';
253
254 3
        if ($this->scanner->eof()) {
255
            throw $this->getParseError($message . ' but instead reached end');
256
        }
257
258 3
        $token = $this->scanner->peek();
259 3
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
260
    }
261
262 66 View Code Duplication
    private function parseSelectionSet()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
    {
264 66
        $location = $this->expect(Token::T_BRACE_LEFT)->location;
265
266
        /** @var Selection[] $selections */
267 66
        $selections = array();
268 66
        while (true) {
269 66
            if ($this->scanner->eof()) {
270 9
                throw $this->getParseError('Unclosed brace of selection set');
271
            }
272
273 63
            if ($this->accept(Token::T_BRACE_RIGHT)) {
274 45
                break;
275
            }
276
277 63
            $selections[] = $this->parseSelection();
278 48
        }
279
280 45
        return new SelectionSet($selections, $location);
281
    }
282
283 6
    private function parseTypeCondition()
284
    {
285 6
        $nameToken = $this->expect(Token::T_NAME);
286 6
        $on = $nameToken->value;
287 6
        if ($on !== 'on') {
288
            $tokenNameName = Token::getNameFor(Token::T_NAME);
289
            throw $this->getParseError("Expected a type condition but instead found \"{$tokenNameName}\" with value \"{$on}\"");
290
        }
291
292 6
        $type = $this->parseNamedType();
293
294 6
        return new TypeCondition($type, $nameToken->location);
295
    }
296
297 3
    private function parseListType()
298
    {
299 3
        $location = $this->expect(Token::T_BRACKET_LEFT)->location;
300 3
        $type = $this->parseType();
301 3
        $this->accept(Token::T_BRACKET_RIGHT);
302
303 3
        return new TypeList($type, $location);
304
    }
305
306 18
    private function parseNamedType()
307
    {
308 18
        $nameToken = $this->expect(Token::T_NAME);
309 18
        $type = new TypeNamed($nameToken->value, $nameToken->location);
310
311 18
        return $type;
312
    }
313
314 12
    private function parseType()
315
    {
316 12
        $type = null;
317 12
        if ($this->is(Token::T_BRACKET_LEFT)) {
318 3
            $type = $this->parseListType();
319 12
        } elseif ($this->is(Token::T_NAME)) {
320 12
            $type = $this->parseNamedType();
321 12
        }
322
323 12
        if ($type !== null) {
324 12
            if ($this->accept(Token::T_EXCLAMATION)) {
325 6
                return new TypeNonNull($type, $type->location);
326
            }
327 9
            return $type;
328
        }
329
330
        $message = 'Expected a type';
331
332
        if ($this->scanner->eof()) {
333
            throw $this->getParseError($message . ' but instead reached end');
334
        }
335
336
        $token = $this->scanner->peek();
337
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
338
    }
339
340 6
    private function parseDirective()
341
    {
342 6
        $location = $this->expect(Token::T_AT)->location;
343 6
        $name = $this->expect(Token::T_NAME)->value;
344 6
        $arguments = $this->parseArgumentList();
345
346 6
        return new Directive($name, $arguments, $location);
347
    }
348
349 51
    private function parseDirectiveList()
350
    {
351 51
        $directives = array();
352 51
        while ($this->is(Token::T_AT)) {
353 6
            $directives[] = $this->parseDirective();
354 6
        }
355
356 51
        return $directives;
357
    }
358
359 12
    private function parseVariableDefinition()
360
    {
361 12
        $variable = $this->parseVariable();
362 12
        $this->expect(Token::T_COLON);
363 12
        $type = $this->parseType();
364 12
        $defaultValue = null;
365
366 12
        if ($this->accept(Token::T_EQUAL)) {
367 3
            $defaultValue = $this->parseValue();
368 3
        }
369
370 12
        return new VariableDefinition($variable, $type, $defaultValue, $variable->location);
371
    }
372
373 18 View Code Duplication
    private function parseVariableDefinitionList()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
374
    {
375 18
        $definitions = array();
376
377 18
        if ($this->is(Token::T_PAREN_LEFT) === false) {
378 6
            return $definitions;
379
        }
380
381 12
        $this->expect(Token::T_PAREN_LEFT);
382
383 12
        while (true) {
384 12
            if ($this->scanner->eof()) {
385
                throw $this->getParseError('Unclosed parenthesis of variable definition list');
386
            }
387
388 12
            if ($this->accept(Token::T_PAREN_RIGHT)) {
389 12
                break;
390
            }
391
392 12
            $definitions[] = $this->parseVariableDefinition();
393 12
            $this->accept(Token::T_COMMA);
394 12
        }
395
396 12
        return $definitions;
397
    }
398
399 66
    private function parseDefinition()
400
    {
401 66
        if ($this->is(Token::T_FRAGMENT)) {
402 3
            $location = $this->expect(Token::T_FRAGMENT)->location;
403
404 3
            $name = $this->expect(Token::T_NAME)->value;
405 3
            if ($name === 'on') {
406
                throw $this->getParseError('A fragment cannot be named "on"');
407
            }
408
409 3
            $typeCondition = $this->parseTypeCondition();
410 3
            $directives = $this->parseDirectiveList();
411 3
            $selectionSet = $this->parseSelectionSet();
412
413 3
            return new Fragment($name, $typeCondition, $directives, $selectionSet, $location);
414
        }
415
416 66 View Code Duplication
        if ($this->is(Token::T_MUTATION)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
417
            $location = $this->expect(Token::T_MUTATION)->location;
418
            $name = $this->expect(Token::T_NAME)->value;
419
            $variables = $this->parseVariableDefinitionList();
420
            $directives = $this->parseDirectiveList();
421
            $selectionSet = $this->parseSelectionSet();
422
423
            return new OperationMutation($name, $variables, $directives, $selectionSet, $location);
424
        }
425
426 66 View Code Duplication
        if ($this->is(Token::T_SUBSCRIPTION)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
427
            $location = $this->expect(Token::T_SUBSCRIPTION)->location;
428
            $name = $this->expect(Token::T_NAME)->value;
429
            $variables = $this->parseVariableDefinitionList();
430
            $directives = $this->parseDirectiveList();
431
            $selectionSet = $this->parseSelectionSet();
432
433
            return new OperationSubscription($name, $variables, $directives, $selectionSet, $location);
434
        }
435
436 66
        if ($this->is(Token::T_QUERY)) {
437 18
            $location = $this->expect(Token::T_QUERY)->location;
438
439 18
            $name = null;
440 18
            if ($this->is(Token::T_NAME)) {
441 15
                $name = $this->expect(Token::T_NAME)->value;
442 15
            }
443
444 18
            $variables = $this->parseVariableDefinitionList();
445 18
            $directives = $this->parseDirectiveList();
446 18
            $selectionSet = $this->parseSelectionSet();
447
448 18
            return new OperationQuery($name, $variables, $directives, $selectionSet, $location);
449
        }
450
451 48
        if ($this->is(Token::T_BRACE_LEFT)) {
452 48
            $selectionSet = $this->parseSelectionSet();
453
454 27
            return new OperationQuery(null, array(), array(), $selectionSet, $selectionSet->location);
455
        }
456
457
        $message = 'Expected a query, a query shorthand, a mutation or a subscription operation';
458
459
        if ($this->scanner->eof()) {
460
            throw $this->getParseError($message . ' but instead reached end');
461
        }
462
463
        $token = $this->scanner->peek();
464
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
465
    }
466
467 69
    private function parseDocument()
468
    {
469
        /** @var Definition[] $definitions */
470 69
        $definitions = array();
471 69
        while ($this->scanner->eof() === false) {
472 66
            $definitions[] = $this->parseDefinition();
473 45
        }
474
475 48
        return new Document($definitions);
476
    }
477
478 96
    public function parse($query)
479
    {
480 96
        $tokens = $this->lexer->lex($query);
481 69
        $scanner = new ScannerGeneric($tokens);
482 69
        $this->scanner = new ScannerTokens($scanner);
483 69
        $document = $this->parseDocument();
484
485 48
        return $document;
486
    }
487
}
488