Completed
Push — master ( d29eda...b0b79e )
by Hans
12:57
created

Parser::parseType()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.1666

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 10
cts 12
cp 0.8333
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 12
nop 0
crap 6.1666
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 96
    public function __construct(Lexer $lexer)
14
    {
15 96
        $this->lexer = $lexer;
16 96
    }
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 63
    private function expect($tokenType)
29
    {
30 63
        $token = $this->scanner->next();
31
32 63
        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 63
        return $token;
38
    }
39
40 60 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 60
        $token = $this->scanner->peek();
43
44 60
        if ($token->type !== $tokenType) {
45 60
            return false;
46
        }
47
48 42
        return $this->scanner->next();
49
    }
50
51 63 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 63
        if ($this->scanner->eof()) {
54 3
            return false;
55
        }
56
57 63
        $token = $this->scanner->peek();
58
59 63
        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 12
    private function parseVariable()
107
    {
108 12
        $location = $this->expect(Token::T_DOLLAR)->location;
109 12
        $name = $this->expect(Token::T_NAME)->value;
110
111 12
        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 57 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 57
        $arguments = array();
178
179 57
        if ($this->is(Token::T_PAREN_LEFT) === false) {
180 27
            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 57
    private function parseField()
202
    {
203 57
        $nameToken = $this->expect(Token::T_NAME);
204 57
        $name = $nameToken->value;
205
206 57
        $alias = null;
207 57
        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 57
        $arguments = $this->parseArgumentList();
215 48
        $directives = $this->parseDirectiveList();
216
217 48
        $selectionSet = null;
218 48
        if ($this->is(Token::T_BRACE_LEFT)) {
219 21
            $selectionSet = $this->parseSelectionSet();
220 18
        }
221
222 45
        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 63
    private function parseSelection()
243
    {
244 63
        if ($this->is(Token::T_SPREAD)) {
245 9
            return $this->parseFragment();
246
        }
247
248 63
        if ($this->is(Token::T_NAME)) {
249 57
            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 63 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 63
        $location = $this->expect(Token::T_BRACE_LEFT)->location;
265
266
        /** @var Selection[] $selections */
267 63
        $selections = array();
268 63
        while (true) {
269 63
            if ($this->scanner->eof()) {
270 9
                throw $this->getParseError('Unclosed brace of selection set');
271
            }
272
273 60
            if ($this->accept(Token::T_BRACE_RIGHT)) {
274 42
                break;
275
            }
276
277 60
            $selections[] = $this->parseSelection();
278 45
        }
279
280 42
        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
    private function parseListType()
298
    {
299
        $location = $this->expect(Token::T_BRACKET_LEFT)->location;
300
        $type = $this->parseType();
301
        $this->accept(Token::T_BRACKET_RIGHT);
302
303
        return new TypeList($type, $location);
304
    }
305
306
    private function parseNamedType()
307
    {
308
        $nameToken = $this->expect(Token::T_NAME);
309
        $type = new TypeNamed($nameToken->value, $nameToken->location);
310
311
        return $type;
312
    }
313
314
    private function parseType()
315
    {
316
        $type = null;
317
        if ($this->is(Token::T_BRACKET_LEFT)) {
318 15
            $type = $this->parseListType();
319
        } elseif ($this->is(Token::T_NAME)) {
320 15
            $type = $this->parseNamedType();
321 15
        }
322
323 15
        if ($type !== null) {
324 3
            if ($this->accept(Token::T_EXCLAMATION)) {
325
                return new TypeNonNull($type, $type->location);
326
            }
327 15
            return $type;
328
        }
329
330 9
        $message = 'Expected a type';
331
332 9
        if ($this->scanner->eof()) {
333
            throw $this->getParseError($message . ' but instead reached end');
334
        }
335
336 9
        $token = $this->scanner->peek();
337 9
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
338
    }
339
340
    private function parseDirective()
341
    {
342
        $location = $this->expect(Token::T_AT)->location;
343
        $name = $this->expect(Token::T_NAME)->value;
344
        $arguments = $this->parseArgumentList();
345
346
        return new Directive($name, $arguments, $location);
347
    }
348
349
    private function parseDirectiveList()
350 6
    {
351
        $directives = array();
352 6
        while ($this->is(Token::T_AT)) {
353 6
            $directives[] = $this->parseDirective();
354 6
        }
355
356 6
        return $directives;
357
    }
358
359 48
    private function parseVariableDefinition()
360
    {
361 48
        $variable = $this->parseVariable();
362 48
        $this->expect(Token::T_COLON);
363 6
        $type = $this->parseType();
364 6
        $defaultValue = null;
365
366 48
        if ($this->accept(Token::T_EQUAL)) {
367
            $defaultValue = $this->parseValue();
368
        }
369 9
370
        return new VariableDefinition($variable, $type, $defaultValue, $variable->location);
371 9
    }
372 9
373 9 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 9
    {
375
        $definitions = array();
376 9
377 3
        if ($this->is(Token::T_PAREN_LEFT) === false) {
378 3
            return $definitions;
379
        }
380 9
381
        $this->expect(Token::T_PAREN_LEFT);
382
383 15
        while (true) {
384
            if ($this->scanner->eof()) {
385 15
                throw $this->getParseError('Unclosed parenthesis of variable definition list');
386
            }
387 15
388 6
            if ($this->accept(Token::T_PAREN_RIGHT)) {
389
                break;
390
            }
391 9
392
            $definitions[] = $this->parseVariableDefinition();
393 9
            $this->accept(Token::T_COMMA);
394 9
        }
395
396
        return $definitions;
397
    }
398 9
399 9
    private function parseDefinition()
400
    {
401
        if ($this->is(Token::T_FRAGMENT)) {
402 9
            $location = $this->expect(Token::T_FRAGMENT)->location;
403 9
404 9
            $name = $this->expect(Token::T_NAME)->value;
405
            if ($name === 'on') {
406 9
                throw $this->getParseError('A fragment cannot be named "on"');
407
            }
408
409 63
            $typeCondition = $this->parseTypeCondition();
410
            $directives = $this->parseDirectiveList();
411 63
            $selectionSet = $this->parseSelectionSet();
412 3
413
            return new Fragment($name, $typeCondition, $directives, $selectionSet, $location);
414 3
        }
415 3
416 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 3
            $variables = $this->parseVariableDefinitionList();
420 3
            $directives = $this->parseDirectiveList();
421 3
            $selectionSet = $this->parseSelectionSet();
422
423 3
            return new OperationMutation($name, $variables, $directives, $selectionSet, $location);
424
        }
425
426 63 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 63
        if ($this->is(Token::T_QUERY)) {
437
            $location = $this->expect(Token::T_QUERY)->location;
438
439
            $name = null;
440
            if ($this->is(Token::T_NAME)) {
441
                $name = $this->expect(Token::T_NAME)->value;
442
            }
443
444
            $variables = $this->parseVariableDefinitionList();
445
            $directives = $this->parseDirectiveList();
446 63
            $selectionSet = $this->parseSelectionSet();
447 15
448
            return new OperationQuery($name, $variables, $directives, $selectionSet, $location);
449 15
        }
450 15
451 12
        if ($this->is(Token::T_BRACE_LEFT)) {
452 12
            $selectionSet = $this->parseSelectionSet();
453
454 15
            return new OperationQuery(null, array(), array(), $selectionSet, $selectionSet->location);
455 15
        }
456 15
457
        $message = 'Expected a query, a query shorthand, a mutation or a subscription operation';
458 15
459
        if ($this->scanner->eof()) {
460
            throw $this->getParseError($message . ' but instead reached end');
461 48
        }
462 48
463
        $token = $this->scanner->peek();
464 27
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
465
    }
466
467
    private function parseDocument()
468
    {
469
        /** @var Definition[] $definitions */
470
        $definitions = array();
471
        while ($this->scanner->eof() === false) {
472
            $definitions[] = $this->parseDefinition();
473
        }
474
475
        return new Document($definitions);
476
    }
477 66
478
    public function parse($query)
479
    {
480 66
        $tokens = $this->lexer->lex($query);
481 66
        $scanner = new ScannerGeneric($tokens);
482 63
        $this->scanner = new ScannerTokens($scanner);
483 42
        $document = $this->parseDocument();
484
485 45
        return $document;
486
    }
487
}
488