Completed
Push — master ( 68ab1d...88b5a8 )
by Hans
01:57
created

Parser::getParseError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
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->line}, column {$token->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
        $this->expect(Token::T_BRACE_LEFT);
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
            $name = $this->expect(Token::T_NAME)->value;
77 3
            $this->expect(Token::T_COLON);
78 3
            $fields[] = new ValueObjectField($name, $this->parseValue());
79 3
            $this->accept(Token::T_COMMA);
80 3
        }
81
82 3
        return new ValueObject($fields);
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
        $this->expect(Token::T_BRACKET_LEFT);
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);
104
    }
105
106 12
    private function parseVariable()
107
    {
108 12
        $this->expect(Token::T_DOLLAR);
109 12
        $name = $this->expect(Token::T_NAME)->value;
110
111 12
        return new ValueVariable($name);
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);
122
        }
123
124 21
        if ($this->accept(Token::T_TRUE)) {
125 3
            return new ValueBoolean(true);
126
        }
127
128 21
        if ($this->accept(Token::T_FALSE)) {
129 3
            return new ValueBoolean(false);
130
        }
131
132 21
        if ($this->accept(Token::T_NULL)) {
133 3
            return new ValueNull;
134
        }
135
136 21
        if ($int = $this->accept(Token::T_INT)) {
137 18
            return new ValueInt($int->value);
138
        }
139
140 6
        if ($float = $this->accept(Token::T_FLOAT)) {
141 3
            return new ValueFloat($float->value);
142
        }
143
144 6
        if ($name = $this->accept(Token::T_NAME)) {
145 3
            return new ValueEnum($name->value);
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
        $name = $this->expect(Token::T_NAME)->value;
169 30
        $this->expect(Token::T_COLON);
170 30
        $value = $this->parseValue();
171
172 27
        return new Argument($name, $value);
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
        $name = $this->expect(Token::T_NAME)->value;
204
205 57
        $alias = null;
206 57
        if ($this->is(Token::T_COLON)) {
207 3
            $this->expect(Token::T_COLON);
208 3
            $aliasToken = $this->expect(Token::T_NAME);
209 3
            $alias = $name;
210 3
            $name = $aliasToken->value;
211 3
        }
212
213 57
        $arguments = $this->parseArgumentList();
214 48
        $directives = $this->parseDirectiveList();
215
216 48
        $selectionSet = null;
217 48
        if ($this->is(Token::T_BRACE_LEFT)) {
218 21
            $selectionSet = $this->parseSelectionSet();
219 18
        }
220
221 45
        return new SelectionField($alias, $name, $arguments, $directives, $selectionSet);
222
    }
223
224 6
    private function parseFragment()
225
    {
226 6
        $this->expect(Token::T_SPREAD);
227 6
        $fragmentName = $this->expect(Token::T_NAME)->value;
228
229 6
        if ($fragmentName !== 'on') {
230 3
            return new SelectionFragmentSpread($fragmentName);
231 3
        }
232
233 3
        $this->scanner->back();
234 3
        $typeCondition = $this->parseTypeCondition();
235 6
        $directives = $this->parseDirectiveList();
236 3
        $selectionSet = $this->parseSelectionSet();
237
238 3
        return new SelectionInlineFragment($typeCondition, $directives, $selectionSet);
239
    }
240
241 60 View Code Duplication
    private function parseSelection()
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...
242
    {
243 60
        if ($this->is(Token::T_SPREAD)) {
244 6
            return $this->parseFragment();
245
        }
246
247 60
        if ($this->is(Token::T_NAME)) {
248 57
            return $this->parseField();
249
        }
250
251 3
        $message = 'Expected a field, a fragment spread or an inline fragment';
252
253 3
        if ($this->scanner->eof()) {
254
            throw $this->getParseError($message . ' but instead reached end');
255
        }
256
257 3
        $token = $this->scanner->peek();
258 3
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
259
    }
