Passed
Pull Request — master (#586)
by Šimon
12:52
created

ParserTest   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 713
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 43
eloc 312
c 1
b 0
f 0
dl 0
loc 713
rs 8.96

How to fix   Complexity   

Complex Class

Complex classes like ParserTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ParserTest, and based on these observations, apply Extract Interface, too.

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