Parser   F
last analyzed

Complexity

Total Complexity 98

Size/Duplication

Total Lines 468
Duplicated Lines 9.83 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 93.59%

Importance

Changes 0
Metric Value
wmc 98
lcom 1
cbo 17
dl 46
loc 468
ccs 263
cts 281
cp 0.9359
rs 2
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A expectMulti() 0 8 2
A parseFragmentReference() 0 9 1
A parseArgument() 8 8 1
A parseDirective() 9 9 2
A parseFragment() 0 15 2
A eat() 0 8 2
A eatMulti() 0 8 2
B parse() 0 41 9
A init() 13 13 1
B parseOperation() 0 37 6
A parseBody() 0 26 5
B parseVariables() 0 55 7
A parseVariableReference() 0 21 5
A findVariable() 0 11 3
A eatIdentifierToken() 0 9 1
C parseBodyItem() 16 38 13
A parseDirectiveList() 0 11 2
B parseValue() 0 25 10
A parseList() 0 15 4
B parseListValue() 0 23 10
A parseObject() 0 19 4
A matchMulti() 0 10 3
A parseArgumentList() 0 15 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 107
    public function parse($source = null)
34
    {
35 107
        $this->init($source);
36
37 107
        while (!$this->end()) {
38 106
            $tokenType = $this->peek()->getType();
39
40
            switch ($tokenType) {
41 106
                case Token::TYPE_LBRACE:
42 57
                    foreach ($this->parseBody() as $query) {
43 52
                        $this->data['queries'][] = $query;
44 53
                    }
45
46 53
                    break;
47 54
                case Token::TYPE_QUERY:
48 32
                    $queries = $this->parseOperation(Token::TYPE_QUERY);
49 32
                    foreach ($queries as $query) {
50 30
                        $this->data['queries'][] = $query;
51 32
                    }
52
53 32
                    break;
54 30
                case Token::TYPE_MUTATION:
55 17
                    $mutations = $this->parseOperation(Token::TYPE_MUTATION);
56 12
                    foreach ($mutations as $query) {
57 12
                        $this->data['mutations'][] = $query;
58 12
                    }
59
60 12
                    break;
61
62 13
                case Token::TYPE_FRAGMENT:
63 12
                    $this->data['fragments'][] = $this->parseFragment();
64
65 12
                    break;
66
67 1
                default:
68 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
69 1
            }
70 96
        }
71
72 97
        return $this->data;
73
    }
74
75 107 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 107
        $this->initTokenizer($source);
78
79 107
        $this->data = [
80 107
            'queries'            => [],
81 107
            'mutations'          => [],
82 107
            'fragments'          => [],
83 107
            'fragmentReferences' => [],
84 107
            'variables'          => [],
85 107
            'variableReferences' => [],
86
        ];
87 107
    }
88
89 49
    protected function parseOperation($type = Token::TYPE_QUERY)
90
    {
91 49
        $operation  = null;
0 ignored issues
show
Unused Code introduced by
$operation is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
92 49
        $directives = [];
93
94 49
        if ($this->matchMulti([Token::TYPE_QUERY, Token::TYPE_MUTATION])) {
95 49
            $this->lex();
96
97 49
            $this->eat(Token::TYPE_IDENTIFIER);
98
99 49
            if ($this->match(Token::TYPE_LPAREN)) {
100 13
                $this->parseVariables();
101 13
            }
102
103 49
            if ($this->match(Token::TYPE_AT)) {
104
                $directives = $this->parseDirectiveList();
105
            }
106
107 49
        }
108
109 49
        $this->lex();
110
111 49
        $fields = [];
112
113 49
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
114 47
            $this->eatMulti([Token::TYPE_COMMA]);
115
116 47
            $operation = $this->parseBodyItem($type, true);
117 42
            $operation->setDirectives($directives);
118
119 42
            $fields[] = $operation;
120 42
        }
121
122 44
        $this->expect(Token::TYPE_RBRACE);