260
261 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...
262
    {
263 63
        $this->expect(Token::T_BRACE_LEFT);
264
265 63
        $selections = array();
266 63
        while (true) {
267 63
            if ($this->scanner->eof()) {
268 9
                throw $this->getParseError('Unclosed brace of selection set');
269
            }
270
271 60
            if ($this->accept(Token::T_BRACE_RIGHT)) {
272 42
                break;
273
            }
274
275 60
            $selections[] = $this->parseSelection();
276 45
        }
277
278 42
        return new SelectionSet($selections);
279
    }
280
281 6
    private function parseTypeCondition()
282
    {
283 6
        $on = $this->expect(Token::T_NAME)->value;
284 6
        if ($on !== 'on') {
285
            $tokenNameName = Token::getNameFor(Token::T_NAME);
286
            throw $this->getParseError("Expected a type condition but instead found \"{$tokenNameName}\" with value \"{$on}\"");
287
        }
288
289 6
        $type = $this->parseNamedType();
290
291 6
        return new TypeCondition($type);
0 ignored issues
show
Bug introduced by
It seems like $type defined by $this->parseNamedType() on line 289 can also be of type object<HansOtt\GraphQL\Query\TypeNonNull>; however, HansOtt\GraphQL\Query\TypeCondition::__construct() does only seem to accept object<HansOtt\GraphQL\Query\TypeNamed>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
292
    }
293
294 View Code Duplication
    private function parseListType()
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...
295
    {
296
        $this->expect(Token::T_BRACKET_LEFT);
297
298
        $types = array();
299
        while (true) {
300
            if ($this->scanner->eof()) {
301
                throw $this->getParseError('Unclosed bracket of list type');
302
            }
303
304
            if ($this->accept(Token::T_BRACE_RIGHT)) {
305
                break;
306
            }
307
308
            $types[] = $this->parseType();
309
            $this->accept(Token::T_COMMA);
310
        }
311
312
        return new TypeList($types);
313
    }
314
315 15
    private function parseNamedType()
316
    {
317 15
        $name = $this->expect(Token::T_NAME)->value;
318 15
        $type = new TypeNamed($name);
319
320 15
        if ($this->accept(Token::T_EXCLAMATION)) {
321 3
            return new TypeNonNull($type);
322
        }
323
324 15
        return $type;
325
    }
326
327 9 View Code Duplication
    private function parseType()
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...
328
    {
329 9
        if ($this->is(Token::T_BRACKET_LEFT)) {
330
            return $this->parseListType();
331
        }
332
333 9
        if ($this->is(Token::T_NAME)) {
334 9
            return $this->parseNamedType();
335
        }
336
337
        $message = 'Expected a type';
338
339
        if ($this->scanner->eof()) {
340
            throw $this->getParseError($message . ' but instead reached end');
341
        }
342
343
        $token = $this->scanner->peek();
344
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
345
    }
346
347 6
    private function parseDirective()
348
    {
349 6
        $this->expect(Token::T_AT);
350 6
        $name = $this->expect(Token::T_NAME)->value;
351 6
        $arguments = $this->parseArgumentList();
352
353 6
        return new Directive($name, $arguments);
354
    }
355
356 48
    private function parseDirectiveList()
357
    {
358 48
        $directives = array();
359 48
        while ($this->is(Token::T_AT)) {
360 6
            $directives[] = $this->parseDirective();
361 6
        }
362
363 48
        return $directives;
364
    }
365
366 9
    private function parseVariableDefinition()
367
    {
368 9
        $variable = $this->parseVariable();
369 9
        $this->expect(Token::T_COLON);
370 9
        $type = $this->parseType();
371 9
        $defaultValue = null;
372
373 9
        if ($this->accept(Token::T_EQUAL)) {
374 3
            $defaultValue = $this->parseValue();
375 3
        }
376
377 9
        return new VariableDefinition($variable, $type, $defaultValue);
378
    }
