Completed
Push — master ( b72ba3...24c473 )
by Vladimir
17s queued 14s
created

testAssertsThatASourceToParseIsNotArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Language;
6
7
use GraphQL\Error\InvariantViolation;
8
use GraphQL\Error\SyntaxError;
9
use GraphQL\Language\AST\ArgumentNode;
10
use GraphQL\Language\AST\FieldNode;
11
use GraphQL\Language\AST\NameNode;
12
use GraphQL\Language\AST\Node;
13
use GraphQL\Language\AST\NodeKind;
14
use GraphQL\Language\AST\NodeList;
15
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
16
use GraphQL\Language\AST\SelectionSetNode;
17
use GraphQL\Language\AST\StringValueNode;
18
use GraphQL\Language\Parser;
19
use GraphQL\Language\Source;
20
use GraphQL\Language\SourceLocation;
21
use GraphQL\Utils\Utils;
22
use PHPUnit\Framework\TestCase;
23
use stdClass;
24
use function file_get_contents;
25
use function sprintf;
26
27
class ParserTest extends TestCase
28
{
29
    public function testAssertsThatASourceToParseIsNotNull() : void
30
    {
31
        $this->expectException(InvariantViolation::class);
32
        $this->expectExceptionMessage('GraphQL query body is expected to be string, but got NULL');
33
        Parser::parse(null);
34
    }
35
36
    public function testAssertsThatASourceToParseIsNotArray() : void
37
    {
38
        $this->expectException(InvariantViolation::class);
39
        $this->expectExceptionMessage('GraphQL query body is expected to be string, but got array');
40
        Parser::parse(['a' => 'b']);
0 ignored issues
show
Bug introduced by
array('a' => 'b') of type array<string,string> is incompatible with the type GraphQL\Language\Source|string expected by parameter $source of GraphQL\Language\Parser::parse(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

40
        Parser::parse(/** @scrutinizer ignore-type */ ['a' => 'b']);
Loading history...
41
    }
42
43
    public function testAssertsThatASourceToParseIsNotObject() : void
44
    {
45
        $this->expectException(InvariantViolation::class);
46
        $this->expectExceptionMessage('GraphQL query body is expected to be string, but got stdClass');
47
        Parser::parse(new stdClass());
0 ignored issues
show
Bug introduced by
new stdClass() of type stdClass is incompatible with the type GraphQL\Language\Source|string expected by parameter $source of GraphQL\Language\Parser::parse(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

47
        Parser::parse(/** @scrutinizer ignore-type */ new stdClass());
Loading history...
48
    }
49
50
    public function parseProvidesUsefulErrors()
51
    {
52
        return [
53
            [
54
                '{',
55
                'Syntax Error: Expected Name, found <EOF>',
56
                "Syntax Error: Expected Name, found <EOF>\n\nGraphQL request (1:2)\n1: {\n    ^\n",
57
                [1],
58
                [new SourceLocation(
59
                    1,
60
                    2
61
                ),
62
                ],
63
            ],
64
            [
65
                '{ ...MissingOn }
66
fragment MissingOn Type
67
', 'Syntax Error: Expected "on", found Name "Type"',
68
                "Syntax Error: Expected \"on\", found Name \"Type\"\n\nGraphQL request (2:20)\n1: { ...MissingOn }\n2: fragment MissingOn Type\n                      ^\n3: \n",
69
            ],
70
            ['{ field: {} }', 'Syntax Error: Expected Name, found {', "Syntax Error: Expected Name, found {\n\nGraphQL request (1:10)\n1: { field: {} }\n            ^\n"],
71
            ['notanoperation Foo { field }', 'Syntax Error: Unexpected Name "notanoperation"', "Syntax Error: Unexpected Name \"notanoperation\"\n\nGraphQL request (1:1)\n1: notanoperation Foo { field }\n   ^\n"],
72
            ['...', 'Syntax Error: Unexpected ...', "Syntax Error: Unexpected ...\n\nGraphQL request (1:1)\n1: ...\n   ^\n"],
73
        ];
74
    }
75
76
    /**
77
     * @see          it('parse provides useful errors')
78
     *
79
     * @dataProvider parseProvidesUsefulErrors
80
     */
81
    public function testParseProvidesUsefulErrors(
82
        $str,
83
        $expectedMessage,
84
        $stringRepresentation,
85
        $expectedPositions = null,
86
        $expectedLocations = null
87
    ) : void {
88
        try {
89
            Parser::parse($str);
90
            self::fail('Expected exception not thrown');
91
        } catch (SyntaxError $e) {
92
            self::assertEquals($expectedMessage, $e->getMessage());
93
            self::assertEquals($stringRepresentation, (string) $e);
94
95
            if ($expectedPositions) {
96
                self::assertEquals($expectedPositions, $e->getPositions());
97
            }
98
99
            if ($expectedLocations) {
100
                self::assertEquals($expectedLocations, $e->getLocations());
101
            }
102
        }
103
    }
104
105
    /**
106
     * @see it('parse provides useful error when using source')
107
     */
108
    public function testParseProvidesUsefulErrorWhenUsingSource() : void
109
    {
110
        try {
111
            Parser::parse(new Source('query', 'MyQuery.graphql'));
112
            self::fail('Expected exception not thrown');
113
        } catch (SyntaxError $error) {
114
            self::assertEquals(
115
                "Syntax Error: Expected {, found <EOF>\n\nMyQuery.graphql (1:6)\n1: query\n        ^\n",
116
                (string) $error
117
            );
118
        }
119
    }
120
121
    /**
122
     * @see it('parses variable inline values')
123
     */
124
    public function testParsesVariableInlineValues() : void
125
    {
126
        $this->expectNotToPerformAssertions();
127
        // Following line should not throw:
128
        Parser::parse('{ field(complex: { a: { b: [ $var ] } }) }');
129
    }
130
131
    /**
132
     * @see it('parses constant default values')
133
     */
134
    public function testParsesConstantDefaultValues() : void
135
    {
136
        $this->expectSyntaxError(
137
            'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }',
138
            'Unexpected $',
139
            $this->loc(1, 37)
140
        );
141
    }
142
143
    private function expectSyntaxError($text, $message, $location)
144
    {
145
        $this->expectException(SyntaxError::class);
146
        $this->expectExceptionMessage($message);
147
        try {
148
            Parser::parse($text);
149
        } catch (SyntaxError $error) {
150
            self::assertEquals([$location], $error->getLocations());
151
            throw $error;
152
        }
153
    }
154
155
    private function loc($line, $column)
156
    {
157
        return new SourceLocation($line, $column);
158
    }
159
160
    /**
161
     * @see it('does not accept fragments spread of "on"')
162
     */
163
    public function testDoesNotAcceptFragmentsNamedOn() : void
164
    {
165
        $this->expectSyntaxError(
166
            'fragment on on on { on }',
167
            'Unexpected Name "on"',
168
            $this->loc(1, 10)
169
        );
170
    }
171
172
    /**
173
     * @see it('does not accept fragments spread of "on"')
174
     */
175
    public function testDoesNotAcceptFragmentSpreadOfOn() : void
176
    {
177
        $this->expectSyntaxError(
178
            '{ ...on }',
179
            'Expected Name, found }',
180
            $this->loc(1, 9)
181
        );
182
    }
183
184
    /**
185
     * @see it('parses multi-byte characters')
186
     */
187
    public function testParsesMultiByteCharacters() : void
188
    {
189
        // Note: \u0A0A could be naively interpreted as two line-feed chars.
190
191
        $char  = Utils::chr(0x0A0A);
192
        $query = <<<HEREDOC
193
        # This comment has a $char multi-byte character.
194
        { field(arg: "Has a $char multi-byte character.") }
195
HEREDOC;
196
197
        $result = Parser::parse($query, ['noLocation' => true]);
198
199
        $expected = new SelectionSetNode([
200
            'selections' => new NodeList([
201
                new FieldNode([
202
                    'name'       => new NameNode(['value' => 'field']),
203
                    'arguments'  => new NodeList([
204
                        new ArgumentNode([
205
                            'name'  => new NameNode(['value' => 'arg']),
206
                            'value' => new StringValueNode(
207
                                ['value' => sprintf('Has a %s multi-byte character.', $char)]
208
                            ),
209
                        ]),
210
                    ]),
211
                    'directives' => new NodeList([]),
212
                ]),
213
            ]),
214
        ]);
215
216
        self::assertEquals($expected, $result->definitions[0]->selectionSet);
217
    }
218
219
    /**
220
     * @see it('parses kitchen sink')
221
     */
222
    public function testParsesKitchenSink() : void
223
    {
224
        // Following should not throw:
225
        $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
226
        $result      = Parser::parse($kitchenSink);
227
        self::assertNotEmpty($result);
228
    }
229
230
    /**
231
     * allows non-keywords anywhere a Name is allowed
232
     */
233
    public function testAllowsNonKeywordsAnywhereANameIsAllowed() : void
234
    {
235
        $nonKeywords = [
236
            'on',
237
            'fragment',
238
            'query',
239
            'mutation',
240
            'subscription',
241
            'true',
242
            'false',
243
        ];
244
        foreach ($nonKeywords as $keyword) {
245
            $fragmentName = $keyword;
246
            if ($keyword === 'on') {
247
                $fragmentName = 'a';
248
            }
249
250
            // Expected not to throw:
251
            $result = Parser::parse(<<<GRAPHQL
252
query $keyword {
253
... $fragmentName
254
... on $keyword { field }
255
}
256
fragment $fragmentName on Type {
257
$keyword($keyword: \$$keyword) @$keyword($keyword: $keyword)
258
}
259
fragment $fragmentName on Type {
260
  $keyword($keyword: \$$keyword) @$keyword($keyword: $keyword)	
261
}
262
GRAPHQL
263
            );
264
            self::assertNotEmpty($result);
265
        }
266
    }
267
268
    /**
269
     * @see it('parses anonymous mutation operations')
270
     */
271
    public function testParsessAnonymousMutationOperations() : void
272
    {
273
        $this->expectNotToPerformAssertions();
274
        // Should not throw:
275
        Parser::parse('
276
          mutation {
277
            mutationField
278
          }
279
        ');
280
    }
281
282
    /**
283
     * @see it('parses anonymous subscription operations')
284
     */
285
    public function testParsesAnonymousSubscriptionOperations() : void
286
    {
287
        $this->expectNotToPerformAssertions();
288
        // Should not throw:
289
        Parser::parse('
290
          subscription {
291
            subscriptionField
292
          }
293
        ');
294
    }
295
296
    /**
297
     * @see it('parses named mutation operations')
298
     */
299
    public function testParsesNamedMutationOperations() : void
300
    {
301
        $this->expectNotToPerformAssertions();
302
        // Should not throw:
303
        Parser::parse('
304
          mutation Foo {
305
            mutationField
306
          }
307
        ');
308
    }
309
310
    /**
311
     * @see it('parses named subscription operations')
312
     */
313
    public function testParsesNamedSubscriptionOperations() : void
314
    {
315
        $this->expectNotToPerformAssertions();
316
        Parser::parse('
317
          subscription Foo {
318
            subscriptionField
319
          }
320
        ');
321
    }
322
323
    /**
324
     * @see it('creates ast')
325
     */
326
    public function testParseCreatesAst() : void
327
    {
328
        $source = new Source('{
329
  node(id: 4) {
330
    id,
331
    name
332
  }
333
}
334
');
335
        $result = Parser::parse($source);
336
337
        $loc = static function ($start, $end) {
338
            return [
339
                'start' => $start,
340
                'end'   => $end,
341
            ];
342
        };
343
344
        $expected = [
345
            'kind'        => NodeKind::DOCUMENT,
346
            'loc'         => $loc(0, 41),
347
            'definitions' => [
348
                [
349
                    'kind'                => NodeKind::OPERATION_DEFINITION,
350
                    'loc'                 => $loc(0, 40),
351
                    'operation'           => 'query',
352
                    'name'                => null,
353
                    'variableDefinitions' => [],
354
                    'directives'          => [],
355
                    'selectionSet'        => [
356
                        'kind'       => NodeKind::SELECTION_SET,
357
                        'loc'        => $loc(0, 40),
358
                        'selections' => [
359
                            [
360
                                'kind'         => NodeKind::FIELD,
361
                                'loc'          => $loc(4, 38),
362
                                'alias'        => null,
363
                                'name'         => [
364
                                    'kind'  => NodeKind::NAME,
365
                                    'loc'   => $loc(4, 8),
366
                                    'value' => 'node',
367
                                ],
368
                                'arguments'    => [
369
                                    [
370
                                        'kind'  => NodeKind::ARGUMENT,
371
                                        'name'  => [
372
                                            'kind'  => NodeKind::NAME,
373
                                            'loc'   => $loc(9, 11),
374
                                            'value' => 'id',
375
                                        ],
376
                                        'value' => [
377
                                            'kind'  => NodeKind::INT,
378
                                            'loc'   => $loc(13, 14),
379
                                            'value' => '4',
380
                                        ],
381
                                        'loc'   => $loc(9, 14, $source),
382
                                    ],
383
                                ],
384
                                'directives'   => [],
385
                                'selectionSet' => [
386
                                    'kind'       => NodeKind::SELECTION_SET,
387
                                    'loc'        => $loc(16, 38),
388
                                    'selections' => [
389
                                        [
390
                                            'kind'         => NodeKind::FIELD,
391
                                            'loc'          => $loc(22, 24),
392
                                            'alias'        => null,
393
                                            'name'         => [
394
                                                'kind'  => NodeKind::NAME,
395
                                                'loc'   => $loc(22, 24),
396
                                                'value' => 'id',
397
                                            ],
398
                                            'arguments'    => [],
399
                                            'directives'   => [],
400
                                            'selectionSet' => null,
401
                                        ],
402
                                        [
403
                                            'kind'         => NodeKind::FIELD,
404
                                            'loc'          => $loc(30, 34),
405
                                            'alias'        => null,
406
                                            'name'         => [
407
                                                'kind'  => NodeKind::NAME,
408
                                                'loc'   => $loc(30, 34),
409
                                                'value' => 'name',
410
                                            ],
411
                                            'arguments'    => [],
412
                                            'directives'   => [],
413
                                            'selectionSet' => null,
414
                                        ],
415
                                    ],
416
                                ],
417
                            ],
418
                        ],
419
                    ],
420
                ],
421
            ],
422
        ];
423
424
        self::assertEquals($expected, self::nodeToArray($result));
425
    }
426
427
    /**
428
     * @return mixed[]
429
     */
430
    public static function nodeToArray(Node $node) : array
431
    {
432
        return TestUtils::nodeToArray($node);
433
    }
434
435
    /**
436
     * @see it('creates ast from nameless query without variables')
437
     */
438
    public function testParseCreatesAstFromNamelessQueryWithoutVariables() : void
439
    {
440
        $source = new Source('query {
441
  node {
442
    id
443
  }
444
}
445
');
446
        $result = Parser::parse($source);
447
448
        $loc = static function ($start, $end) {
449
            return [
450
                'start' => $start,
451
                'end'   => $end,
452
            ];
453
        };
454
455
        $expected = [
456
            'kind'        => NodeKind::DOCUMENT,
457
            'loc'         => $loc(0, 30),
458
            'definitions' => [
459
                [
460
                    'kind'                => NodeKind::OPERATION_DEFINITION,
461
                    'loc'                 => $loc(0, 29),
462
                    'operation'           => 'query',
463
                    'name'                => null,
464
                    'variableDefinitions' => [],
465
                    'directives'          => [],
466
                    'selectionSet'        => [
467
                        'kind'       => NodeKind::SELECTION_SET,
468
                        'loc'        => $loc(6, 29),
469
                        'selections' => [
470
                            [
471
                                'kind'         => NodeKind::FIELD,
472
                                'loc'          => $loc(10, 27),
473
                                'alias'        => null,
474
                                'name'         => [
475
                                    'kind'  => NodeKind::NAME,
476
                                    'loc'   => $loc(10, 14),
477
                                    'value' => 'node',
478
                                ],
479
                                'arguments'    => [],
480
                                'directives'   => [],
481
                                'selectionSet' => [
482
                                    'kind'       => NodeKind::SELECTION_SET,
483
                                    'loc'        => $loc(15, 27),
484
                                    'selections' => [
485
                                        [
486
                                            'kind'         => NodeKind::FIELD,
487
                                            'loc'          => $loc(21, 23),
488
                                            'alias'        => null,
489
                                            'name'         => [
490
                                                'kind'  => NodeKind::NAME,
491
                                                'loc'   => $loc(21, 23),
492
                                                'value' => 'id',
493
                                            ],
494
                                            'arguments'    => [],
495
                                            'directives'   => [],
496
                                            'selectionSet' => null,
497
                                        ],
498
                                    ],
499
                                ],
500
                            ],
501
                        ],
502
                    ],
503
                ],
504
            ],
505
        ];
506
507
        self::assertEquals($expected, self::nodeToArray($result));
508
    }
509
510
    /**
511
     * @see it('allows parsing without source location information')
512
     */
513
    public function testAllowsParsingWithoutSourceLocationInformation() : void
514
    {
515
        $source = new Source('{ id }');
516
        $result = Parser::parse($source, ['noLocation' => true]);
517
518
        self::assertEquals(null, $result->loc);
519
    }
520
521
    /**
522
     * @see it('Experimental: allows parsing fragment defined variables')
523
     */
524
    public function testExperimentalAllowsParsingFragmentDefinedVariables() : void
525
    {
526
        $source = new Source('fragment a($v: Boolean = false) on t { f(v: $v) }');
527
        // not throw
528
        Parser::parse($source, ['experimentalFragmentVariables' => true]);
529
530
        $this->expectException(SyntaxError::class);
531
        Parser::parse($source);
532
    }
533
534
    // Describe: parseValue
535
536
    /**
537
     * @see it('contains location information that only stringifys start/end')
538
     */
539
    public function testContainsLocationInformationThatOnlyStringifysStartEnd() : void
540
    {
541
        $source = new Source('{ id }');
542
        $result = Parser::parse($source);
543
        self::assertEquals(['start' => 0, 'end' => '6'], TestUtils::locationToArray($result->loc));
544
    }
545
546
    /**
547
     * @see it('contains references to source')
548
     */
549
    public function testContainsReferencesToSource() : void
550
    {
551
        $source = new Source('{ id }');
552
        $result = Parser::parse($source);
553
        self::assertEquals($source, $result->loc->source);
554
    }
555
556
    // Describe: parseType
557
558
    /**
559
     * @see it('contains references to start and end tokens')
560
     */
561
    public function testContainsReferencesToStartAndEndTokens() : void
562
    {
563
        $source = new Source('{ id }');
564
        $result = Parser::parse($source);
565
        self::assertEquals('<SOF>', $result->loc->startToken->kind);
566
        self::assertEquals('<EOF>', $result->loc->endToken->kind);
567
    }
568
569
    /**
570
     * @see it('parses null value')
571
     */
572
    public function testParsesNullValues() : void
573
    {
574
        self::assertEquals(
575
            [
576
                'kind' => NodeKind::NULL,
577
                'loc'  => ['start' => 0, 'end' => 4],
578
            ],
579
            self::nodeToArray(Parser::parseValue('null'))
580
        );
581
    }
582
583
    /**
584
     * @see it('parses list values')
585
     */
586
    public function testParsesListValues() : void
587
    {
588
        self::assertEquals(
589
            [
590
                'kind'   => NodeKind::LST,
591
                'loc'    => ['start' => 0, 'end' => 11],
592
                'values' => [
593
                    [
594
                        'kind'  => NodeKind::INT,
595
                        'loc'   => ['start' => 1, 'end' => 4],
596
                        'value' => '123',
597
                    ],
598
                    [
599
                        'kind'  => NodeKind::STRING,
600
                        'loc'   => ['start' => 5, 'end' => 10],
601
                        'value' => 'abc',
602
                        'block' => false,
603
                    ],
604
                ],
605
            ],
606
            self::nodeToArray(Parser::parseValue('[123 "abc"]'))
607
        );
608
    }
609
610
    /**
611
     * @see it('parses well known types')
612
     */
613
    public function testParsesWellKnownTypes() : void
614
    {
615
        self::assertEquals(
616
            [
617
                'kind' => NodeKind::NAMED_TYPE,
618
                'loc'  => ['start' => 0, 'end' => 6],
619
                'name' => [
620
                    'kind'  => NodeKind::NAME,
621
                    'loc'   => ['start' => 0, 'end' => 6],
622
                    'value' => 'String',
623
                ],
624
            ],
625
            self::nodeToArray(Parser::parseType('String'))
626
        );
627
    }
628
629
    /**
630
     * @see it('parses custom types')
631
     */
632
    public function testParsesCustomTypes() : void
633
    {
634
        self::assertEquals(
635
            [
636
                'kind' => NodeKind::NAMED_TYPE,
637
                'loc'  => ['start' => 0, 'end' => 6],
638
                'name' => [
639
                    'kind'  => NodeKind::NAME,
640
                    'loc'   => ['start' => 0, 'end' => 6],
641
                    'value' => 'MyType',
642
                ],
643
            ],
644
            self::nodeToArray(Parser::parseType('MyType'))
645
        );
646
    }
647
648
    /**
649
     * @see it('parses list types')
650
     */
651
    public function testParsesListTypes() : void
652
    {
653
        self::assertEquals(
654
            [
655
                'kind' => NodeKind::LIST_TYPE,
656
                'loc'  => ['start' => 0, 'end' => 8],
657
                'type' => [
658
                    'kind' => NodeKind::NAMED_TYPE,
659
                    'loc'  => ['start' => 1, 'end' => 7],
660
                    'name' => [
661
                        'kind'  => NodeKind::NAME,
662
                        'loc'   => ['start' => 1, 'end' => 7],
663
                        'value' => 'MyType',
664
                    ],
665
                ],
666
            ],
667
            self::nodeToArray(Parser::parseType('[MyType]'))
668
        );
669
    }
670
671
    /**
672
     * @see it('parses non-null types')
673
     */
674
    public function testParsesNonNullTypes() : void
675
    {
676
        self::assertEquals(
677
            [
678
                'kind' => NodeKind::NON_NULL_TYPE,
679
                'loc'  => ['start' => 0, 'end' => 7],
680
                'type' => [
681
                    'kind' => NodeKind::NAMED_TYPE,
682
                    'loc'  => ['start' => 0, 'end' => 6],
683
                    'name' => [
684
                        'kind'  => NodeKind::NAME,
685
                        'loc'   => ['start' => 0, 'end' => 6],
686
                        'value' => 'MyType',
687
                    ],
688
                ],
689
            ],
690
            self::nodeToArray(Parser::parseType('MyType!'))
691
        );
692
    }
693
694
    /**
695
     * @see it('parses nested types')
696
     */
697
    public function testParsesNestedTypes() : void
698
    {
699
        self::assertEquals(
700
            [
701
                'kind' => NodeKind::LIST_TYPE,
702
                'loc'  => ['start' => 0, 'end' => 9],
703
                'type' => [
704
                    'kind' => NodeKind::NON_NULL_TYPE,
705
                    'loc'  => ['start' => 1, 'end' => 8],
706
                    'type' => [
707
                        'kind' => NodeKind::NAMED_TYPE,
708
                        'loc'  => ['start' => 1, 'end' => 7],
709
                        'name' => [
710
                            'kind'  => NodeKind::NAME,
711
                            'loc'   => ['start' => 1, 'end' => 7],
712
                            'value' => 'MyType',
713
                        ],
714
                    ],
715
                ],
716
            ],
717
            self::nodeToArray(Parser::parseType('[MyType!]'))
718
        );
719
    }
720
721
    public function testPartiallyParsesSource() : void
722
    {
723
        self::assertInstanceOf(
724
            NameNode::class,
725
            Parser::name('Foo')
726
        );
727
728
        self::assertInstanceOf(
729
            ObjectTypeDefinitionNode::class,
730
            Parser::objectTypeDefinition('type Foo { name: String }')
731
        );
732
    }
733
}
734