Completed
Push — master ( 9e4cdb...1641ea )
by Alexandr
02:44
created

Parser::eatIdentifierToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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