Passed
Pull Request — master (#196)
by Christoffer
02:50
created

Parser::parseExecutableDefinition()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.2222
c 0
b 0
f 0
cc 7
eloc 11
nc 7
nop 1
1
<?php
2
3
namespace Digia\GraphQL\Language;
4
5
use Digia\GraphQL\Error\InvariantException;
6
use Digia\GraphQL\Error\SyntaxErrorException;
7
use Digia\GraphQL\Language\Node\ArgumentNode;
8
use Digia\GraphQL\Language\Node\BooleanValueNode;
9
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
10
use Digia\GraphQL\Language\Node\DirectiveNode;
11
use Digia\GraphQL\Language\Node\DocumentNode;
12
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
13
use Digia\GraphQL\Language\Node\EnumTypeExtensionNode;
14
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
15
use Digia\GraphQL\Language\Node\EnumValueNode;
16
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
17
use Digia\GraphQL\Language\Node\FieldNode;
18
use Digia\GraphQL\Language\Node\FloatValueNode;
19
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
20
use Digia\GraphQL\Language\Node\FragmentNodeInterface;
21
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
22
use Digia\GraphQL\Language\Node\InlineFragmentNode;
23
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
24
use Digia\GraphQL\Language\Node\InputObjectTypeExtensionNode;
25
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
26
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
27
use Digia\GraphQL\Language\Node\InterfaceTypeExtensionNode;
28
use Digia\GraphQL\Language\Node\IntValueNode;
29
use Digia\GraphQL\Language\Node\ListTypeNode;
30
use Digia\GraphQL\Language\Node\ListValueNode;
31
use Digia\GraphQL\Language\Node\NamedTypeNode;
32
use Digia\GraphQL\Language\Node\NameNode;
33
use Digia\GraphQL\Language\Node\NodeInterface;
34
use Digia\GraphQL\Language\Node\NonNullTypeNode;
35
use Digia\GraphQL\Language\Node\NullValueNode;
36
use Digia\GraphQL\Language\Node\ObjectFieldNode;
37
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
38
use Digia\GraphQL\Language\Node\ObjectTypeExtensionNode;
39
use Digia\GraphQL\Language\Node\ObjectValueNode;
40
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
41
use Digia\GraphQL\Language\Node\OperationTypeDefinitionNode;
42
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
43
use Digia\GraphQL\Language\Node\ScalarTypeExtensionNode;
44
use Digia\GraphQL\Language\Node\SchemaDefinitionNode;
45
use Digia\GraphQL\Language\Node\SelectionSetNode;
46
use Digia\GraphQL\Language\Node\StringValueNode;
47
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
48
use Digia\GraphQL\Language\Node\TypeExtensionNodeInterface;
49
use Digia\GraphQL\Language\Node\TypeNodeInterface;
50
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
51
use Digia\GraphQL\Language\Node\UnionTypeExtensionNode;
52
use Digia\GraphQL\Language\Node\ValueNodeInterface;
53
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
54
use Digia\GraphQL\Language\Node\VariableNode;
55
use function Digia\GraphQL\Util\arraySome;
56
57
class Parser implements ParserInterface
58
{
59
    /**
60
     * @inheritdoc
61
     * @return NodeInterface
62
     * @throws SyntaxErrorException
63
     * @throws \ReflectionException
64
     * @throws InvariantException
65
     */
66
    public function parse($source, array $options = []): NodeInterface
67
    {
68
        return $this->parseDocument($this->createLexer($source, $options));
69
    }
70
71
    /**
72
     * @inheritdoc
73
     * @return NodeInterface
74
     * @throws SyntaxErrorException
75
     * @throws InvariantException
76
     */
77
    public function parseValue($source, array $options = []): NodeInterface
78
    {
79
        $lexer = $this->createLexer($source, $options);
80
81
        $this->expect($lexer, TokenKindEnum::SOF);
82
        $value = $this->parseValueLiteral($lexer);
83
        $this->expect($lexer, TokenKindEnum::EOF);
84
85
        return $value;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     * @return NodeInterface
91
     * @throws SyntaxErrorException
92
     * @throws InvariantException
93
     */
94
    public function parseType($source, array $options = []): NodeInterface
95
    {
96
        $lexer = $this->createLexer($source, $options);
97
98
        $this->expect($lexer, TokenKindEnum::SOF);
99
        $type = $this->parseTypeReference($lexer);
100
        $this->expect($lexer, TokenKindEnum::EOF);
101
102
        return $type;
103
    }
104
105
    /**
106
     * @param LexerInterface $lexer
107
     * @return DocumentNode
108
     * @throws SyntaxErrorException
109
     * @throws \ReflectionException
110
     */
111
    public function parseDocument(LexerInterface $lexer): DocumentNode
112
    {
113
        $start = $lexer->getToken();
114
115
        $this->expect($lexer, TokenKindEnum::SOF);
116
117
        $definitions = [];
118
119
        do {
120
            $definitions[] = $this->parseDefinition($lexer);
121
        } while (!$this->skip($lexer, TokenKindEnum::EOF));
122
123
        return new DocumentNode([
124
            'definitions' => $definitions,
125
            'location'    => $this->createLocation($lexer, $start),
126
        ]);
127
    }
128
129
    /**
130
     * @param LexerInterface $lexer
131
     * @return OperationDefinitionNode
132
     * @throws SyntaxErrorException
133
     */
134
    protected function parseOperationDefinition(LexerInterface $lexer): OperationDefinitionNode
135
    {
136
        $start = $lexer->getToken();
137
138
        if ($this->peek($lexer, TokenKindEnum::BRACE_L)) {
139
            // Anonymous operation
140
            return new OperationDefinitionNode([
141
                'operation'    => 'query',
142
                'selectionSet' => $this->parseSelectionSet($lexer),
143
                'location'     => $this->createLocation($lexer, $start),
144
            ]);
145
        }
146
147
        $operation = $this->parseOperationType($lexer);
148
149
        if ($this->peek($lexer, TokenKindEnum::NAME)) {
150
            $name = $this->parseName($lexer);
151
        }
152
153
        return new OperationDefinitionNode([
154
            'operation'           => $operation,
155
            'name'                => $name ?? null,
156
            'variableDefinitions' => $this->parseVariableDefinitions($lexer),
157
            'directives'          => $this->parseDirectives($lexer),
158
            'selectionSet'        => $this->parseSelectionSet($lexer),
159
            'location'            => $this->createLocation($lexer, $start),
160
        ]);
161
    }
162
163
    /**
164
     * @param LexerInterface $lexer
165
     * @param bool           $isConst
166
     * @return array
167
     * @throws SyntaxErrorException
168
     */
169
    protected function parseArguments(LexerInterface $lexer, bool $isConst = false): ?array
170
    {
171
        $parseFunction = function (LexerInterface $lexer) use ($isConst): ArgumentNode {
172
            return $this->parseArgument($lexer, $isConst);
173
        };
174
175
        return $this->peek($lexer, TokenKindEnum::PAREN_L)
176
            ? $this->many(
177
                $lexer,
178
                TokenKindEnum::PAREN_L,
179
                $parseFunction,
180
                TokenKindEnum::PAREN_R
181
            )
182
            : [];
183
    }
184
185
    /**
186
     * @param LexerInterface $lexer
187
     * @param bool           $isConst
188
     * @return ArgumentNode
189
     * @throws SyntaxErrorException
190
     */
191
    protected function parseArgument(LexerInterface $lexer, bool $isConst = false): ArgumentNode
192
    {
193
        $start = $lexer->getToken();
194
195
        /**
196
         * @param LexerInterface $lexer
197
         * @return NodeInterface|TypeNodeInterface|ValueNodeInterface
198
         */
199
        $parseValue = function (LexerInterface $lexer) use ($isConst): NodeInterface {
200
            $this->expect($lexer, TokenKindEnum::COLON);
201
            return $this->parseValueLiteral($lexer, $isConst);
202
        };
203
204
        return new ArgumentNode([
205
            'name'     => $this->parseName($lexer),
206
            'value'    => $parseValue($lexer),
207
            'location' => $this->createLocation($lexer, $start),
208
        ]);
209
    }
210
211
    /**
212
     * @param LexerInterface $lexer
213
     * @return array
214
     * @throws SyntaxErrorException
215
     */
216
    protected function parseArgumentsDefinition(LexerInterface $lexer): array
217
    {
218
        $parseFunction = function (LexerInterface $lexer): InputValueDefinitionNode {
219
            return $this->parseInputValueDefinition($lexer);
220
        };
221
222
        return $this->peek($lexer, TokenKindEnum::PAREN_L)
223
            ? $this->many(
224
                $lexer,
225
                TokenKindEnum::PAREN_L,
226
                $parseFunction,
227
                TokenKindEnum::PAREN_R
228
            )
229
            : [];
230
    }
231
232
    /**
233
     * @param LexerInterface $lexer
234
     * @return NodeInterface|null
235
     */
236
    public function parseDescription(LexerInterface $lexer): ?NodeInterface
237
    {
238
        return $this->peekDescription($lexer)
239
            ? $this->parseStringLiteral($lexer)
240
            : null;
241
    }
242
243
    /**
244
     * @param LexerInterface $lexer
245
     * @return DirectiveDefinitionNode
246
     * @throws SyntaxErrorException
247
     * @throws \ReflectionException
248
     */
249
    protected function parseDirectiveDefinition(LexerInterface $lexer): DirectiveDefinitionNode
250
    {
251
        $start = $lexer->getToken();
252
253
        $description = $this->parseDescription($lexer);
254
255
        $this->expectKeyword($lexer, KeywordEnum::DIRECTIVE);
256
        $this->expect($lexer, TokenKindEnum::AT);
257
258
        $name      = $this->parseName($lexer);
259
        $arguments = $this->parseArgumentsDefinition($lexer);
260
261
        $this->expectKeyword($lexer, KeywordEnum::ON);
262
263
        $locations = $this->parseDirectiveLocations($lexer);
264
265
        return new DirectiveDefinitionNode([
266
            'description' => $description,
267
            'name'        => $name,
268
            'arguments'   => $arguments,
269
            'locations'   => $locations,
270
            'location'    => $this->createLocation($lexer, $start),
271
        ]);
272
    }
273
274
    /**
275
     * @param LexerInterface $lexer
276
     * @return array
277
     * @throws SyntaxErrorException
278
     * @throws \ReflectionException
279
     */
280
    protected function parseDirectiveLocations(LexerInterface $lexer): array
281
    {
282
        $this->skip($lexer, TokenKindEnum::PIPE);
283
284
        $locations = [];
285
286
        do {
287
            $locations[] = $this->parseDirectiveLocation($lexer);
288
        } while ($this->skip($lexer, TokenKindEnum::PIPE));
289
290
        return $locations;
291
    }
292
293
    /**
294
     * @param LexerInterface $lexer
295
     * @return NameNode
296
     * @throws SyntaxErrorException
297
     * @throws \ReflectionException
298
     */
299
    protected function parseDirectiveLocation(LexerInterface $lexer): NameNode
300
    {
301
        $start = $lexer->getToken();
302
303
        $name = $this->parseName($lexer);
304
305
        if (arraySome(DirectiveLocationEnum::values(), function ($value) use ($name) {
306
            return $name->getValue() === $value;
307
        })) {
308
            return $name;
309
        }
310
311
        throw $this->unexpected($lexer, $start);
312
    }
313
314
    /**
315
     * @param LexerInterface $lexer
316
     * @param bool           $isConst
317
     * @return array
318
     * @throws SyntaxErrorException
319
     */
320
    protected function parseDirectives(LexerInterface $lexer, bool $isConst = false): array
321
    {
322
        $directives = [];
323
324
        while ($this->peek($lexer, TokenKindEnum::AT)) {
325
            $directives[] = $this->parseDirective($lexer, $isConst);
326
        }
327
328
        return $directives;
329
    }
330
331
    /**
332
     * @param LexerInterface $lexer
333
     * @param bool           $isConst
334
     * @return DirectiveNode
335
     * @throws SyntaxErrorException
336
     */
337
    protected function parseDirective(LexerInterface $lexer, bool $isConst): DirectiveNode
338
    {
339
        $start = $lexer->getToken();
340
341
        $this->expect($lexer, TokenKindEnum::AT);
342
343
        return new DirectiveNode([
344
            'name'      => $this->parseName($lexer),
345
            'arguments' => $this->parseArguments($lexer, $isConst),
346
            'location'  => $this->createLocation($lexer, $start),
347
        ]);
348
    }
349
350
    /**
351
     * @param LexerInterface $lexer
352
     * @return NodeInterface
353
     * @throws SyntaxErrorException
354
     * @throws \ReflectionException
355
     */
356
    protected function parseDefinition(LexerInterface $lexer): NodeInterface
357
    {
358
        if ($this->peek($lexer, TokenKindEnum::NAME)) {
359
            switch ($lexer->getTokenValue()) {
360
                case KeywordEnum::QUERY:
361
                case KeywordEnum::MUTATION:
362
                case KeywordEnum::SUBSCRIPTION:
363
                case KeywordEnum::FRAGMENT:
364
                    return $this->parseExecutableDefinition($lexer);
365
                case KeywordEnum::SCHEMA:
366
                case KeywordEnum::SCALAR:
367
                case KeywordEnum::TYPE:
368
                case KeywordEnum::INTERFACE:
369
                case KeywordEnum::UNION:
370
                case KeywordEnum::ENUM:
371
                case KeywordEnum::INPUT:
372
                case KeywordEnum::EXTEND:
373
                case KeywordEnum::DIRECTIVE:
374
                    // Note: The schema definition language is an experimental addition.
375
                    return $this->parseTypeSystemDefinition($lexer);
376
            }
377
        } elseif ($this->peek($lexer, TokenKindEnum::BRACE_L)) {
378
            return $this->parseExecutableDefinition($lexer);
379
        } elseif ($this->peekDescription($lexer)) {
380
            // Note: The schema definition language is an experimental addition.
381
            return $this->parseTypeSystemDefinition($lexer);
382
        }
383
384
        throw $this->unexpected($lexer);
385
    }
386
387
    /**
388
     * @param LexerInterface $lexer
389
     * @return NodeInterface
390
     * @throws SyntaxErrorException
391
     */
392
    protected function parseExecutableDefinition(LexerInterface $lexer): NodeInterface
393
    {
394
        if ($this->peek($lexer, TokenKindEnum::NAME)) {
395
            // valid names are: query, mutation, subscription and fragment
396
            switch ($lexer->getToken()->getValue()) {
397
                case KeywordEnum::QUERY:
398
                case KeywordEnum::MUTATION:
399
                case KeywordEnum::SUBSCRIPTION:
400
                    return $this->parseOperationDefinition($lexer);
401
                case KeywordEnum::FRAGMENT:
402
                    return $this->parseFragmentDefinition($lexer);
403
            }
404
        } elseif ($this->peek($lexer, TokenKindEnum::BRACE_L)) {
405
            // Anonymous query
406
            return $this->parseOperationDefinition($lexer);
407
        }
408
409
        throw $this->unexpected($lexer);
410
    }
411
412
    /**
413
     * @param LexerInterface $lexer
414
     * @return NodeInterface|TypeDefinitionNodeInterface|TypeExtensionNodeInterface
415
     * @throws SyntaxErrorException
416
     * @throws \ReflectionException
417
     */
418
    protected function parseTypeSystemDefinition(LexerInterface $lexer): NodeInterface
419
    {
420
        // Many definitions begin with a description and require a lookahead.
421
        $keywordToken = $this->peekDescription($lexer) ? $lexer->lookahead() : $lexer->getToken();
422
423
        if ($keywordToken->getKind() === TokenKindEnum::NAME) {
424
            switch ($keywordToken->getValue()) {
425
                case KeywordEnum::SCHEMA:
426
                    return $this->parseSchemaDefinition($lexer);
427
                case KeywordEnum::SCALAR:
428
                    return $this->parseScalarTypeDefinition($lexer);
429
                case KeywordEnum::TYPE:
430
                    return $this->parseObjectTypeDefinition($lexer);
431
                case KeywordEnum::INTERFACE:
432
                    return $this->parseInterfaceTypeDefinition($lexer);
433
                case KeywordEnum::UNION:
434
                    return $this->parseUnionTypeDefinition($lexer);
435
                case KeywordEnum::ENUM:
436
                    return $this->parseEnumTypeDefinition($lexer);
437
                case KeywordEnum::INPUT:
438
                    return $this->parseInputObjectTypeDefinition($lexer);
439
                case KeywordEnum::DIRECTIVE:
440
                    return $this->parseDirectiveDefinition($lexer);
441
                case KeywordEnum::EXTEND:
442
                    return $this->parseTypeSystemExtension($lexer);
443
            }
444
        }
445
446
        throw $this->unexpected($lexer, $keywordToken);
447
    }
448
449
    /**
450
     * @param LexerInterface $lexer
451
     * @return TypeExtensionNodeInterface
452
     * @throws SyntaxErrorException
453
     */
454
    protected function parseTypeSystemExtension(LexerInterface $lexer): TypeExtensionNodeInterface
455
    {
456
        $keywordToken = $lexer->lookahead();
457
458
        if ($keywordToken->getKind() === TokenKindEnum::NAME) {
459
            switch ($keywordToken->getValue()) {
460
                case KeywordEnum::SCALAR:
461
                    return $this->parseScalarTypeExtension($lexer, false);
462
                case KeywordEnum::TYPE:
463
                    return $this->parseObjectTypeExtension($lexer);
464
                case KeywordEnum::INTERFACE:
465
                    return $this->parseInterfaceTypeExtension($lexer);
466
                case KeywordEnum::UNION:
467
                    return $this->parseUnionTypeExtension($lexer);
468
                case KeywordEnum::ENUM:
469
                    return $this->parseEnumTypeExtension($lexer);
470
                case KeywordEnum::INPUT:
471
                    return $this->parseInputObjectTypeExtension($lexer);
472
            }
473
        }
474
475
        throw $this->unexpected($lexer, $keywordToken);
476
    }
477
478
    /**
479
     * @param LexerInterface $lexer
480
     * @return EnumTypeDefinitionNode
481
     * @throws SyntaxErrorException
482
     */
483
    protected function parseEnumTypeDefinition(LexerInterface $lexer): EnumTypeDefinitionNode
484
    {
485
        $start = $lexer->getToken();
486
487
        $description = $this->parseDescription($lexer);
488
489
        $this->expectKeyword($lexer, KeywordEnum::ENUM);
490
491
        return new EnumTypeDefinitionNode([
492
            'description' => $description,
493
            'name'        => $this->parseName($lexer),
494
            'directives'  => $this->parseDirectives($lexer),
495
            'values'      => $this->parseEnumValuesDefinition($lexer),
496
            'location'    => $this->createLocation($lexer, $start),
497
        ]);
498
    }
499
500
    /**
501
     * @param LexerInterface $lexer
502
     * @return EnumTypeExtensionNode
503
     * @throws SyntaxErrorException
504
     */
505
    protected function parseEnumTypeExtension(LexerInterface $lexer): EnumTypeExtensionNode
506
    {
507
        $start = $lexer->getToken();
508
509
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
510
        $this->expectKeyword($lexer, KeywordEnum::ENUM);
511
512
        $name       = $this->parseName($lexer);
513
        $directives = $this->parseDirectives($lexer);
514
        $values     = $this->parseEnumValuesDefinition($lexer);
515
516
        if (empty($directives) && empty($values)) {
517
            throw $this->unexpected($lexer);
518
        }
519
520
        return new EnumTypeExtensionNode([
521
            'name'       => $name,
522
            'directives' => $directives,
523
            'values'     => $values,
524
            'location'   => $this->createLocation($lexer, $start),
525
        ]);
526
    }
527
528
    /**
529
     * @param LexerInterface $lexer
530
     * @return array
531
     * @throws SyntaxErrorException
532
     */
533
    protected function parseEnumValuesDefinition(LexerInterface $lexer): array
534
    {
535
        return $this->peek($lexer, TokenKindEnum::BRACE_L)
536
            ? $this->many(
537
                $lexer,
538
                TokenKindEnum::BRACE_L,
539
                [$this, 'parseEnumValueDefinition'],
540
                TokenKindEnum::BRACE_R
541
            )
542
            : [];
543
    }
544
545
    /**
546
     * @param LexerInterface $lexer
547
     * @return EnumValueDefinitionNode
548
     * @throws SyntaxErrorException
549
     */
550
    protected function parseEnumValueDefinition(LexerInterface $lexer): EnumValueDefinitionNode
551
    {
552
        $start = $lexer->getToken();
553
554
        return new EnumValueDefinitionNode([
555
            'description' => $this->parseDescription($lexer),
556
            'name'        => $this->parseName($lexer),
557
            'directives'  => $this->parseDirectives($lexer),
558
            'location'    => $this->createLocation($lexer, $start),
559
        ]);
560
    }
561
562
    /**
563
     * @param LexerInterface $lexer
564
     * @return FieldNode
565
     * @throws SyntaxErrorException
566
     */
567
    protected function parseField(LexerInterface $lexer): FieldNode
568
    {
569
        $start = $lexer->getToken();
570
571
        $nameOrAlias = $this->parseName($lexer);
572
573
        if ($this->skip($lexer, TokenKindEnum::COLON)) {
574
            $alias = $nameOrAlias;
575
            $name  = $this->parseName($lexer);
576
        } else {
577
            $name = $nameOrAlias;
578
        }
579
580
        return new FieldNode([
581
            'alias'        => $alias ?? null,
582
            'name'         => $name,
583
            'arguments'    => $this->parseArguments($lexer, false),
584
            'directives'   => $this->parseDirectives($lexer),
585
            'selectionSet' => $this->peek($lexer, TokenKindEnum::BRACE_L)
586
                ? $this->parseSelectionSet($lexer)
587
                : null,
588
            'location'     => $this->createLocation($lexer, $start),
589
        ]);
590
    }
591
592
    /**
593
     * @param LexerInterface $lexer
594
     * @return array
595
     * @throws SyntaxErrorException
596
     */
597
    protected function parseFieldsDefinition(LexerInterface $lexer): array
598
    {
599
        return $this->peek($lexer, TokenKindEnum::BRACE_L)
600
            ? $this->many(
601
                $lexer,
602
                TokenKindEnum::BRACE_L,
603
                [$this, 'parseFieldDefinition'],
604
                TokenKindEnum::BRACE_R
605
            )
606
            : [];
607
    }
608
609
    /**
610
     * @param LexerInterface $lexer
611
     * @return FieldDefinitionNode
612
     * @throws SyntaxErrorException
613
     */
614
    protected function parseFieldDefinition(LexerInterface $lexer): FieldDefinitionNode
615
    {
616
        $start = $lexer->getToken();
617
618
        $description = $this->parseDescription($lexer);
619
        $name        = $this->parseName($lexer);
620
        $arguments   = $this->parseArgumentsDefinition($lexer);
621
622
        $this->expect($lexer, TokenKindEnum::COLON);
623
624
        return new FieldDefinitionNode([
625
            'description' => $description,
626
            'name'        => $name,
627
            'arguments'   => $arguments,
628
            'type'        => $this->parseTypeReference($lexer),
629
            'directives'  => $this->parseDirectives($lexer),
630
            'location'    => $this->createLocation($lexer, $start),
631
        ]);
632
    }
633
634
    /**
635
     * @param LexerInterface $lexer
636
     * @return FragmentNodeInterface
637
     * @throws SyntaxErrorException
638
     */
639
    protected function parseFragment(LexerInterface $lexer): FragmentNodeInterface
640
    {
641
        $start = $lexer->getToken();
642
643
        $this->expect($lexer, TokenKindEnum::SPREAD);
644
645
        $tokenValue = $lexer->getTokenValue();
646
647
        if ($tokenValue !== 'on' && $this->peek($lexer, TokenKindEnum::NAME)) {
648
            return new FragmentSpreadNode([
649
                'name'       => $this->parseFragmentName($lexer),
650
                'directives' => $this->parseDirectives($lexer),
651
                'location'   => $this->createLocation($lexer, $start),
652
            ]);
653
        }
654
655
        if ($tokenValue === 'on') {
656
            $lexer->advance();
657
            $typeCondition = $this->parseNamedType($lexer);
658
        }
659
660
        return new InlineFragmentNode([
661
            'typeCondition' => $typeCondition ?? null,
662
            'directives'    => $this->parseDirectives($lexer),
663
            'selectionSet'  => $this->parseSelectionSet($lexer),
664
            'location'      => $this->createLocation($lexer, $start),
665
        ]);
666
    }
667
668
    /**
669
     * @param LexerInterface $lexer
670
     * @return FragmentDefinitionNode
671
     * @throws SyntaxErrorException
672
     */
673
    protected function parseFragmentDefinition(LexerInterface $lexer): FragmentDefinitionNode
674
    {
675
        $start = $lexer->getToken();
676
677
        $this->expectKeyword($lexer, KeywordEnum::FRAGMENT);
678
679
        $parseTypeCondition = function (LexerInterface $lexer) {
680
            $this->expectKeyword($lexer, 'on');
681
            return $this->parseNamedType($lexer);
682
        };
683
684
        return new FragmentDefinitionNode([
685
            'name'                => $this->parseFragmentName($lexer),
686
            'variableDefinitions' => $this->parseVariableDefinitions($lexer),
687
            'typeCondition'       => $parseTypeCondition($lexer),
688
            'directives'          => $this->parseDirectives($lexer),
689
            'selectionSet'        => $this->parseSelectionSet($lexer),
690
            'location'            => $this->createLocation($lexer, $start),
691
        ]);
692
    }
693
694
    /**
695
     * @param LexerInterface $lexer
696
     * @return NameNode
697
     * @throws SyntaxErrorException
698
     */
699
    protected function parseFragmentName(LexerInterface $lexer): NameNode
700
    {
701
        if ($lexer->getTokenValue() === 'on') {
702
            throw $this->unexpected($lexer);
703
        }
704
705
        return $this->parseName($lexer);
706
    }
707
708
    /**
709
     * @param LexerInterface $lexer
710
     * @return array
711
     * @throws SyntaxErrorException
712
     */
713
    protected function parseImplementsInterfaces(LexerInterface $lexer): array
714
    {
715
        $types = [];
716
717
        if ($lexer->getTokenValue() === 'implements') {
718
            $lexer->advance();
719
720
            // Optional leading ampersand
721
            $this->skip($lexer, TokenKindEnum::AMP);
722
723
            do {
724
                $types[] = $this->parseNamedType($lexer);
725
            } while ($this->skip($lexer, TokenKindEnum::AMP));
726
        }
727
728
        return $types;
729
    }
730
731
    /**
732
     * @param LexerInterface $lexer
733
     * @return array
734
     * @throws SyntaxErrorException
735
     */
736
    protected function parseInputFieldsDefinition(LexerInterface $lexer): array
737
    {
738
        $parseFunction = function (LexerInterface $lexer): InputValueDefinitionNode {
739
            return $this->parseInputValueDefinition($lexer);
740
        };
741
742
        return $this->peek($lexer, TokenKindEnum::BRACE_L)
743
            ? $this->many(
744
                $lexer,
745
                TokenKindEnum::BRACE_L,
746
                $parseFunction,
747
                TokenKindEnum::BRACE_R
748
            )
749
            : [];
750
    }
751
752
    /**
753
     * @param LexerInterface $lexer
754
     * @return InputObjectTypeDefinitionNode
755
     * @throws SyntaxErrorException
756
     */
757
    protected function parseInputObjectTypeDefinition(LexerInterface $lexer): InputObjectTypeDefinitionNode
758
    {
759
        $start = $lexer->getToken();
760
761
        $description = $this->parseDescription($lexer);
762
763
        $this->expectKeyword($lexer, KeywordEnum::INPUT);
764
765
        return new InputObjectTypeDefinitionNode([
766
            'description' => $description,
767
            'name'        => $this->parseName($lexer),
768
            'directives'  => $this->parseDirectives($lexer, true),
769
            'fields'      => $this->parseInputFieldsDefinition($lexer),
770
            'location'    => $this->createLocation($lexer, $start),
771
        ]);
772
    }
773
774
    /**
775
     * @param LexerInterface $lexer
776
     * @return InputObjectTypeExtensionNode
777
     * @throws SyntaxErrorException
778
     */
779
    protected function parseInputObjectTypeExtension(LexerInterface $lexer): InputObjectTypeExtensionNode
780
    {
781
        $start = $lexer->getToken();
782
783
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
784
        $this->expectKeyword($lexer, KeywordEnum::INPUT);
785
786
        $name       = $this->parseName($lexer);
787
        $directives = $this->parseDirectives($lexer, true);
788
        $fields     = $this->parseInputFieldsDefinition($lexer);
789
790
        if (empty($directives) && empty($fields)) {
791
            throw $this->unexpected($lexer);
792
        }
793
794
        return new InputObjectTypeExtensionNode([
795
            'name'       => $name,
796
            'directives' => $directives,
797
            'fields'     => $fields,
798
            'location'   => $this->createLocation($lexer, $start),
799
        ]);
800
    }
801
802
    /**
803
     * @param LexerInterface $lexer
804
     * @return InputValueDefinitionNode
805
     * @throws SyntaxErrorException
806
     */
807
    protected function parseInputValueDefinition(LexerInterface $lexer): InputValueDefinitionNode
808
    {
809
        $start = $lexer->getToken();
810
811
        $description = $this->parseDescription($lexer);
812
        $name        = $this->parseName($lexer);
813
814
        $this->expect($lexer, TokenKindEnum::COLON);
815
816
        return new InputValueDefinitionNode([
817
            'description'  => $description,
818
            'name'         => $name,
819
            'type'         => $this->parseTypeReference($lexer),
820
            'defaultValue' => $this->skip($lexer, TokenKindEnum::EQUALS)
821
                ? $this->parseValueLiteral($lexer, true)
822
                : null,
823
            'directives'   => $this->parseDirectives($lexer, true),
824
            'location'     => $this->createLocation($lexer, $start),
825
        ]);
826
    }
827
828
    /**
829
     * @param LexerInterface $lexer
830
     * @return InterfaceTypeDefinitionNode
831
     * @throws SyntaxErrorException
832
     */
833
    protected function parseInterfaceTypeDefinition(LexerInterface $lexer): InterfaceTypeDefinitionNode
834
    {
835
        $start = $lexer->getToken();
836
837
        $description = $this->parseDescription($lexer);
838
839
        $this->expectKeyword($lexer, KeywordEnum::INTERFACE);
840
841
        return new InterfaceTypeDefinitionNode([
842
            'description' => $description,
843
            'name'        => $this->parseName($lexer),
844
            'directives'  => $this->parseDirectives($lexer),
845
            'fields'      => $this->parseFieldsDefinition($lexer),
846
            'location'    => $this->createLocation($lexer, $start),
847
        ]);
848
    }
849
850
    /**
851
     * @param LexerInterface $lexer
852
     * @return InterfaceTypeExtensionNode
853
     * @throws SyntaxErrorException
854
     */
855
    protected function parseInterfaceTypeExtension(LexerInterface $lexer): InterfaceTypeExtensionNode
856
    {
857
        $start = $lexer->getToken();
858
859
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
860
        $this->expectKeyword($lexer, KeywordEnum::INTERFACE);
861
862
        $name       = $this->parseName($lexer);
863
        $directives = $this->parseDirectives($lexer);
864
        $fields     = $this->parseFieldsDefinition($lexer);
865
866
        if (empty($directives) && empty($fields)) {
867
            throw $this->unexpected($lexer);
868
        }
869
870
        return new InterfaceTypeExtensionNode([
871
            'name'       => $name,
872
            'directives' => $directives,
873
            'fields'     => $fields,
874
            'location'   => $this->createLocation($lexer, $start),
875
        ]);
876
    }
877
878
    /**
879
     * @param LexerInterface $lexer
880
     * @return NamedTypeNode
881
     * @throws SyntaxErrorException
882
     */
883
    protected function parseNamedType(LexerInterface $lexer): NamedTypeNode
884
    {
885
        $start = $lexer->getToken();
886
887
        return new NamedTypeNode([
888
            'name'     => $this->parseName($lexer),
889
            'location' => $this->createLocation($lexer, $start),
890
        ]);
891
    }
892
893
    /**
894
     * @param LexerInterface $lexer
895
     * @return NameNode
896
     * @throws SyntaxErrorException
897
     */
898
    protected function parseName(LexerInterface $lexer): NameNode
899
    {
900
        $token = $this->expect($lexer, TokenKindEnum::NAME);
901
902
        return new NameNode([
903
            'value'    => $token->getValue(),
904
            'location' => $this->createLocation($lexer, $token),
905
        ]);
906
    }
907
908
    /**
909
     * @param LexerInterface $lexer
910
     * @return ObjectTypeDefinitionNode
911
     * @throws SyntaxErrorException
912
     */
913
    protected function parseObjectTypeDefinition(LexerInterface $lexer): ObjectTypeDefinitionNode
914
    {
915
        $start = $lexer->getToken();
916
917
        $description = $this->parseDescription($lexer);
918
919
        $this->expectKeyword($lexer, KeywordEnum::TYPE);
920
921
        return new ObjectTypeDefinitionNode([
922
            'description' => $description,
923
            'name'        => $this->parseName($lexer),
924
            'interfaces'  => $this->parseImplementsInterfaces($lexer),
925
            'directives'  => $this->parseDirectives($lexer),
926
            'fields'      => $this->parseFieldsDefinition($lexer),
927
            'location'    => $this->createLocation($lexer, $start),
928
        ]);
929
    }
930
931
    /**
932
     * @param LexerInterface $lexer
933
     * @return ObjectTypeExtensionNode
934
     * @throws SyntaxErrorException
935
     */
936
    protected function parseObjectTypeExtension(LexerInterface $lexer): ObjectTypeExtensionNode
937
    {
938
        $start = $lexer->getToken();
939
940
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
941
        $this->expectKeyword($lexer, KeywordEnum::TYPE);
942
943
        $name       = $this->parseName($lexer);
944
        $interfaces = $this->parseImplementsInterfaces($lexer);
945
        $directives = $this->parseDirectives($lexer);
946
        $fields     = $this->parseFieldsDefinition($lexer);
947
948
        if (empty($interfaces) && empty($directives) && empty($fields)) {
949
            throw $this->unexpected($lexer);
950
        }
951
952
        return new ObjectTypeExtensionNode([
953
            'name'       => $name,
954
            'interfaces' => $interfaces,
955
            'directives' => $directives,
956
            'fields'     => $fields,
957
            'location'   => $this->createLocation($lexer, $start),
958
        ]);
959
    }
960
961
    /**
962
     * @param LexerInterface $lexer
963
     * @return SchemaDefinitionNode
964
     * @throws SyntaxErrorException
965
     */
966
    protected function parseSchemaDefinition(LexerInterface $lexer): SchemaDefinitionNode
967
    {
968
        $start = $lexer->getToken();
969
970
        $this->expectKeyword($lexer, KeywordEnum::SCHEMA);
971
972
        return new SchemaDefinitionNode([
973
            'directives'     => $this->parseDirectives($lexer),
974
            'operationTypes' => $this->many(
975
                $lexer,
976
                TokenKindEnum::BRACE_L,
977
                [$this, 'parseOperationTypeDefinition'],
978
                TokenKindEnum::BRACE_R
979
            ),
980
            'location'       => $this->createLocation($lexer, $start),
981
        ]);
982
    }
983
984
    /**
985
     * @param LexerInterface $lexer
986
     * @return OperationTypeDefinitionNode
987
     * @throws SyntaxErrorException
988
     */
989
    protected function parseOperationTypeDefinition(LexerInterface $lexer): OperationTypeDefinitionNode
990
    {
991
        $start = $lexer->getToken();
992
993
        $operation = $this->parseOperationType($lexer);
994
995
        $this->expect($lexer, TokenKindEnum::COLON);
996
997
        return new OperationTypeDefinitionNode([
998
            'operation' => $operation,
999
            'type'      => $this->parseNamedType($lexer),
1000
            'location'  => $this->createLocation($lexer, $start),
1001
        ]);
1002
    }
1003
1004
    /**
1005
     * @param LexerInterface $lexer
1006
     * @return string
1007
     * @throws SyntaxErrorException
1008
     */
1009
    protected function parseOperationType(LexerInterface $lexer): string
1010
    {
1011
        $token = $this->expect($lexer, TokenKindEnum::NAME);
1012
        $value = $token->getValue();
1013
1014
        if (isOperation($value)) {
1015
            return $value;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1016
        }
1017
1018
        throw $this->unexpected($lexer, $token);
1019
    }
1020
1021
    /**
1022
     * @param LexerInterface $lexer
1023
     * @return SelectionSetNode
1024
     * @throws SyntaxErrorException
1025
     */
1026
    protected function parseSelectionSet(LexerInterface $lexer): SelectionSetNode
1027
    {
1028
        $start = $lexer->getToken();
1029
1030
        return new SelectionSetNode([
1031
            'selections' => $this->many(
1032
                $lexer,
1033
                TokenKindEnum::BRACE_L,
1034
                [$this, 'parseSelection'],
1035
                TokenKindEnum::BRACE_R
1036
            ),
1037
            'location'   => $this->createLocation($lexer, $start),
1038
        ]);
1039
    }
1040
1041
    /**
1042
     * @param LexerInterface $lexer
1043
     * @return NodeInterface|FragmentNodeInterface|FieldNode
1044
     * @throws SyntaxErrorException
1045
     */
1046
    protected function parseSelection(LexerInterface $lexer): NodeInterface
1047
    {
1048
        return $this->peek($lexer, TokenKindEnum::SPREAD)
1049
            ? $this->parseFragment($lexer)
1050
            : $this->parseField($lexer);
1051
    }
1052
1053
    /**
1054
     * @param LexerInterface $lexer
1055
     * @return ScalarTypeDefinitionNode
1056
     * @throws SyntaxErrorException
1057
     */
1058
    protected function parseScalarTypeDefinition(LexerInterface $lexer): ScalarTypeDefinitionNode
1059
    {
1060
        $start = $lexer->getToken();
1061
1062
        $description = $this->parseDescription($lexer);
1063
1064
        $this->expectKeyword($lexer, KeywordEnum::SCALAR);
1065
1066
        return new ScalarTypeDefinitionNode([
1067
            'description' => $description,
1068
            'name'        => $this->parseName($lexer),
1069
            'directives'  => $this->parseDirectives($lexer),
1070
            'location'    => $this->createLocation($lexer, $start),
1071
        ]);
1072
    }
1073
1074
    /**
1075
     * @param LexerInterface $lexer
1076
     * @param bool           $isConst
1077
     * @return ScalarTypeExtensionNode
1078
     * @throws SyntaxErrorException
1079
     */
1080
    protected function parseScalarTypeExtension(LexerInterface $lexer, bool $isConst = false): ScalarTypeExtensionNode
1081
    {
1082
        $start = $lexer->getToken();
1083
1084
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
1085
        $this->expectKeyword($lexer, KeywordEnum::SCALAR);
1086
1087
        $name       = $this->parseName($lexer);
1088
        $directives = $this->parseDirectives($lexer, $isConst);
1089
1090
        if (empty($directives)) {
1091
            throw $this->unexpected($lexer);
1092
        }
1093
1094
        return new ScalarTypeExtensionNode([
1095
            'name'       => $name,
1096
            'directives' => $directives,
1097
            'location'   => $this->createLocation($lexer, $start),
1098
        ]);
1099
    }
1100
1101
    /**
1102
     * @param LexerInterface $lexer
1103
     * @return StringValueNode
1104
     */
1105
    protected function parseStringLiteral(LexerInterface $lexer): StringValueNode
1106
    {
1107
        $token = $lexer->getToken();
1108
1109
        $lexer->advance();
1110
1111
        return new StringValueNode([
1112
            'value'    => $token->getValue(),
1113
            'block'    => $token->getKind() === TokenKindEnum::BLOCK_STRING,
1114
            'location' => $this->createLocation($lexer, $token),
1115
        ]);
1116
    }
1117
1118
    /**
1119
     * @param LexerInterface $lexer
1120
     * @return TypeNodeInterface
1121
     * @throws SyntaxErrorException
1122
     */
1123
    protected function parseTypeReference(LexerInterface $lexer): TypeNodeInterface
1124
    {
1125
        $start = $lexer->getToken();
1126
1127
        if ($this->skip($lexer, TokenKindEnum::BRACKET_L)) {
1128
            $type = $this->parseTypeReference($lexer);
1129
1130
            $this->expect($lexer, TokenKindEnum::BRACKET_R);
1131
1132
            $type = new ListTypeNode([
1133
                'type'     => $type,
1134
                'location' => $this->createLocation($lexer, $start),
1135
            ]);
1136
        } else {
1137
            $type = $this->parseNamedType($lexer);
1138
        }
1139
1140
        if ($this->skip($lexer, TokenKindEnum::BANG)) {
1141
            return new NonNullTypeNode([
1142
                'type'     => $type,
1143
                'location' => $this->createLocation($lexer, $start),
1144
            ]);
1145
        }
1146
1147
        return $type;
1148
    }
1149
1150
    /**
1151
     * @param LexerInterface $lexer
1152
     * @return UnionTypeDefinitionNode
1153
     * @throws SyntaxErrorException
1154
     */
1155
    protected function parseUnionTypeDefinition(LexerInterface $lexer): UnionTypeDefinitionNode
1156
    {
1157
        $start = $lexer->getToken();
1158
1159
        $description = $this->parseDescription($lexer);
1160
1161
        $this->expectKeyword($lexer, KeywordEnum::UNION);
1162
1163
        return new UnionTypeDefinitionNode([
1164
            'description' => $description,
1165
            'name'        => $this->parseName($lexer),
1166
            'directives'  => $this->parseDirectives($lexer),
1167
            'types'       => $this->parseUnionMemberTypes($lexer),
1168
            'location'    => $this->createLocation($lexer, $start),
1169
        ]);
1170
    }
1171
1172
    /**
1173
     * @param LexerInterface $lexer
1174
     * @return UnionTypeExtensionNode
1175
     * @throws SyntaxErrorException
1176
     */
1177
    protected function parseUnionTypeExtension(LexerInterface $lexer): UnionTypeExtensionNode
1178
    {
1179
        $start = $lexer->getToken();
1180
1181
        $this->expectKeyword($lexer, KeywordEnum::EXTEND);
1182
        $this->expectKeyword($lexer, KeywordEnum::UNION);
1183
1184
        $name       = $this->parseName($lexer);
1185
        $directives = $this->parseDirectives($lexer);
1186
        $types      = $this->parseUnionMemberTypes($lexer);
1187
1188
        if (empty($directives) && empty($types)) {
1189
            throw $this->unexpected($lexer);
1190
        }
1191
1192
        return new UnionTypeExtensionNode([
1193
            'name'       => $name,
1194
            'directives' => $directives,
1195
            'types'      => $types,
1196
            'location'   => $this->createLocation($lexer, $start),
1197
        ]);
1198
    }
1199
1200
    /**
1201
     * @param LexerInterface $lexer
1202
     * @return array
1203
     * @throws SyntaxErrorException
1204
     */
1205
    protected function parseUnionMemberTypes(LexerInterface $lexer): array
1206
    {
1207
        $types = [];
1208
1209
        if ($this->skip($lexer, TokenKindEnum::EQUALS)) {
1210
            // Optional leading pipe
1211
            $this->skip($lexer, TokenKindEnum::PIPE);
1212
1213
            do {
1214
                $types[] = $this->parseNamedType($lexer);
1215
            } while ($this->skip($lexer, TokenKindEnum::PIPE));
1216
        }
1217
1218
        return $types;
1219
    }
1220
1221
    /**
1222
     * @param LexerInterface $lexer
1223
     * @param bool           $isConst
1224
     * @return NodeInterface|ValueNodeInterface|TypeNodeInterface
1225
     * @throws SyntaxErrorException
1226
     */
1227
    protected function parseValueLiteral(LexerInterface $lexer, bool $isConst = false): NodeInterface
1228
    {
1229
        $token = $lexer->getToken();
1230
1231
        switch ($token->getKind()) {
1232
            case TokenKindEnum::BRACKET_L:
1233
                return $this->parseList($lexer, $isConst);
1234
            case TokenKindEnum::BRACE_L:
1235
                return $this->parseObject($lexer, $isConst);
1236
            case TokenKindEnum::INT:
1237
                $lexer->advance();
1238
1239
                return new IntValueNode([
1240
                    'value'    => $token->getValue(),
1241
                    'location' => $this->createLocation($lexer, $token),
1242
                ]);
1243
            case TokenKindEnum::FLOAT:
1244
                $lexer->advance();
1245
1246
                return new FloatValueNode([
1247
                    'value'    => $token->getValue(),
1248
                    'location' => $this->createLocation($lexer, $token),
1249
                ]);
1250
            case TokenKindEnum::STRING:
1251
            case TokenKindEnum::BLOCK_STRING:
1252
                return $this->parseStringLiteral($lexer);
1253
            case TokenKindEnum::NAME:
1254
                $value = $token->getValue();
1255
1256
                if ($value === 'true' || $value === 'false') {
1257
                    $lexer->advance();
1258
1259
                    return new BooleanValueNode([
1260
                        'value'    => $value === 'true',
1261
                        'location' => $this->createLocation($lexer, $token),
1262
                    ]);
1263
                }
1264
1265
                if ($value === 'null') {
1266
                    $lexer->advance();
1267
1268
                    return new NullValueNode([
1269
                        'location' => $this->createLocation($lexer, $token),
1270
                    ]);
1271
                }
1272
1273
                $lexer->advance();
1274
1275
                return new EnumValueNode([
1276
                    'value'    => $token->getValue(),
1277
                    'location' => $this->createLocation($lexer, $token),
1278
                ]);
1279
            case TokenKindEnum::DOLLAR:
1280
                if (!$isConst) {
1281
                    return $this->parseVariable($lexer);
1282
                }
1283
                break;
1284
        }
1285
1286
        throw $this->unexpected($lexer);
1287
    }
1288
1289
    /**
1290
     * @param LexerInterface $lexer
1291
     * @param bool           $isConst
1292
     * @return ListValueNode
1293
     * @throws SyntaxErrorException
1294
     */
1295
    protected function parseList(LexerInterface $lexer, bool $isConst): ListValueNode
1296
    {
1297
        $start = $lexer->getToken();
1298
1299
        $parseFunction = function (LexerInterface $lexer) use ($isConst) {
1300
            return $this->parseValueLiteral($lexer, $isConst);
1301
        };
1302
1303
        return new ListValueNode([
1304
            'values'   => $this->any(
1305
                $lexer,
1306
                TokenKindEnum::BRACKET_L,
1307
                $parseFunction,
1308
                TokenKindEnum::BRACKET_R
1309
            ),
1310
            'location' => $this->createLocation($lexer, $start),
1311
        ]);
1312
    }
1313
1314
    /**
1315
     * @param LexerInterface $lexer
1316
     * @param bool           $isConst
1317
     * @return ObjectValueNode
1318
     * @throws SyntaxErrorException
1319
     */
1320
    protected function parseObject(LexerInterface $lexer, bool $isConst): ObjectValueNode
1321
    {
1322
        $start = $lexer->getToken();
1323
1324
        $this->expect($lexer, TokenKindEnum::BRACE_L);
1325
1326
        $fields = [];
1327
1328
        while (!$this->skip($lexer, TokenKindEnum::BRACE_R)) {
1329
            $fields[] = $this->parseObjectField($lexer, $isConst);
1330
        }
1331
1332
        return new ObjectValueNode([
1333
            'fields'   => $fields,
1334
            'location' => $this->createLocation($lexer, $start),
1335
        ]);
1336
    }
1337
1338
    /**
1339
     * @param LexerInterface $lexer
1340
     * @param bool           $isConst
1341
     * @return ObjectFieldNode
1342
     * @throws SyntaxErrorException
1343
     */
1344
    protected function parseObjectField(LexerInterface $lexer, bool $isConst): ObjectFieldNode
1345
    {
1346
        $start = $lexer->getToken();
1347
1348
        $parseValue = function (LexerInterface $lexer, bool $isConst) {
1349
            $this->expect($lexer, TokenKindEnum::COLON);
1350
1351
            return $this->parseValueLiteral($lexer, $isConst);
1352
        };
1353
1354
        return new ObjectFieldNode([
1355
            'name'     => $this->parseName($lexer),
1356
            'value'    => $parseValue($lexer, $isConst),
1357
            'location' => $this->createLocation($lexer, $start),
1358
        ]);
1359
    }
1360
1361
    /**
1362
     * @param LexerInterface $lexer
1363
     * @return VariableNode
1364
     * @throws SyntaxErrorException
1365
     */
1366
    protected function parseVariable(LexerInterface $lexer): VariableNode
1367
    {
1368
        $start = $lexer->getToken();
1369
1370
        $this->expect($lexer, TokenKindEnum::DOLLAR);
1371
1372
        return new VariableNode([
1373
            'name'     => $this->parseName($lexer),
1374
            'location' => $this->createLocation($lexer, $start),
1375
        ]);
1376
    }
1377
1378
    /**
1379
     * @param LexerInterface $lexer
1380
     * @return array
1381
     * @throws SyntaxErrorException
1382
     */
1383
    protected function parseVariableDefinitions(LexerInterface $lexer): array
1384
    {
1385
        return $this->peek($lexer, TokenKindEnum::PAREN_L)
1386
            ? $this->many(
1387
                $lexer,
1388
                TokenKindEnum::PAREN_L,
1389
                [$this, 'parseVariableDefinition'],
1390
                TokenKindEnum::PAREN_R
1391
            )
1392
            : [];
1393
    }
1394
1395
    /**
1396
     * @param LexerInterface $lexer
1397
     * @return VariableDefinitionNode
1398
     * @throws SyntaxErrorException
1399
     */
1400
    protected function parseVariableDefinition(LexerInterface $lexer): VariableDefinitionNode
1401
    {
1402
        $start = $lexer->getToken();
1403
1404
        /**
1405
         * @param LexerInterface $lexer
1406
         * @return mixed
1407
         */
1408
        $parseType = function (LexerInterface $lexer): TypeNodeInterface {
1409
            $this->expect($lexer, TokenKindEnum::COLON);
1410
1411
            return $this->parseTypeReference($lexer);
1412
        };
1413
1414
        return new VariableDefinitionNode([
1415
            'variable'     => $this->parseVariable($lexer),
1416
            'type'         => $parseType($lexer),
1417
            'defaultValue' => $this->skip($lexer, TokenKindEnum::EQUALS)
1418
                ? $this->parseValueLiteral($lexer, true)
1419
                : null,
1420
            'location'     => $this->createLocation($lexer, $start),
1421
        ]);
1422
    }
1423
1424
    /**
1425
     * @param string|Source $source
1426
     * @param array         $options
1427
     * @return LexerInterface
1428
     * @throws InvariantException
1429
     */
1430
    protected function createLexer($source, array $options): LexerInterface
1431
    {
1432
        return new Lexer($source instanceof Source ? $source : new Source($source), $options);
1433
    }
1434
1435
    /**
1436
     * @param LexerInterface $lexer
1437
     * @param Token          $startToken
1438
     * @return Location|null
1439
     */
1440
    protected function createLocation(LexerInterface $lexer, Token $startToken): ?Location
1441
    {
1442
        return !$lexer->getOption('noLocation', false)
1443
            ? new Location($startToken->getStart(), $lexer->getLastToken()->getEnd(), $lexer->getSource())
1444
            : null;
1445
    }
1446
1447
    /**
1448
     * Determines if the next token is of a given kind.
1449
     *
1450
     * @param LexerInterface $lexer
1451
     * @param string         $kind
1452
     * @return bool
1453
     */
1454
    protected function peek(LexerInterface $lexer, string $kind): bool
1455
    {
1456
        return $lexer->getTokenKind() === $kind;
1457
    }
1458
1459
    /**
1460
     * If the next token is of the given kind, return true after advancing
1461
     * the lexer. Otherwise, do not change the parser state and return false.
1462
     *
1463
     * @param LexerInterface $lexer
1464
     * @param string         $kind
1465
     * @return bool
1466
     */
1467
    protected function skip(LexerInterface $lexer, string $kind): bool
1468
    {
1469
        if ($match = $this->peek($lexer, $kind)) {
1470
            $lexer->advance();
1471
        }
1472
1473
        return $match;
1474
    }
1475
1476
    /**
1477
     * If the next token is of the given kind, return that token after advancing
1478
     * the lexer. Otherwise, do not change the parser state and throw an error.
1479
     *
1480
     * @param LexerInterface $lexer
1481
     * @param string         $kind
1482
     * @return Token
1483
     * @throws SyntaxErrorException
1484
     */
1485
    protected function expect(LexerInterface $lexer, string $kind): Token
1486
    {
1487
        $token = $lexer->getToken();
1488
1489
        if ($token->getKind() === $kind) {
1490
            $lexer->advance();
1491
            return $token;
1492
        }
1493
1494
        throw new SyntaxErrorException(
1495
            $lexer->getSource(),
1496
            $token->getStart(),
1497
            sprintf('Expected %s, found %s.', $kind, $token)
1498
        );
1499
    }
1500
1501
    /**
1502
     * @param LexerInterface $lexer
1503
     * @param string         $value
1504
     * @return Token
1505
     * @throws SyntaxErrorException
1506
     */
1507
    protected function expectKeyword(LexerInterface $lexer, string $value): Token
1508
    {
1509
        $token = $lexer->getToken();
1510
1511
        if ($token->getKind() === TokenKindEnum::NAME && $token->getValue() === $value) {
1512
            $lexer->advance();
1513
            return $token;
1514
        }
1515
1516
        throw new SyntaxErrorException(
1517
            $lexer->getSource(),
1518
            $token->getStart(),
1519
            sprintf('Expected %s, found %s', $value, $token)
1520
        );
1521
    }
1522
1523
    /**
1524
     * Helper function for creating an error when an unexpected lexed token
1525
     * is encountered.
1526
     *
1527
     * @param LexerInterface $lexer
1528
     * @param Token|null     $atToken
1529
     * @return SyntaxErrorException
1530
     */
1531
    protected function unexpected(LexerInterface $lexer, ?Token $atToken = null): SyntaxErrorException
1532
    {
1533
        $token = $atToken ?: $lexer->getToken();
1534
1535
        return new SyntaxErrorException(
1536
            $lexer->getSource(),
1537
            $token->getStart(),
1538
            sprintf('Unexpected %s', $token)
1539
        );
1540
    }
1541
1542
    /**
1543
     * Returns a possibly empty list of parse nodes, determined by
1544
     * the parseFn. This list begins with a lex token of openKind
1545
     * and ends with a lex token of closeKind. Advances the parser
1546
     * to the next lex token after the closing token.
1547
     *
1548
     * @param LexerInterface $lexer
1549
     * @param string         $openKind
1550
     * @param callable       $parseFunction
1551
     * @param string         $closeKind
1552
     * @return array
1553
     * @throws SyntaxErrorException
1554
     */
1555
    protected function any(LexerInterface $lexer, string $openKind, callable $parseFunction, string $closeKind): array
1556
    {
1557
        $this->expect($lexer, $openKind);
1558
1559
        $nodes = [];
1560
1561
        while (!$this->skip($lexer, $closeKind)) {
1562
            $nodes[] = $parseFunction($lexer);
1563
        }
1564
1565
        return $nodes;
1566
    }
1567
1568
    /**
1569
     * Returns a non-empty list of parse nodes, determined by
1570
     * the parseFn. This list begins with a lex token of openKind
1571
     * and ends with a lex token of closeKind. Advances the parser
1572
     * to the next lex token after the closing token.
1573
     *
1574
     * @param LexerInterface $lexer
1575
     * @param string         $openKind
1576
     * @param callable       $parseFunction
1577
     * @param string         $closeKind
1578
     * @return array
1579
     * @throws SyntaxErrorException
1580
     */
1581
    protected function many(LexerInterface $lexer, string $openKind, callable $parseFunction, string $closeKind): array
1582
    {
1583
        $this->expect($lexer, $openKind);
1584
1585
        $nodes = [$parseFunction($lexer)];
1586
1587
        while (!$this->skip($lexer, $closeKind)) {
1588
            $nodes[] = $parseFunction($lexer);
1589
        }
1590
1591
        return $nodes;
1592
    }
1593
1594
    /**
1595
     * @param LexerInterface $lexer
1596
     * @return bool
1597
     */
1598
    protected function peekDescription(LexerInterface $lexer): bool
1599
    {
1600
        return $this->peek($lexer, TokenKindEnum::STRING) || $this->peek($lexer, TokenKindEnum::BLOCK_STRING);
1601
    }
1602
}
1603