Completed
Pull Request — master (#158)
by Sam
08:49
created

Parser   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 463
Duplicated Lines 9.94 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 93.39%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 97
lcom 1
cbo 17
dl 46
loc 463
ccs 226
cts 242
cp 0.9339
rs 1.5789
c 2
b 1
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 41 9
A init() 13 13 1
B parseOperation() 0 32 5
B parseBody() 0 26 5
B parseVariables() 0 55 7
A expectMulti() 0 8 2
B parseVariableReference() 0 21 5
A findVariable() 0 11 3
A parseFragmentReference() 0 9 1
A eatIdentifierToken() 0 9 1
C parseBodyItem() 16 38 13
A parseArgumentList() 0 15 3
A parseArgument() 8 8 1
A parseDirectiveList() 0 11 2
A parseDirective() 9 9 2
D parseValue() 0 25 10
A parseList() 0 15 4
C parseListValue() 0 23 10
A parseObject() 0 19 4
A parseFragment() 0 15 2
A eat() 0 8 2
A eatMulti() 0 8 2
A matchMulti() 0 10 3

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
* This file is a part of graphql-youshido project.
4
*
5
* @author Portey Vasil <[email protected]>
6
* created: 11/23/15 1:22 AM
7
*/
8
9
namespace Youshido\GraphQL\Parser;
10
11
12
use Youshido\GraphQL\Exception\Parser\SyntaxErrorException;
13
use Youshido\GraphQL\Parser\Ast\Argument;
14
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputList;
15
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputObject;
16
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Literal;
17
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Variable;
18
use Youshido\GraphQL\Parser\Ast\ArgumentValue\VariableReference;
19
use Youshido\GraphQL\Parser\Ast\Directive;
20
use Youshido\GraphQL\Parser\Ast\Field;
21
use Youshido\GraphQL\Parser\Ast\Fragment;
22
use Youshido\GraphQL\Parser\Ast\FragmentReference;
23
use Youshido\GraphQL\Parser\Ast\Mutation;
24
use Youshido\GraphQL\Parser\Ast\Query;
25
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
26
27
class Parser extends Tokenizer
28
{
29
30
    /** @var array */
31
    private $data = [];
32
33 98
    public function parse($source = null)
34
    {
35 98
        $this->init($source);
36
37 98
        while (!$this->end()) {
38 97
            $tokenType = $this->peek()->getType();
39
40
            switch ($tokenType) {
41 97
                case Token::TYPE_LBRACE:
42 55
                    foreach ($this->parseBody() as $query) {
43 50
                        $this->data['queries'][] = $query;
44
                    }
45
46 51
                    break;
47 46
                case Token::TYPE_QUERY:
48 25
                    $query = $this->parseOperation(Token::TYPE_QUERY);
49 25
                    if ($query instanceof Query) {
50 23
                        $this->data['queries'][] = $query;
51
                    }
52
53 25
                    break;
54 26
                case Token::TYPE_MUTATION:
55 17
                    $mutation = $this->parseOperation(Token::TYPE_MUTATION);
56 12
                    if ($mutation instanceof Mutation) {
57 12
                        $this->data['mutations'][] = $mutation;
58
                    }
59
60 12
                    break;
61
62 9
                case Token::TYPE_FRAGMENT:
63 8
                    $this->data['fragments'][] = $this->parseFragment();
64
65 8
                    break;
66
67
                default:
68 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
69
            }
70
        }
71
72 88
        return $this->data;
73
    }
74
75 98 View Code Duplication
    private function init($source = null)
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...
76
    {
77 98
        $this->initTokenizer($source);
78
79 98
        $this->data = [
80
            'queries'            => [],
81
            'mutations'          => [],
82
            'fragments'          => [],
83
            'fragmentReferences' => [],
84
            'variables'          => [],
85
            'variableReferences' => [],
86
        ];
87 98
    }
88
89 42
    protected function parseOperation($type = Token::TYPE_QUERY)
90
    {
91 42
        $operation  = null;
92 42
        $directives = [];
93
94 42
        if ($this->matchMulti([Token::TYPE_QUERY, Token::TYPE_MUTATION])) {
95 42
            $this->lex();
96
97 42
            $this->eat(Token::TYPE_IDENTIFIER);
98
99 42
            if ($this->match(Token::TYPE_LPAREN)) {
100 13
                $this->parseVariables();
101
            }
102
103 42
            if ($this->match(Token::TYPE_AT)) {
104
                $directives = $this->parseDirectiveList();
105
            }
106
107
        }
108
109 42
        $this->lex();
110
111 42
        if (!$this->match(Token::TYPE_RBRACE)) {
112 40
            $operation = $this->parseBodyItem($type, true);
113 35
            $operation->setDirectives($directives);
114
        }
115
116 37
        $this->expect(Token::TYPE_RBRACE);
117
118
119 37
        return $operation;
120
    }
121
122 76
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
123
    {
124 76
        $fields = [];
125
126 76
        $this->lex();
127
128 76
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
129 75
            $this->eatMulti([Token::TYPE_COMMA]);
130
131 75
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
132 10
                $this->lex();
133
134 10
                if ($this->eat(Token::TYPE_ON)) {
135 3
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
136
                } else {
137 8
                    $fields[] = $this->parseFragmentReference();
138
                }
139
            } else {
140 74
                $fields[] = $this->parseBodyItem($token, $highLevel);
141
            }
142
        }
143
144 72
        $this->expect(Token::TYPE_RBRACE);
145
146 72
        return $fields;
147
    }
