Completed
Pull Request — master (#153)
by Portey
04:20
created

Parser   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 398
Duplicated Lines 4.02 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 95.19%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 88
c 2
b 1
f 0
lcom 1
cbo 16
dl 16
loc 398
ccs 198
cts 208
cp 0.9519
rs 1.5789

20 Methods

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