Completed
Push — master ( 9459ba...5c21ca )
by Portey
05:54
created

Parser   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 391
Duplicated Lines 4.09 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 99.57%

Importance

Changes 0
Metric Value
wmc 87
c 0
b 0
f 0
lcom 1
cbo 17
dl 16
loc 391
ccs 229
cts 230
cp 0.9957
rs 1.5789

19 Methods

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