148
149 13
    protected function parseVariables()
150
    {
151 13
        $this->eat(Token::TYPE_LPAREN);
152
153 13
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
154 13
            $this->eat(Token::TYPE_COMMA);
155
156 13
            $variableToken = $this->eat(Token::TYPE_VARIABLE);
157 13
            $nameToken     = $this->eatIdentifierToken();
158 13
            $this->eat(Token::TYPE_COLON);
159
160 13
            $isArray              = false;
161 13
            $arrayElementNullable = true;
162
163 13
            if ($this->match(Token::TYPE_LSQUARE_BRACE)) {
164 3
                $isArray = true;
165
166 3
                $this->eat(Token::TYPE_LSQUARE_BRACE);
167 3
                $type = $this->eatIdentifierToken()->getData();
168
169 3
                if ($this->match(Token::TYPE_REQUIRED)) {
170 2
                    $arrayElementNullable = false;
171 2
                    $this->eat(Token::TYPE_REQUIRED);
172
                }
173
174 3
                $this->eat(Token::TYPE_RSQUARE_BRACE);
175
            } else {
176 12
                $type = $this->eatIdentifierToken()->getData();
177
            }
178
179 13
            $required = false;
180 13
            if ($this->match(Token::TYPE_REQUIRED)) {
181 2
                $required = true;
182 2
                $this->eat(Token::TYPE_REQUIRED);
183
            }
184
185 13
            $variable = new Variable(
186 13
                $nameToken->getData(),
187 13
                $type,
188 13
                $required,
189 13
                $isArray,
190 13
                $arrayElementNullable,
191 13
                new Location($variableToken->getLine(), $variableToken->getColumn())
192
            );
193
194 13
            if ($this->match(Token::TYPE_EQUAL)) {
195 1
                $this->eat(Token::TYPE_EQUAL);
196 1
                $variable->setDefaultValue($this->parseValue());
197
            }
198
199 13
            $this->data['variables'][] = $variable;
200
        }
201
202 13
        $this->expect(Token::TYPE_RPAREN);
203 13
    }
204
205 93
    protected function expectMulti($types)
206
    {
207 93
        if ($this->matchMulti($types)) {
208 93
            return $this->lex();
209
        }
210
211 2
        throw $this->createUnexpectedException($this->peek());
212
    }
213
214 12
    protected function parseVariableReference()
215
    {
216 12
        $startToken = $this->expectMulti([Token::TYPE_VARIABLE]);
217
218 12
        if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER) || $this->match(Token::TYPE_QUERY)) {
219 12
            $name = $this->lex()->getData();
220
221 12
            $variable = $this->findVariable($name);
222 12
            if ($variable) {
223 12
                $variable->setUsed(true);
224
            }
225
226 12
            $variableReference = new VariableReference($name, $variable, new Location($startToken->getLine(), $startToken->getColumn()));
227
228 12
            $this->data['variableReferences'][] = $variableReference;
229
230 12
            return $variableReference;
231
        }
232
233
        throw $this->createUnexpectedException($this->peek());
234
    }
