Completed
Push — master ( 4406e7...95db8c )
by Alexandr
02:55
created

Parser   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 422
Duplicated Lines 3.79 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 99.23%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 93
c 2
b 1
f 0
lcom 1
cbo 16
dl 16
loc 422
ccs 257
cts 259
cp 0.9923
rs 1.5789

20 Methods

Rating   Name   Duplication   Size   Complexity  
D parseBody() 0 40 9
C parse() 0 35 8
A init() 0 13 1
B parseVariables() 0 53 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 37 12
B parseArgumentList() 0 23 5
A parseArgument() 0 8 1
D parseValue() 0 25 10
B parseList() 0 17 5
C parseListValue() 0 23 10
B parseObject() 0 21 5
A parseFragment() 0 12 1
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\Field;
20
use Youshido\GraphQL\Parser\Ast\Fragment;
21
use Youshido\GraphQL\Parser\Ast\FragmentReference;
22
use Youshido\GraphQL\Parser\Ast\Mutation;
23
use Youshido\GraphQL\Parser\Ast\Query;
24
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
25
26
class Parser extends Tokenizer
27
{
28
29
    /** @var array */
30
    private $data = [];
31
32 90
    public function parse($source = null)
33
    {
34 90
        $this->init($source);
35
36 90
        while (!$this->end()) {
37 89
            $tokenType = $this->peek()->getType();
38
39 1
            switch ($tokenType) {
40 89
                case Token::TYPE_LBRACE:
41 89
                case Token::TYPE_QUERY:
42 74
                    foreach ($this->parseBody() as $query) {
43 65
                        $this->data['queries'][] = $query;
44 68
                    }
45
46 68
                    break;
47
48 23
                case Token::TYPE_MUTATION:
49 14
                    foreach ($this->parseBody(Token::TYPE_MUTATION) as $query) {
50 8
                        $this->data['mutations'][] = $query;
51 8
                    }
52
53 8
                    break;
54
55 9
                case Token::TYPE_FRAGMENT:
56 8
                    $this->data['fragments'][] = $this->parseFragment();
57
58 8
                    break;
59
60 1
                default:
61 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
62 1
            }
63 76
        }
64
65 77
        return $this->data;
66
    }
67
68 90
    private function init($source = null)
69
    {
70 90
        $this->initTokenizer($source);
71
72 90
        $this->data = [
73 90
            'queries'            => [],
74 90
            'mutations'          => [],
75 90
            'fragments'          => [],
76 90
            'fragmentReferences' => [],
77 90
            'variables'          => [],
78 90
            'variableReferences' => []
79 90
        ];
80 90
    }
81
82 88
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
83
    {
84 88
        $fields = [];
85 88
        $first  = true;
86
87 88
        if ($highLevel && $this->peek()->getType() === $token) {
88 36
            $this->lex();
89 36
            $this->eat(Token::TYPE_IDENTIFIER);
90
91 36
            if ($this->match(Token::TYPE_LPAREN)) {
92 8
                $this->parseVariables();
93 8
            }
94 36
        }
95
96 88
        $this->lex();
97
98 88
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
99 85
            if ($first) {
100 85
                $first = false;
101 85
            } else {
102 27
                $this->eatMulti([Token::TYPE_COMMA]);
103
            }
104
105 85
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
106 10
                $this->lex();
107
108 10
                if ($this->eat(Token::TYPE_ON)) {
109 3
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
110 3
                } else {
111 8
                    $fields[] = $this->parseFragmentReference();
112
                }
113 10
            } else {
114 85
                $fields[] = $this->parseBodyItem($token, $highLevel);
115
            }
116 76
        }
117
118 77
        $this->expect(Token::TYPE_RBRACE);
119
120 77
        return $fields;
121
    }
122
123 8
    protected function parseVariables()
124
    {
125 8
        $first = true;
126 8
        $this->eat(Token::TYPE_LPAREN);
127
128 8
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
129 8
            if ($first) {
130 8
                $first = false;
131 8
            } else {
132 1
                $this->expect(Token::TYPE_COMMA);
133
            }
134
135 8
            $variableToken = $this->eat(Token::TYPE_VARIABLE);
136 8
            $nameToken     = $this->eatIdentifierToken();
137 8
            $this->eat(Token::TYPE_COLON);
138
139 8
            $isArray              = false;
140 8
            $arrayElementNullable = true;
141
142 8
            if ($this->match(Token::TYPE_LSQUARE_BRACE)) {
143 2
                $isArray = true;
144
145 2
                $this->eat(Token::TYPE_LSQUARE_BRACE);
146 2
                $type = $this->eatIdentifierToken()->getData();
147
148 2
                if ($this->match(Token::TYPE_REQUIRED)) {
149 2
                    $arrayElementNullable = false;
150 2
                    $this->eat(Token::TYPE_REQUIRED);
151 2
                }
152
153 2
                $this->eat(Token::TYPE_RSQUARE_BRACE);
154 2
            } else {
155 7
                $type = $this->eatIdentifierToken()->getData();
156
            }
157
158 8
            $required = false;
159 8
            if ($this->match(Token::TYPE_REQUIRED)) {
160 2
                $required = true;
161 2
                $this->eat(Token::TYPE_REQUIRED);
162 2
            }
163
164 8
            $this->data['variables'][] = new Variable(
165 8
                $nameToken->getData(),
166 8
                $type,
167 8
                $required,
168 8
                $isArray,
169 8
                new Location($variableToken->getLine(), $variableToken->getColumn()),
170
                $arrayElementNullable
171 8
            );
172 8
        }