123
124 44
        return $fields;
125
    }
126
127 85
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
128
    {
129 85
        $fields = [];
130
131 85
        $this->lex();
132
133 85
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
134 84
            $this->eatMulti([Token::TYPE_COMMA]);
135
136 84
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
137 14
                $this->lex();
138
139 14
                if ($this->eat(Token::TYPE_ON)) {
140 4
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
141 4
                } else {
142 12
                    $fields[] = $this->parseFragmentReference();
143
                }
144 14
            } else {
145 83
                $fields[] = $this->parseBodyItem($token, $highLevel);
146
            }
147 80
        }
148
149 81
        $this->expect(Token::TYPE_RBRACE);
150
151 81
        return $fields;
152
    }
153
154 13
    protected function parseVariables()
155
    {
156 13
        $this->eat(Token::TYPE_LPAREN);
157
158 13
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
159 13
            $this->eat(Token::TYPE_COMMA);
160
161 13
            $variableToken = $this->eat(Token::TYPE_VARIABLE);
162 13
            $nameToken     = $this->eatIdentifierToken();
163 13
            $this->eat(Token::TYPE_COLON);
164
165 13
            $isArray              = false;
166 13
            $arrayElementNullable = true;
167
168 13
            if ($this->match(Token::TYPE_LSQUARE_BRACE)) {
169 3
                $isArray = true;
170
171 3
                $this->eat(Token::TYPE_LSQUARE_BRACE);
172 3
                $type = $this->eatIdentifierToken()->getData();
173
174 3
                if ($this->match(Token::TYPE_REQUIRED)) {
175 2
                    $arrayElementNullable = false;
176 2
                    $this->eat(Token::TYPE_REQUIRED);
177 2
                }
178
179 3
                $this->eat(Token::TYPE_RSQUARE_BRACE);
180 3
            } else {
181 12
                $type = $this->eatIdentifierToken()->getData();
182
            }
183
184 13
            $required = false;
185 13
            if ($this->match(Token::TYPE_REQUIRED)) {
186 2
                $required = true;
187 2
                $this->eat(Token::TYPE_REQUIRED);
188 2
            }
189
190 13
            $variable = new Variable(
191 13
                $nameToken->getData(),
192 13
                $type,
193 13
                $required,
194 13
                $isArray,
195 13
                $arrayElementNullable,
196 13
                new Location($variableToken->getLine(), $variableToken->getColumn())
197 13
            );
198
199 13
            if ($this->match(Token::TYPE_EQUAL)) {
200 1
                $this->eat(Token::TYPE_EQUAL);
201 1
                $variable->setDefaultValue($this->parseValue());
202 1
            }
203
204 13
            $this->data['variables'][] = $variable;
205 13
        }
206
207 13
        $this->expect(Token::TYPE_RPAREN);
208 13
    }
209
210 102
    protected function expectMulti($types)
211
    {
212 102
        if ($this->matchMulti($types)) {
213 102
            return $this->lex();
214
        }
215
216 2
        throw $this->createUnexpectedException($this->peek());
217
    }
218
219 12
    protected function parseVariableReference()
220
    {
221 12
        $startToken = $this->expectMulti([Token::TYPE_VARIABLE]);
222
223 12
        if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER) || $this->match(Token::TYPE_QUERY)) {
224 12
            $name = $this->lex()->getData();
225
226 12
            $variable = $this->findVariable($name);
227 12
            if ($variable) {
228 12
                $variable->setUsed(true);
229 12
            }
230
231 12
            $variableReference = new VariableReference($name, $variable, new Location($startToken->getLine(), $startToken->getColumn()));
232
233 12
            $this->data['variableReferences'][] = $variableReference;
234
235 12
            return $variableReference;
236
        }
237
238
        throw $this->createUnexpectedException($this->peek());
239
    }
240
241 12
    protected function findVariable($name)
