Completed
Pull Request — master (#77)
by Sebastian
03:02
created

Parser   F

Complexity

Total Complexity 90

Size/Duplication

Total Lines 404
Duplicated Lines 7.18 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 98.57%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 90
c 1
b 1
f 0
lcom 1
cbo 15
dl 29
loc 404
ccs 207
cts 210
cp 0.9857
rs 1.5789

20 Methods

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