173
174 8
        $this->expect(Token::TYPE_RPAREN);
175 8
    }
176
177 85
    protected function expectMulti($types)
178
    {
179 85
        if ($this->matchMulti($types)) {
180 85
            return $this->lex();
181
        }
182
183 3
        throw $this->createUnexpectedException($this->peek());
184
    }
185
186 8
    protected function parseVariableReference()
187
    {
188 8
        $startToken = $this->expectMulti([Token::TYPE_VARIABLE]);
189
190 8
        if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER) || $this->match(Token::TYPE_QUERY)) {
191 7
            $name = $this->lex()->getData();
192
193 7
            $variable = $this->findVariable($name);
194 7
            if ($variable) {
195 7
                $variable->setUsed(true);
196 7
            }
197
198 7
            $variableReference = new VariableReference($name, $variable, new Location($startToken->getLine(), $startToken->getColumn()));
199
200 7
            $this->data['variableReferences'][] = $variableReference;
201
202 7
            return $variableReference;
203
        }
204
205 1
        throw $this->createUnexpectedException($this->peek());
206
    }
207
208 7
    protected function findVariable($name)
209
    {
210 7
        foreach ($this->data['variables'] as $variable) {
211
            /** @var $variable Variable */
212 7
            if ($variable->getName() == $name) {
213 7
                return $variable;
214
            }
215 1
        }
216
217
        return null;
218
    }
219
220 8
    protected function parseFragmentReference()
221
    {
222 8
        $nameToken         = $this->eatIdentifierToken();
223 8
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
224
225 8
        $this->data['fragmentReferences'][] = $fragmentReference;
226
227 8
        return $fragmentReference;
228
    }
229
230 85
    protected function eatIdentifierToken()
231
    {
232 85
        return $this->expectMulti([
233 85
            Token::TYPE_IDENTIFIER,
234 85
            Token::TYPE_MUTATION,
235 85
            Token::TYPE_QUERY,
236 85
            Token::TYPE_FRAGMENT,
237 85
        ]);
238
    }
239
240 85
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
241
    {
242 85
        $nameToken = $this->eatIdentifierToken();
243 85
        $alias     = null;
244
245 85
        if ($this->eat(Token::TYPE_COLON)) {
246 17
            $alias     = $nameToken->getData();
247 17
            $nameToken = $this->eatIdentifierToken();
248 17
        }
249
250 85
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
251 85
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
252
253 77
        if ($this->match(Token::TYPE_LBRACE)) {
254 53
            $fields = $this->parseBody($type == Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
255
256 50
            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...
257 3
                throw $this->createUnexpectedTokenTypeException($this->lookAhead->getType());
258
            }
259
260 49 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...
261 48
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $bodyLocation);
262 5
            } elseif ($type == Token::TYPE_TYPED_FRAGMENT) {
263 3
                return new TypedFragmentReference($nameToken->getData(), $fields, $bodyLocation);
264
            } else {
265 2
                return new Mutation($nameToken->getData(), $alias, $arguments, $fields, $bodyLocation);
266
            }
267 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...
268 75
            if ($highLevel && $type == Token::TYPE_MUTATION) {
269 7
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $bodyLocation);
270 69
            } elseif ($highLevel && $type == Token::TYPE_QUERY) {
271 22
                return new Query($nameToken->getData(), $alias, $arguments, [], $bodyLocation);
272
            }
273
274 51
            return new Field($nameToken->getData(), $alias, $arguments, $bodyLocation);
275
        }
276
    }
277
278 49
    protected function parseArgumentList()
279
    {
280 49
        $args  = [];
281 49
        $first = true;
282
283 49
        $this->expect(Token::TYPE_LPAREN);
284
285 49
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
286 49
            if ($first) {
287 49
                $first = false;
288 49
            } else {
289 5
                if ($this->match(Token::TYPE_COMMA)) {
290 4
                    $this->eat(Token::TYPE_COMMA);
291 4
                }
292
            }
293
294 49
            $args[] = $this->parseArgument();
295 42
        }
296
297 41
        $this->expect(Token::TYPE_RPAREN);
298
299 41
        return $args;
300
    }
301
302 49
    protected function parseArgument()