242
    {
243 12
        foreach ((array) $this->data['variables'] as $variable) {
244
            /** @var $variable Variable */
245 12
            if ($variable->getName() === $name) {
246 12
                return $variable;
247
            }
248 3
        }
249
250
        return null;
251
    }
252
253 12
    protected function parseFragmentReference()
254
    {
255 12
        $nameToken         = $this->eatIdentifierToken();
256 12
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
257
258 12
        $this->data['fragmentReferences'][] = $fragmentReference;
259
260 12
        return $fragmentReference;
261
    }
262
263 102
    protected function eatIdentifierToken()
264
    {
265 102
        return $this->expectMulti([
266 102
            Token::TYPE_IDENTIFIER,
267 102
            Token::TYPE_MUTATION,
268 102
            Token::TYPE_QUERY,
269 102
            Token::TYPE_FRAGMENT,
270 102
        ]);
271
    }
272
273 102
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
274
    {
275 102
        $nameToken = $this->eatIdentifierToken();
276 102
        $alias     = null;
277
278 102
        if ($this->eat(Token::TYPE_COLON)) {
279 20
            $alias     = $nameToken->getData();
280 20
            $nameToken = $this->eatIdentifierToken();
281 20
        }
282
283 102
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
284 102
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
285 95
        $directives   = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
286
287 95
        if ($this->match(Token::TYPE_LBRACE)) {
288 66
            $fields = $this->parseBody($type === Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
289
290 65
            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...
291 3
                throw $this->createUnexpectedTokenTypeException($this->lookAhead->getType());
292
            }
293
294 64 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...
295 62
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
296 7
            } elseif ($type === Token::TYPE_TYPED_FRAGMENT) {
297 4
                return new TypedFragmentReference($nameToken->getData(), $fields, $directives, $bodyLocation);
298
            } else {
299 3
                return new Mutation($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
300
            }
301 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...
302 93
            if ($highLevel && $type === Token::TYPE_MUTATION) {
303 10
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
304 84
            } elseif ($highLevel && $type === Token::TYPE_QUERY) {
305 24
                return new Query($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
306
            }
307
308 64
            return new Field($nameToken->getData(), $alias, $arguments, $directives, $bodyLocation);
309
        }
310
    }
311
312 60
    protected function parseArgumentList()
313
    {
314 60
        $args = [];
315
316 60
        $this->expect(Token::TYPE_LPAREN);
317
318 60
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
319 60
            $this->eat(Token::TYPE_COMMA);
320 60
            $args[] = $this->parseArgument();
321 55
        }
322
323 54
        $this->expect(Token::TYPE_RPAREN);
324
325 53
        return $args;
326
    }
327
328 60 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...
329
    {
330 60
        $nameToken = $this->eatIdentifierToken();
331 60
        $this->expect(Token::TYPE_COLON);
332 59
        $value = $this->parseValue();
333
334 55
        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 332 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...
335
    }
336
337
    protected function parseDirectiveList()
338
    {
339
        $directives = [];
340
341
        while ($this->match(Token::TYPE_AT)) {
342
            $directives[] = $this->parseDirective();
343
            $this->eat(Token::TYPE_COMMA);
344
        }
345
346
        return $directives;
347
    }
348
349 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...
350
    {
351
        $this->expect(Token::TYPE_AT);
352
353
        $nameToken = $this->eatIdentifierToken();
354
        $args      = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
355
356
        return new Directive($nameToken->getData(), $args, new Location($nameToken->getLine(), $nameToken->getColumn()));
357
    }
358
359
    /**
360
     * @return array|InputList|InputObject|Literal|VariableReference
361
     *
362
     * @throws SyntaxErrorException
363
     */
364 59
    protected function parseValue()
