Parser   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 638
Duplicated Lines 3.45 %

Coupling/Cohesion

Components 1
Dependencies 26

Test Coverage

Coverage 92.97%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 75
c 2
b 0
f 0
lcom 1
cbo 26
dl 22
loc 638
ccs 238
cts 256
cp 0.9297
rs 1.2052

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A parse() 0 9 1
A location() 0 12 3
A advance() 0 5 1
A peek() 0 4 1
A skip() 0 8 2
A expect() 0 10 2
A expectKeyword() 0 8 3
A unexpected() 0 6 2
A any() 11 11 2
A many() 11 11 2
A parseName() 0 7 1
A parseFragmentName() 0 7 2
C parseDocument() 0 24 7
A parseOperationDefinition() 0 22 2
A parseVariableDefinitions() 0 4 2
A parseVariableDefinition() 0 13 2
A parseVariable() 0 6 1
A parseSelectionSet() 0 9 1
A parseSelection() 0 4 2
A parseField() 0 18 3
A parseArguments() 0 4 2
A parseArgument() 0 10 1
A parseFragment() 0 22 2
A parseFragmentDefinition() 0 16 1
A parseVariableValue() 0 4 1
A parseConstValue() 0 4 1
C parseValue() 0 36 11
A parseArray() 0 10 2
A parseObject() 0 13 2
A parseObjectField() 0 15 2
A parseDirectives() 0 9 2
A parseDirective() 0 11 1
A parseType() 0 18 3
A parseNamedType() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Fubhy\GraphQL\Language;
4
5
use Fubhy\GraphQL\Language\Node\Argument;
6
use Fubhy\GraphQL\Language\Node\ArrayValue;
7
use Fubhy\GraphQL\Language\Node\BooleanValue;
8
use Fubhy\GraphQL\Language\Node\Directive;
9
use Fubhy\GraphQL\Language\Node\Document;
10
use Fubhy\GraphQL\Language\Node\EnumValue;
11
use Fubhy\GraphQL\Language\Node\Field;
12
use Fubhy\GraphQL\Language\Node\FloatValue;
13
use Fubhy\GraphQL\Language\Node\FragmentDefinition;
14
use Fubhy\GraphQL\Language\Node\FragmentSpread;
15
use Fubhy\GraphQL\Language\Node\InlineFragment;
16
use Fubhy\GraphQL\Language\Node\IntValue;
17
use Fubhy\GraphQL\Language\Node\ListType;
18
use Fubhy\GraphQL\Language\Node\Name;
19
use Fubhy\GraphQL\Language\Node\NamedType;
20
use Fubhy\GraphQL\Language\Node\NonNullType;
21
use Fubhy\GraphQL\Language\Node\ObjectField;
22
use Fubhy\GraphQL\Language\Node\ObjectValue;
23
use Fubhy\GraphQL\Language\Node\OperationDefinition;
24
use Fubhy\GraphQL\Language\Node\SelectionSet;
25
use Fubhy\GraphQL\Language\Node\StringValue;
26
use Fubhy\GraphQL\Language\Node\Variable;
27
use Fubhy\GraphQL\Language\Node\VariableDefinition;
28
29
class Parser
30
{
31
    /**
32
     * @var array
33
     */
34
    protected $options;
35
36
    /**
37
     * @var \Fubhy\GraphQL\Language\Source
38
     */
39
    protected $source;
40
41
    /**
42
     * @var \Fubhy\GraphQL\Language\Lexer
43
     */
44
    protected $lexer;
45
46
    /**
47
     * @var \Fubhy\GraphQL\Language\Token
48
     */
49
    protected $token;
50
51
    /**
52
     * @var int
53
     */
54
    protected $cursor;
55
56
    /**
57
     * Constructor.
58
     *
59
     * Returns the parser object that is used to store state throughout the
60
     * process of parsing.
61
     *
62
     * @param array $options
63
     */
64 309
    public function __construct(array $options = [])
65
    {
66 309
        $this->options = $options;
67 309
    }
68
69
    /**
70
     * @param \Fubhy\GraphQL\Language\Source $source
71
     *
72
     * @return \Fubhy\GraphQL\Language\Node\Document
73
     */
74 309
    public function parse(Source $source)
75
    {
76 309
        $this->source = $source;
77 309
        $this->lexer = new Lexer($source);
78 309
        $this->token = $this->lexer->readToken(0);
79 309
        $this->cursor = 0;
80
81 309
        return $this->parseDocument();
82
    }
83
84
    /**
85
     * Returns a location object, used to identify the place in the source that
86
     * created a given parsed object.
87
     *
88
     * @param int $start
89
     *
90
     * @return \Fubhy\GraphQL\Language\Location|null
91
     */
92 309
    protected function location($start)
93
    {
94 309
        if (!empty($this->options['noLocation'])) {
95
            return NULL;
96
        }
97
98 309
        if (!empty($this->options['noSource'])) {
99
            return new Location($start, $this->cursor);
100
        }
101
102 309
        return new Location($start, $this->cursor, $this->source);
103
    }
104
105
106
    /**
107
     * Moves the internal parser object to the next lexed token.
108
     */
109 309
    protected function advance()
110
    {
111 309
        $this->cursor = $this->token->getEnd();
112 309
        return $this->token = $this->lexer->readToken($this->cursor);
113
    }
114
115
    /**
116
     * Determines if the next token is of a given kind
117
     *
118
     * @param int $type
119
     *
120
     * @return bool
121
     */
122 309
    protected function peek($type)
123
    {
124 309
        return $this->token->getType() === $type;
125
    }
126
127
    /**
128
     * If the next token is of the given kind, return true after advancing the
129
     * parser. Otherwise, do not change the parser state and return false.
130
     *
131
     * @param int $type
132
     *
133
     * @return bool
134
     */
135 309
    protected function skip($type)
136
    {
137 309
        if ($match = ($this->token->getType() === $type)) {
138 309
            $this->advance();
139 309
        }
140
141 309
        return $match;
142
    }
143
144
    /**
145
     * If the next token is of the given kind, return that token after advancing
146
     * the parser. Otherwise, do not change the parser state and return false.
147
     *
148
     * @param int $type
149
     *
150
     * @return \Fubhy\GraphQL\Language\Token
151
     *
152
     * @throws \Exception
153
     */
154 309
    protected function expect($type)
155
    {
156 309
        if ($this->token->getType() !== $type) {
157
            throw new \Exception(sprintf('Expected %s, found %s', Token::typeToString($type), (string) $this->token));
158
        }
159
160 309
        $token = $this->token;
161 309
        $this->advance();
162 309
        return $token;
163
    }
164
165
    /**
166
     * If the next token is a keyword with the given value, return that token
167
     * after advancing the parser. Otherwise, do not change the parser state and
168
     * return false.
169
     *
170
     * @param string $value
171
     *
172
     * @return \Fubhy\GraphQL\Language\Token
173
     *
174
     * @throws \Exception
175
     */
176 36
    protected function expectKeyword($value)
177
    {
178 36
        if ($this->token->getType() !== Token::NAME_TYPE || $this->token->getValue() !== $value) {
179
            throw new \Exception(sprintf('Expected %s, found %s', $value, $this->token->getDescription()));
180
        }
181
182 36
        return $this->advance();
183
    }
184
185
    /**
186
     * Helper protected function for creating an error when an unexpected lexed token is
187
     * encountered.
188
     *
189
     * @param \Fubhy\GraphQL\Language\Token|null $atToken
190
     *
191
     * @return \Exception
192
     */
193
    protected function unexpected(Token $atToken = NULL)
194
    {
195
        $token = $atToken ?: $this->token;
196
197
        return new \Exception(sprintf('Unexpected %s', $token->getDescription()));
198
    }
199
200
    /**
201
     * Returns a possibly empty list of parse nodes, determined by the parseFn.
202
     *
203
     * This list begins with a lex token of openKind and ends with a lex token
204
     * of closeKind. Advances the parser to the next lex token after the closing
205
     * token.
206
     *
207
     * @param int $openKind
208
     * @param callable $parseFn
209
     * @param int $closeKind
210
     *
211
     * @return array
212
     */
213 9 View Code Duplication
    protected function any($openKind, $parseFn, $closeKind)
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...
214
    {
215 9
        $this->expect($openKind);
216 9
        $nodes = [];
217
218 9
        while (!$this->skip($closeKind)) {
219 9
            array_push($nodes, $parseFn($this));
220 9
        }
221
222 9
        return $nodes;
223
    }
224
225
    /**
226
     * Returns a non-empty list of parse nodes, determined by the parseFn.
227
     *
228
     * This list begins with a lex token of openKind and ends with a lex token
229
     * of closeKind. Advances the parser to the next lex token after the closing
230
     * token.
231
     *
232
     * @param int $openKind
233
     * @param callable $parseFn
234
     * @param int $closeKind
235
     *
236
     * @return array
237
     */
238 309 View Code Duplication
    protected function many($openKind, $parseFn, $closeKind)
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...
239
    {
240 309
        $this->expect($openKind);
241 309
        $nodes = [$parseFn($this)];
242
243 309
        while (!$this->skip($closeKind)) {
244 123
            array_push($nodes, $parseFn($this));
245 123
        }
246
247 309
        return $nodes;
248
    }
249
250
    /**
251
     * Converts a name lex token into a name parse node.
252
     *
253
     * @return \Fubhy\GraphQL\Language\Node\Name
254
     */
255 309
    protected function parseName()
256
    {
257 309
        $start = $this->token->getStart();
258 309
        $token = $this->expect(Token::NAME_TYPE);
259
260 309
        return new Name($token->getValue(), $this->location($start));
261
    }
262
263
    /**
264
     * Converts a fragment name lex token into a name parse node.
265
     *
266
     * @return \Fubhy\GraphQL\Language\Node\Name
267
     *
268
     * @throws \Exception
269
     */
270 33
    protected function parseFragmentName()
271
    {
272 33
        if ($this->token->getValue() === 'on') {
273
            throw $this->unexpected();
274
        }
275 33
        return $this->parseName();
276
    }
277
278
    /**
279
     * @return \Fubhy\GraphQL\Language\Node\Document
280
     *
281
     * @throws \Exception
282
     */
283 309
    protected function parseDocument()
284
    {
285 309
        $start = $this->token->getStart();
286 309
        $definitions = [];
287
288
        do {
289 309
            if ($this->peek(Token::BRACE_L_TYPE)) {
290 87
                $definitions[] = $this->parseOperationDefinition();
291 309
            } else if ($this->peek(Token::NAME_TYPE)) {
292 237
                $value = $this->token->getValue();
293 237
                if ($value === 'query' || $value === 'mutation') {
294 225
                    $definitions[] = $this->parseOperationDefinition();
295 237
                } else if ($value === 'fragment') {
296 36
                    $definitions[] = $this->parseFragmentDefinition();
297 36
                } else {
298
                    throw $this->unexpected();
299
                }
300 237
            } else {
301
                throw $this->unexpected();
302
            }
303 309
        } while (!$this->skip(Token::EOF_TYPE));
304
305 309
        return new Document($definitions, $this->location($start));
306
    }
307
308
    /**
309
     * @return \Fubhy\GraphQL\Language\Node\OperationDefinition
310
     */
311 309
    protected function parseOperationDefinition()
312
    {
313 309
        $start = $this->token->getStart();
314
315 309
        if ($this->peek(Token::BRACE_L_TYPE)) {
316 87
            return new OperationDefinition('query', NULL, [], [],
317 87
                $this->parseSelectionSet(),
318 87
                $this->location($start)
319 87
            );
320
        }
321
322 225
        $operationToken = $this->expect(Token::NAME_TYPE);
323 225
        $operation = $operationToken->getValue();
324
325 225
        return new OperationDefinition($operation,
326 225
            $this->parseName(),
327 225
            $this->parseVariableDefinitions(),
328 225
            $this->parseDirectives(),
329 225
            $this->parseSelectionSet(),
330 225
            $this->location($start)
331 225
        );
332
    }
333
334
    /**
335
     * @return \Fubhy\GraphQL\Language\Node\VariableDefinition[]
336
     */
337 225
    protected function parseVariableDefinitions()
338
    {
339 225
      return $this->peek(Token::PAREN_L_TYPE) ? $this->many(Token::PAREN_L_TYPE, [$this, 'parseVariableDefinition'], Token::PAREN_R_TYPE) : [];
340
    }
341
342
    /**
343
     * @return \Fubhy\GraphQL\Language\Node\VariableDefinition
344
     */
345 72
    protected function parseVariableDefinition()
346
    {
347 72
        $start = $this->token->getStart();
348 72
        $variable = $this->parseVariable();
349 72
        $this->expect(Token::COLON_TYPE);
350
351 72
        return new VariableDefinition(
352 72
            $variable,
353 72
            $this->parseType(),
354 72
            $this->skip(Token::EQUALS_TYPE) ? $this->parseValue(TRUE) : NULL,
355 72
            $this->location($start)
356 72
        );
357
    }
358
359
    /**
360
     * @return \Fubhy\GraphQL\Language\Node\Variable
361
     */
362 78
    protected function parseVariable() {
363 78
        $start = $this->token->getStart();
364 78
        $this->expect(Token::DOLLAR_TYPE);
365
366 78
        return new Variable($this->parseName(), $this->location($start));
367
    }
368
369
    /**
370
     * @return \Fubhy\GraphQL\Language\Node\SelectionSet
371
     */
372 309
    protected function parseSelectionSet()
373
    {
374 309
        $start = $this->token->getStart();
375
376 309
        return new SelectionSet(
377 309
            $this->many(Token::BRACE_L_TYPE, [$this, 'parseSelection'], Token::BRACE_R_TYPE),
378 309
            $this->location($start)
379 309
        );
380
    }
381
382
    /**
383
     * @return \Fubhy\GraphQL\Language\Node\SelectionInterface
384
     */
385 309
    protected function parseSelection()
386
    {
387 309
        return $this->peek(Token::SPREAD_TYPE) ? $this->parseFragment() : $this->parseField();
388
    }
389
390
    /**
391
     * @return \Fubhy\GraphQL\Language\Node\Field
392
     */
393 309
    protected function parseField()
394
    {
395 309
        $start = $this->token->getStart();
396 309
        $name = $this->parseName();
397 309
        $alias = NULL;
398
399 309
        if ($this->skip(Token::COLON_TYPE)) {
400 36
            $alias = $name;
401 36
            $name = $this->parseName();
402 36
        }
403
404 309
        return new Field($name, $alias,
405 309
            $this->parseArguments(),
406 309
            $this->parseDirectives(),
407 309
            $this->peek(Token::BRACE_L_TYPE) ? $this->parseSelectionSet() : NULL,
408 309
            $this->location($start)
409 309
        );
410
    }
411
412
    /**
413
     * @return \Fubhy\GraphQL\Language\Node\Argument[]
414
     */
415 309
    protected function parseArguments()
416
    {
417 309
        return $this->peek(Token::PAREN_L_TYPE) ? $this->many(Token::PAREN_L_TYPE, [$this, 'parseArgument'], Token::PAREN_R_TYPE) : [];
418
    }
419
420
    /**
421
     * @return \Fubhy\GraphQL\Language\Node\Argument
422
     */
423 168
    protected function parseArgument()
424
    {
425 168
        $start = $this->token->getStart();
426 168
        $name = $this->parseName();
427
428 168
        $this->expect(Token::COLON_TYPE);
429 168
        $value = $this->parseValue(FALSE);
430
431 168
        return new Argument($name, $value, $this->location($start));
432
    }
433
434
    /**
435
     * Corresponds to both FragmentSpread and InlineFragment in the spec.
436
     *
437
     * @return \Fubhy\GraphQL\Language\Node\FragmentSpread|\Fubhy\GraphQL\Language\Node\InlineFragment
438
     */
439 42
    protected function parseFragment()
440
    {
441 42
        $start = $this->token->getStart();
442 42
        $this->expect(Token::SPREAD_TYPE);
443
444 42
        if ($this->token->getValue() === 'on') {
445 18
            $this->advance();
446
447 18
            return new InlineFragment(
448 18
                $this->parseNamedType(),
449 18
                $this->parseDirectives(),
450 18
                $this->parseSelectionSet(),
451 18
                $this->location($start)
452 18
            );
453
        }
454
455 33
        return new FragmentSpread(
456 33
            $this->parseFragmentName(),
457 33
            $this->parseDirectives(),
458 33
            $this->location($start)
459 33
        );
460
    }
461
462
    /**
463
     * @return \Fubhy\GraphQL\Language\Node\FragmentDefinition
464
     */
465 36
    protected function parseFragmentDefinition()
466
    {
467 36
        $start = $this->token->getStart();
468 36
        $this->expectKeyword('fragment');
469 36
        $name = $this->parseName();
470 36
        $this->expectKeyword('on');
471 36
        $typeCondition = $this->parseNamedType();
472
473 36
        return new FragmentDefinition(
474 36
            $name,
475 36
            $typeCondition,
476 36
            $this->parseDirectives(),
477 36
            $this->parseSelectionSet(),
478 36
            $this->location($start)
479 36
        );
480
    }
481
482
    /**
483
     * @return \Fubhy\GraphQL\Language\Node\ValueInterface
484
     */
485 9
    protected function parseVariableValue()
486
    {
487 9
        return $this->parseValue(FALSE);
488
    }
489
490
    /**
491
     * @return \Fubhy\GraphQL\Language\Node\ValueInterface
492
     */
493
    protected function parseConstValue()
494
    {
495
        return $this->parseValue(TRUE);
496
    }
497
498
    /**
499
     * @param bool $isConst
500
     *
501
     * @return \Fubhy\GraphQL\Language\Node\ValueInterface
502
     *
503
     * @throws \Exception
504
     */
505 168
    protected function parseValue($isConst)
506
    {
507 168
        $start = $this->token->getStart();
508 168
        $token = $this->token;
509
510 168
        switch ($this->token->getType()) {
511 168
            case Token::BRACKET_L_TYPE:
512 9
                return $this->parseArray($isConst);
513 168
            case Token::BRACE_L_TYPE:
514 9
                return $this->parseObject($isConst);
515 168
            case Token::INT_TYPE:
516 12
                $this->advance();
517 12
                return new IntValue($token->getValue(), $this->location($start));
518 165
            case Token::FLOAT_TYPE:
519
                $this->advance();
520
                return new FloatValue($token->getValue(), $this->location($start));
521 165
            case Token::STRING_TYPE:
522 75
                $this->advance();
523 75
                return new StringValue($token->getValue(), $this->location($start));
524 105
            case Token::NAME_TYPE:
525 30
                $this->advance();
526 30
                switch ($value = $token->getValue()) {
527 30
                    case 'true':
528 30
                    case 'false':
529 24
                        return new BooleanValue($value === 'true', $this->location($start));
530 6
                }
531 6
                return new EnumValue($value, $this->location($start));
532 78
            case Token::DOLLAR_TYPE:
533 78
                if (!$isConst) {
534 78
                    return $this->parseVariable();
535
                }
536
                break;
537
        }
538
539
        throw $this->unexpected();
540
    }
541
542
    /**
543
     * @param bool $isConst
544
     *
545
     * @return \Fubhy\GraphQL\Language\Node\ArrayValue
546
     */
547 9
    protected function parseArray($isConst)
548
    {
549 9
        $start = $this->token->getStart();
550 9
        $item = $isConst ? 'parseConstValue' : 'parseVariableValue';
551
552 9
        return new ArrayValue(
553 9
            $this->any(Token::BRACKET_L_TYPE, [$this, $item], Token::BRACKET_R_TYPE),
554 9
            $this->location($start)
555 9
        );
556
    }
557
558
    /**
559
     * @param bool $isConst
560
     *
561
     * @return \Fubhy\GraphQL\Language\Node\ObjectValue
562
     */
563 9
    protected function parseObject($isConst)
564
    {
565 9
        $start = $this->token->getStart();
566 9
        $this->expect(Token::BRACE_L_TYPE);
567
568 9
        $fieldNames = [];
569 9
        $fields = [];
570 9
        while (!$this->skip(Token::BRACE_R_TYPE)) {
571 9
            array_push($fields, $this->parseObjectField($isConst, $fieldNames));
572 9
        }
573
574 9
        return new ObjectValue($fields, $this->location($start));
575
    }
576
577
    /**
578
     * @param bool $isConst
579
     * @param array $fieldNames
580
     *
581
     * @return \Fubhy\GraphQL\Language\Node\ObjectField
582
     *
583
     * @throws \Exception
584
     */
585 9
    protected function parseObjectField($isConst, &$fieldNames)
586
    {
587 9
        $start = $this->token->getStart();
588 9
        $name = $this->parseName();
589 9
        $value = $name->get('value');
590
591 9
        if (array_key_exists($value, $fieldNames)) {
592
            throw new \Exception(sprintf('Duplicate input object field %s.', $value));
593
        }
594
595 9
        $fieldNames[$value] = TRUE;
596 9
        $this->expect(Token::COLON_TYPE);
597
598 9
        return new ObjectField($name, $this->parseValue($isConst), $this->location($start));
599
    }
600
601
    /**
602
     * @return \Fubhy\GraphQL\Language\Node\Directive[]
603
     */
604 309
    protected function parseDirectives()
605
    {
606 309
        $directives = [];
607 309
        while ($this->peek(Token::AT_TYPE)) {
608 15
            array_push($directives, $this->parseDirective());
609 15
        }
610
611 309
        return $directives;
612
    }
613
614
    /**
615
     * @return \Fubhy\GraphQL\Language\Node\Directive
616
     *
617
     * @throws \Exception
618
     */
619 15
    protected function parseDirective()
620
    {
621 15
        $start = $this->token->getStart();
622 15
        $this->expect(Token::AT_TYPE);
623
624 15
        return new Directive(
625 15
            $this->parseName(),
626 15
            $this->parseArguments(),
627 15
            $this->location($start)
628 15
        );
629
    }
630
631
    /**
632
     * Handles the type: TypeName, ListType, and NonNullType parsing rules.
633
     *
634
     * @return \Fubhy\GraphQL\Language\Node\TypeInterface
635
     *
636
     * @throws \Exception
637
     */
638 72
    protected function parseType()
639
    {
640 72
        $start = $this->token->getStart();
641
642 72
        if ($this->skip(Token::BRACKET_L_TYPE)) {
643 36
            $type = $this->parseType();
644 36
            $this->expect(Token::BRACKET_R_TYPE);
645 36
            $type = new ListType($type, $this->location($start));
646 36
        } else {
647 72
            $type = $this->parseNamedType();
648
        }
649
650 72
        if ($this->skip(Token::BANG_TYPE)) {
651 45
            return new NonNullType($type, $this->location($start));
652
        }
653
654 45
        return $type;
655
    }
656
657
    /**
658
     * @return \Fubhy\GraphQL\Language\Node\NamedType
659
     */
660 108
    protected function parseNamedType()
661
    {
662 108
        $start = $this->token->getStart();
663
664 108
        return new NamedType($this->parseName(), $this->location($start));
665
    }
666
}
667