303
    {
304 49
        $nameToken = $this->eatIdentifierToken();
305 49
        $this->expect(Token::TYPE_COLON);
306 48
        $value = $this->parseValue();
307
308 42
        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 306 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...
309
    }
310
311
    /**
312
     * @return array|InputList|InputObject|Literal|VariableReference
313
     *
314
     * @throws SyntaxErrorException
315
     */
316 48
    protected function parseValue()
317
    {
318 48
        switch ($this->lookAhead->getType()) {
319 48
            case Token::TYPE_LSQUARE_BRACE:
320 7
                return $this->parseList();
321
322 41
            case Token::TYPE_LBRACE:
323 6
                return $this->parseObject();
324
325 35
            case Token::TYPE_VARIABLE:
326 8
                return $this->parseVariableReference();
327
328 29
            case Token::TYPE_NUMBER:
329 29
            case Token::TYPE_STRING:
330 29
            case Token::TYPE_IDENTIFIER:
331 29
            case Token::TYPE_NULL:
332 29
            case Token::TYPE_TRUE:
333 29
            case Token::TYPE_FALSE:
334 28
                $token = $this->lex();
335
336 28
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
337 1
        }
338
339 1
        throw $this->createUnexpectedException($this->lookAhead);
340
    }
341
342 9
    protected function parseList($createType = true)
343
    {
344 9
        $startToken = $this->eat(Token::TYPE_LSQUARE_BRACE);
345
346 9
        $list = [];
347 9
        while (!$this->match(Token::TYPE_RSQUARE_BRACE) && !$this->end()) {
348 9
            $list[] = $this->parseListValue();
349
350 8
            if ($this->lookAhead->getType() != Token::TYPE_RSQUARE_BRACE) {
351 7
                $this->expect(Token::TYPE_COMMA);
352 7
            }
353 8
        }
354
355 8
        $this->expect(Token::TYPE_RSQUARE_BRACE);
356
357 8
        return $createType ? new InputList($list, new Location($startToken->getLine(), $startToken->getColumn())) : $list;
358
    }
359
360 13
    protected function parseListValue()
361
    {
362 13
        switch ($this->lookAhead->getType()) {
363 13
            case Token::TYPE_NUMBER:
364 13
            case Token::TYPE_STRING:
365 13
            case Token::TYPE_TRUE:
366 13
            case Token::TYPE_FALSE:
367 13
            case Token::TYPE_NULL:
368 13
            case Token::TYPE_IDENTIFIER:
369 12
                return $this->expect($this->lookAhead->getType())->getData();
370
371 4
            case Token::TYPE_VARIABLE:
372
                return $this->parseVariableReference();
373
374 4
            case Token::TYPE_LBRACE:
375 3
                return $this->parseObject(true);
376
377 3
            case Token::TYPE_LSQUARE_BRACE:
378 2
                return $this->parseList(false);
379 1
        }
380
381 1
        throw new SyntaxErrorException('Can\'t parse argument', $this->getLocation());
382
    }
383
384 7
    protected function parseObject($createType = true)
385
    {
386 7
        $startToken = $this->eat(Token::TYPE_LBRACE);
387
388 7
        $object = [];
389 7
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
390 7
            $key = $this->expectMulti([Token::TYPE_STRING, Token::TYPE_IDENTIFIER])->getData();
391 7
            $this->expect(Token::TYPE_COLON);
392 7
            $value = $this->parseListValue();
393
394 7
            if ($this->peek()->getType() != Token::TYPE_RBRACE) {
395 5
                $this->expect(Token::TYPE_COMMA);
396 3
            }
397
398 5
            $object[$key] = $value;
399 5
        }
400
401 4
        $this->eat(Token::TYPE_RBRACE);
402
403 4
        return $createType ? new InputObject($object, new Location($startToken->getLine(), $startToken->getColumn())) : $object;
404
    }
405
406 8
    protected function parseFragment()
407
    {
408 8
        $this->lex();
409 8
        $nameToken = $this->eatIdentifierToken();
410
411 8
        $this->eat(Token::TYPE_ON);
412
413 8
        $model  = $this->eatIdentifierToken();
414 8
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
415
416 8
        return new Fragment($nameToken->getData(), $model->getData(), $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
417
    }
418
419 87
    protected function eat($type)
420
    {
421 87
        if ($this->match($type)) {
422 44
            return $this->lex();
423
        }
424
425 82
        return null;
426
    }
427
428 27
    protected function eatMulti($types)
429
    {
430 27
        if ($this->matchMulti($types)) {
431 20
            return $this->lex();
432
        }
433
434 8
        return null;
435
    }
436
437 85
    protected function matchMulti($types)
438
    {
439 85
        foreach ($types as $type) {
440 85
            if ($this->peek()->getType() == $type) {
441 85
                return true;
442
            }
443 17
        }
444
445 11
        return false;
446
    }
447
}
448