379
380 15 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...
381
    {
382 15
        $definitions = array();
383
384 15
        if ($this->is(Token::T_PAREN_LEFT) === false) {
385 6
            return $definitions;
386
        }
387
388 9
        $this->expect(Token::T_PAREN_LEFT);
389
390 9
        while (true) {
391 9
            if ($this->scanner->eof()) {
392
                throw $this->getParseError('Unclosed parenthesis of variable definition list');
393
            }
394
395 9
            if ($this->accept(Token::T_PAREN_RIGHT)) {
396 9
                break;
397
            }
398
399 9
            $definitions[] = $this->parseVariableDefinition();
400 9
            $this->accept(Token::T_COMMA);
401 9
        }
402
403 9
        return $definitions;
404
    }
405
406 63
    private function parseDefinition()
407
    {
408 63
        if ($this->is(Token::T_FRAGMENT)) {
409 3
            $this->expect(Token::T_FRAGMENT);
410
411 3
            $name = $this->expect(Token::T_NAME)->value;
412 3
            if ($name === 'on') {
413
                throw $this->getParseError('A fragment cannot be named "on"');
414
            }
415
416 3
            $typeCondition = $this->parseTypeCondition();
417 3
            $directives = $this->parseDirectiveList();
418 3
            $selectionSet = $this->parseSelectionSet();
419
420 3
            return new Fragment($name, $typeCondition, $directives, $selectionSet);
421
        }
422
423 63 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...
424
            $this->expect(Token::T_MUTATION);
425
            $name = $this->expect(Token::T_NAME)->value;
426
            $variables = $this->parseVariableDefinitionList();
427
            $directives = $this->parseDirectiveList();
428
            $selectionSet = $this->parseSelectionSet();
429
430
            return new OperationMutation($name, $variables, $directives, $selectionSet);
431
        }
432
433 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...
434
            $this->expect(Token::T_SUBSCRIPTION);
435
            $name = $this->expect(Token::T_NAME)->value;
436
            $variables = $this->parseVariableDefinitionList();
437
            $directives = $this->parseDirectiveList();
438
            $selectionSet = $this->parseSelectionSet();
439
440
            return new OperationSubscription($name, $variables, $directives, $selectionSet);
441
        }
442
443 63
        if ($this->is(Token::T_QUERY)) {
444 15
            $this->expect(Token::T_QUERY);
445
446 15
            $name = null;
447 15
            if ($this->is(Token::T_NAME)) {
448 12
                $name = $this->expect(Token::T_NAME)->value;
449 12
            }
450
451 15
            $variables = $this->parseVariableDefinitionList();
452 15
            $directives = $this->parseDirectiveList();
453 15
            $selectionSet = $this->parseSelectionSet();
454
455 15
            return new OperationQuery($name, $variables, $directives, $selectionSet);
456
        }
457
458 48
        if ($this->is(Token::T_BRACE_LEFT)) {
459 48
            $selectionSet = $this->parseSelectionSet();
460
461 27
            return new OperationQuery(null, array(), array(), $selectionSet);
462
        }
463
464
        $message = 'Expected a query, a query shorthand, a mutation or a subscription operation';
465
466
        if ($this->scanner->eof()) {
467
            throw $this->getParseError($message . ' but instead reached end');
468
        }
469
470
        $token = $this->scanner->peek();
471
        throw $this->getParseError($message . " but instead found \"{$token->getName()}\" with value \"{$token->value}\"");
472
    }
473
474 66
    private function parseDocument()
475
    {
476 66
        $definitions = array();
477 66
        while ($this->scanner->eof() === false) {
478 63
            $definitions[] = $this->parseDefinition();
479 42
        }
480
481 45
        return new Document($definitions);
482
    }
483
484 93
    public function parse($query)
485
    {
486 93
        $tokens = $this->lexer->lex($query);
487 66
        $scanner = new ScannerGeneric($tokens);
488 66
        $this->scanner = new ScannerTokens($scanner);
489
490 66
        return $this->parseDocument();
491
    }
492
}
493