365
    {
366 59
        switch ($this->lookAhead->getType()) {
367 59
            case Token::TYPE_LSQUARE_BRACE:
368 11
                return $this->parseList();
369
370 48
            case Token::TYPE_LBRACE:
371 8
                return $this->parseObject();
372
373 40
            case Token::TYPE_VARIABLE:
374 12
                return $this->parseVariableReference();
375
376 33
            case Token::TYPE_NUMBER:
377 33
            case Token::TYPE_STRING:
378 33
            case Token::TYPE_IDENTIFIER:
379 33
            case Token::TYPE_NULL:
380 33
            case Token::TYPE_TRUE:
381 33
            case Token::TYPE_FALSE:
382 32
                $token = $this->lex();
383
384 32
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
385 1
        }
386
387 1
        throw $this->createUnexpectedException($this->lookAhead);
388
    }
389
390 13
    protected function parseList($createType = true)
391
    {
392 13
        $startToken = $this->eat(Token::TYPE_LSQUARE_BRACE);
393
394 13
        $list = [];
395 13
        while (!$this->match(Token::TYPE_RSQUARE_BRACE) && !$this->end()) {
396 13
            $list[] = $this->parseListValue();
397
398 12
            $this->eat(Token::TYPE_COMMA);
399 12
        }
400
401 12
        $this->expect(Token::TYPE_RSQUARE_BRACE);
402
403 12
        return $createType ? new InputList($list, new Location($startToken->getLine(), $startToken->getColumn())) : $list;
404
    }
405
406 19
    protected function parseListValue()
407
    {
408 19
        switch ($this->lookAhead->getType()) {
409 19
            case Token::TYPE_NUMBER:
410 19
            case Token::TYPE_STRING:
411 19
            case Token::TYPE_TRUE:
412 19
            case Token::TYPE_FALSE:
413 19
            case Token::TYPE_NULL:
414 19
            case Token::TYPE_IDENTIFIER:
415 18
                return $this->expect($this->lookAhead->getType())->getData();
416
417 5
            case Token::TYPE_VARIABLE:
418
                return $this->parseVariableReference();
419
420 5
            case Token::TYPE_LBRACE:
421 4
                return $this->parseObject(true);
422
423 3
            case Token::TYPE_LSQUARE_BRACE:
424 2
                return $this->parseList(false);
425 1
        }
426
427 1
        throw new SyntaxErrorException('Can\'t parse argument', $this->getLocation());
428
    }
429
430 9
    protected function parseObject($createType = true)
431
    {
432 9
        $startToken = $this->eat(Token::TYPE_LBRACE);
433
434 9
        $object = [];
435 9
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
436 9
            $key = $this->expectMulti([Token::TYPE_STRING, Token::TYPE_IDENTIFIER])->getData();
437 9
            $this->expect(Token::TYPE_COLON);
438 9
            $value = $this->parseListValue();
439
440 9
            $this->eat(Token::TYPE_COMMA);
441
442 9
            $object[$key] = $value;
443 9
        }
444
445 7
        $this->eat(Token::TYPE_RBRACE);
446
447 7
        return $createType ? new InputObject($object, new Location($startToken->getLine(), $startToken->getColumn())) : $object;
448
    }
449
450 12
    protected function parseFragment()
451
    {
452 12
        $this->lex();
453 12
        $nameToken = $this->eatIdentifierToken();
454
455 12
        $this->eat(Token::TYPE_ON);
456
457 12
        $model = $this->eatIdentifierToken();
458
459 12
        $directives = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
460
461 12
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
462
463 12
        return new Fragment($nameToken->getData(), $model->getData(), $directives, $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
464
    }
465
466 104
    protected function eat($type)
467
    {
468 104
        if ($this->match($type)) {
469 56
            return $this->lex();
470
        }
471
472 102
        return null;
473
    }
474
475 102
    protected function eatMulti($types)
476
    {
477 102
        if ($this->matchMulti($types)) {
478
            return $this->lex();
479
        }
480
481 102
        return null;
482
    }
483
484 104
    protected function matchMulti($types)
485
    {
486 104
        foreach ((array) $types as $type) {
487 104
            if ($this->peek()->getType() === $type) {
488 104
                return true;
489
            }
490 102
        }
491
492 102
        return false;
493
    }
494
}
495