235
236 12
    protected function findVariable($name)
237
    {
238 12
        foreach ((array) $this->data['variables'] as $variable) {
239
            /** @var $variable Variable */
240 12
            if ($variable->getName() === $name) {
241 12
                return $variable;
242
            }
243
        }
244
245
        return null;
246
    }
247
248 8
    protected function parseFragmentReference()
249
    {
250 8
        $nameToken         = $this->eatIdentifierToken();
251 8
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
252
253 8
        $this->data['fragmentReferences'][] = $fragmentReference;
254
255 8
        return $fragmentReference;
256
    }
257
258 93
    protected function eatIdentifierToken()
259
    {
260 93
        return $this->expectMulti([
261 93
            Token::TYPE_IDENTIFIER,
262 93
            Token::TYPE_MUTATION,
263 93
            Token::TYPE_QUERY,
264 93
            Token::TYPE_FRAGMENT,
265
        ]);
266
    }
267
268 93
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
269
    {
270 93
        $nameToken = $this->eatIdentifierToken();
271 93
        $alias     = null;
272
273 93
        if ($this->eat(Token::TYPE_COLON)) {
274 17
            $alias     = $nameToken->getData();
275 17
            $nameToken = $this->eatIdentifierToken();
276
        }
277
278 93
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
279 93
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
280 86
        $directives   = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
281
282 86
        if ($this->match(Token::TYPE_LBRACE)) {
283 57
            $fields = $this->parseBody($type === Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
284
285 56
            if (!$fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
286 3
                throw $this->createUnexpectedTokenTypeException($this->lookAhead->getType());
287
            }
288
289 55 View Code Duplication
            if ($type === Token::TYPE_QUERY) {
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...
290 53
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
291 6
            } elseif ($type === Token::TYPE_TYPED_FRAGMENT) {
292 3
                return new TypedFragmentReference($nameToken->getData(), $fields, $directives, $bodyLocation);
293
            } else {
294 3
                return new Mutation($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
295
            }
296 View Code Duplication
        } else {
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...
297 84
            if ($highLevel && $type === Token::TYPE_MUTATION) {
298 10
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
299 24
            } elseif ($highLevel && $type === Token::TYPE_QUERY) {
300 24
                return new Query($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
301
            }
302
303 55
            return new Field($nameToken->getData(), $alias, $arguments, $directives, $bodyLocation);
304
        }
305
    }
306
307 56
    protected function parseArgumentList()
308
    {
309 56
        $args = [];
310
311 56
        $this->expect(Token::TYPE_LPAREN);
312
313 56
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
314 56
            $this->eat(Token::TYPE_COMMA);
315 56
            $args[] = $this->parseArgument();
316
        }
317
318 50
        $this->expect(Token::TYPE_RPAREN);
319
320 49
        return $args;
321
    }
322
323 56 View Code Duplication
    protected function parseArgument()
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...
324
    {
325 56
        $nameToken = $this->eatIdentifierToken();
326 56
        $this->expect(Token::TYPE_COLON);
327 55
        $value = $this->parseValue();
328
329 51
        return new Argument($nameToken->getData(), $value, new Location($nameToken->getLine(), $nameToken->getColumn()));
0 ignored issues
show
Bug introduced by
It seems like $value defined by $this->parseValue() on line 327 can also be of type array; however, Youshido\GraphQL\Parser\...Argument::__construct() does only seem to accept object<Youshido\GraphQL\...erfaces\ValueInterface>, 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...
330
    }
331
332
    protected function parseDirectiveList()
333
    {
334
        $directives = [];
335
336
        while ($this->match(Token::TYPE_AT)) {
337
            $directives[] = $this->parseDirective();
338
            $this->eat(Token::TYPE_COMMA);
339
        }
340
341
        return $directives;
342
    }
343
344 View Code Duplication
    protected function parseDirective()
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...
345
    {
346
        $this->expect(Token::TYPE_AT);
347
348
        $nameToken = $this->eatIdentifierToken();
349
        $args      = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
350
351
        return new Directive($nameToken->getData(), $args, new Location($nameToken->getLine(), $nameToken->getColumn()));
352
    }
353
354
    /**
355
     * @return array|InputList|InputObject|Literal|VariableReference
356
     *
357
     * @throws SyntaxErrorException
358
     */
359 55
    protected function parseValue()
360
    {
361 55
        switch ($this->lookAhead->getType()) {
362 55
            case Token::TYPE_LSQUARE_BRACE:
363 7
                return $this->parseList();
364
365 48
            case Token::TYPE_LBRACE:
366 8
                return $this->parseObject();
367
368 40
            case Token::TYPE_VARIABLE:
369 12
                return $this->parseVariableReference();
370
371 33
            case Token::TYPE_NUMBER:
372 25
            case Token::TYPE_STRING:
373 11
            case Token::TYPE_IDENTIFIER:
374 8
            case Token::TYPE_NULL:
375 5
            case Token::TYPE_TRUE:
376 1
            case Token::TYPE_FALSE:
377 32
                $token = $this->lex();
378
379 32
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
380
        }
381
382 1
        throw $this->createUnexpectedException($this->lookAhead);
383
    }
384
385 9
    protected function parseList($createType = true)
386
    {
387 9
        $startToken = $this->eat(Token::TYPE_LSQUARE_BRACE);
388
389 9
        $list = [];
390 9
        while (!$this->match(Token::TYPE_RSQUARE_BRACE) && !$this->end()) {
391 9
            $list[] = $this->parseListValue();
392
393 8
            $this->eat(Token::TYPE_COMMA);
394
        }
395
396 8
        $this->expect(Token::TYPE_RSQUARE_BRACE);
397
398 8
        return $createType ? new InputList($list, new Location($startToken->getLine(), $startToken->getColumn())) : $list;
399
    }
400
401 15
    protected function parseListValue()
402
    {
403 15
        switch ($this->lookAhead->getType()) {
404 15
            case Token::TYPE_NUMBER:
405 11
            case Token::TYPE_STRING:
406 9
            case Token::TYPE_TRUE:
407 9
            case Token::TYPE_FALSE:
408 9
            case Token::TYPE_NULL:
409 6
            case Token::TYPE_IDENTIFIER:
410 14
                return $this->expect($this->lookAhead->getType())->getData();
411
412 5
            case Token::TYPE_VARIABLE:
413
                return $this->parseVariableReference();
414
415 5
            case Token::TYPE_LBRACE:
416 4
                return $this->parseObject(true);
417
418 3
            case Token::TYPE_LSQUARE_BRACE:
419 2
                return $this->parseList(false);
420
        }
421
422 1
        throw new SyntaxErrorException('Can\'t parse argument', $this->getLocation());
423
    }
424
425 9
    protected function parseObject($createType = true)
426
    {
427 9
        $startToken = $this->eat(Token::TYPE_LBRACE);
428
429 9
        $object = [];
430 9
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
431 9
            $key = $this->expectMulti([Token::TYPE_STRING, Token::TYPE_IDENTIFIER])->getData();
432 9
            $this->expect(Token::TYPE_COLON);
433 9
            $value = $this->parseListValue();
434
435 9
            $this->eat(Token::TYPE_COMMA);
436
437 9
            $object[$key] = $value;
438
        }
439
440 7
        $this->eat(Token::TYPE_RBRACE);
441
442 7
        return $createType ? new InputObject($object, new Location($startToken->getLine(), $startToken->getColumn())) : $object;
443
    }
444
445 8
    protected function parseFragment()
446
    {
447 8
        $this->lex();
448 8
        $nameToken = $this->eatIdentifierToken();
449
450 8
        $this->eat(Token::TYPE_ON);
451
452 8
        $model = $this->eatIdentifierToken();
453
454 8
        $directives = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
455
456 8
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
457
458 8
        return new Fragment($nameToken->getData(), $model->getData(), $directives, $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
459
    }
460
461 95
    protected function eat($type)
462
    {
463 95
        if ($this->match($type)) {
464 48
            return $this->lex();
465
        }
466
467 93
        return null;
468
    }
469
470 75
    protected function eatMulti($types)
471
    {
472 75
        if ($this->matchMulti($types)) {
473
            return $this->lex();
474
        }
475
476 75
        return null;
477
    }
478
479 95
    protected function matchMulti($types)
480
    {
481 95
        foreach ((array) $types as $type) {
482 95
            if ($this->peek()->getType() === $type) {
483 95
                return true;
484
            }
485
        }
486
487 76
        return false;
488
    }
489
}
490