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

Parser::parseDocument()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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