Completed
Push — master ( eea4af...ac3565 )
by Alexandr
13:12
created

Parser   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 391
Duplicated Lines 4.09 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 97.02%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 87
c 2
b 1
f 0
lcom 1
cbo 16
dl 16
loc 391
ccs 228
cts 235
cp 0.9702
rs 1.5789

20 Methods

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