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

OverlappingFieldsCanBeMergedTest   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 1329
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 42
eloc 818
c 3
b 0
f 0
dl 0
loc 1329
rs 8.8219

How to fix   Complexity   

Complex Class

Complex classes like OverlappingFieldsCanBeMergedTest 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 OverlappingFieldsCanBeMergedTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Validator;
6
7
use GraphQL\Error\FormattedError;
8
use GraphQL\Language\SourceLocation;
9
use GraphQL\Type\Definition\InterfaceType;
10
use GraphQL\Type\Definition\ObjectType;
11
use GraphQL\Type\Definition\Type;
12
use GraphQL\Type\Schema;
13
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
14
15
class OverlappingFieldsCanBeMergedTest extends ValidatorTestCase
16
{
17
    // Validate: Overlapping fields can be merged
18
    /**
19
     * @see it('unique fields')
20
     */
21
    public function testUniqueFields() : void
22
    {
23
        $this->expectPassesRule(
24
            new OverlappingFieldsCanBeMerged(),
25
            '
26
      fragment uniqueFields on Dog {
27
        name
28
        nickname
29
      }
30
        '
31
        );
32
    }
33
34
    /**
35
     * @see it('identical fields')
36
     */
37
    public function testIdenticalFields() : void
38
    {
39
        $this->expectPassesRule(
40
            new OverlappingFieldsCanBeMerged(),
41
            '
42
      fragment mergeIdenticalFields on Dog {
43
        name
44
        name
45
      }
46
        '
47
        );
48
    }
49
50
    /**
51
     * @see it('identical fields with identical args')
52
     */
53
    public function testIdenticalFieldsWithIdenticalArgs() : void
54
    {
55
        $this->expectPassesRule(
56
            new OverlappingFieldsCanBeMerged(),
57
            '
58
      fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
59
        doesKnowCommand(dogCommand: SIT)
60
        doesKnowCommand(dogCommand: SIT)
61
      }
62
        '
63
        );
64
    }
65
66
    /**
67
     * @see it('identical fields with identical directives')
68
     */
69
    public function testIdenticalFieldsWithIdenticalDirectives() : void
70
    {
71
        $this->expectPassesRule(
72
            new OverlappingFieldsCanBeMerged(),
73
            '
74
      fragment mergeSameFieldsWithSameDirectives on Dog {
75
        name @include(if: true)
76
        name @include(if: true)
77
      }
78
        '
79
        );
80
    }
81
82
    /**
83
     * @see it('different args with different aliases')
84
     */
85
    public function testDifferentArgsWithDifferentAliases() : void
86
    {
87
        $this->expectPassesRule(
88
            new OverlappingFieldsCanBeMerged(),
89
            '
90
      fragment differentArgsWithDifferentAliases on Dog {
91
        knowsSit : doesKnowCommand(dogCommand: SIT)
92
        knowsDown : doesKnowCommand(dogCommand: DOWN)
93
      }
94
        '
95
        );
96
    }
97
98
    /**
99
     * @see it('different directives with different aliases')
100
     */
101
    public function testDifferentDirectivesWithDifferentAliases() : void
102
    {
103
        $this->expectPassesRule(
104
            new OverlappingFieldsCanBeMerged(),
105
            '
106
      fragment differentDirectivesWithDifferentAliases on Dog {
107
        nameIfTrue : name @include(if: true)
108
        nameIfFalse : name @include(if: false)
109
      }
110
        '
111
        );
112
    }
113
114
    /**
115
     * @see it('different skip/include directives accepted')
116
     */
117
    public function testDifferentSkipIncludeDirectivesAccepted() : void
118
    {
119
        // Note: Differing skip/include directives don't create an ambiguous return
120
        // value and are acceptable in conditions where differing runtime values
121
        // may have the same desired effect of including or skipping a field.
122
        $this->expectPassesRule(
123
            new OverlappingFieldsCanBeMerged(),
124
            '
125
      fragment differentDirectivesWithDifferentAliases on Dog {
126
        name @include(if: true)
127
        name @include(if: false)
128
      }
129
    '
130
        );
131
    }
132
133
    /**
134
     * @see it('Same aliases with different field targets')
135
     */
136
    public function testSameAliasesWithDifferentFieldTargets() : void
137
    {
138
        $this->expectFailsRule(
139
            new OverlappingFieldsCanBeMerged(),
140
            '
141
      fragment sameAliasesWithDifferentFieldTargets on Dog {
142
        fido : name
143
        fido : nickname
144
      }
145
        ',
146
            [
147
                FormattedError::create(
148
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
149
                        'fido',
150
                        'name and nickname are different fields'
151
                    ),
152
                    [new SourceLocation(3, 9), new SourceLocation(4, 9)]
153
                ),
154
            ]
155
        );
156
    }
157
158
    /**
159
     * @see it('Same aliases allowed on non-overlapping fields')
160
     */
161
    public function testSameAliasesAllowedOnNonOverlappingFields() : void
162
    {
163
        // This is valid since no object can be both a "Dog" and a "Cat", thus
164
        // these fields can never overlap.
165
        $this->expectPassesRule(
166
            new OverlappingFieldsCanBeMerged(),
167
            '
168
      fragment sameAliasesWithDifferentFieldTargets on Pet {
169
        ... on Dog {
170
          name
171
        }
172
        ... on Cat {
173
          name: nickname
174
        }
175
      }
176
        '
177
        );
178
    }
179
180
    /**
181
     * @see it('Alias masking direct field access')
182
     */
183
    public function testAliasMaskingDirectFieldAccess() : void
184
    {
185
        $this->expectFailsRule(
186
            new OverlappingFieldsCanBeMerged(),
187
            '
188
      fragment aliasMaskingDirectFieldAccess on Dog {
189
        name : nickname
190
        name
191
      }
192
        ',
193
            [
194
                FormattedError::create(
195
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
196
                        'name',
197
                        'nickname and name are different fields'
198
                    ),
199
                    [new SourceLocation(3, 9), new SourceLocation(4, 9)]
200
                ),
201
            ]
202
        );
203
    }
204
205
    /**
206
     * @see it('different args, second adds an argument')
207
     */
208
    public function testDifferentArgsSecondAddsAnArgument() : void
209
    {
210
        $this->expectFailsRule(
211
            new OverlappingFieldsCanBeMerged(),
212
            '
213
      fragment conflictingArgs on Dog {
214
        doesKnowCommand
215
        doesKnowCommand(dogCommand: HEEL)
216
      }
217
        ',
218
            [
219
                FormattedError::create(
220
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
221
                        'doesKnowCommand',
222
                        'they have differing arguments'
223
                    ),
224
                    [new SourceLocation(3, 9), new SourceLocation(4, 9)]
225
                ),
226
            ]
227
        );
228
    }
229
230
    /**
231
     * @see it('different args, second missing an argument')
232
     */
233
    public function testDifferentArgsSecondMissingAnArgument() : void
234
    {
235
        $this->expectFailsRule(
236
            new OverlappingFieldsCanBeMerged(),
237
            '
238
      fragment conflictingArgs on Dog {
239
        doesKnowCommand(dogCommand: SIT)
240
        doesKnowCommand
241
      }
242
        ',
243
            [
244
                FormattedError::create(
245
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
246
                        'doesKnowCommand',
247
                        'they have differing arguments'
248
                    ),
249
                    [new SourceLocation(3, 9), new SourceLocation(4, 9)]
250
                ),
251
            ]
252
        );
253
    }
254
255
    /**
256
     * @see it('conflicting args')
257
     */
258
    public function testConflictingArgs() : void
259
    {
260
        $this->expectFailsRule(
261
            new OverlappingFieldsCanBeMerged(),
262
            '
263
      fragment conflictingArgs on Dog {
264
        doesKnowCommand(dogCommand: SIT)
265
        doesKnowCommand(dogCommand: HEEL)
266
      }
267
        ',
268
            [
269
                FormattedError::create(
270
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
271
                        'doesKnowCommand',
272
                        'they have differing arguments'
273
                    ),
274
                    [new SourceLocation(3, 9), new SourceLocation(4, 9)]
275
                ),
276
            ]
277
        );
278
    }
279
280
    /**
281
     * @see it('allows different args where no conflict is possible')
282
     */
283
    public function testAllowsDifferentArgsWhereNoConflictIsPossible() : void
284
    {
285
        // This is valid since no object can be both a "Dog" and a "Cat", thus
286
        // these fields can never overlap.
287
        $this->expectPassesRule(
288
            new OverlappingFieldsCanBeMerged(),
289
            '
290
      fragment conflictingArgs on Pet {
291
        ... on Dog {
292
          name(surname: true)
293
        }
294
        ... on Cat {
295
          name
296
        }
297
      }
298
        '
299
        );
300
    }
301
302
    /**
303
     * @see it('encounters conflict in fragments')
304
     */
305
    public function testEncountersConflictInFragments() : void
306
    {
307
        $this->expectFailsRule(
308
            new OverlappingFieldsCanBeMerged(),
309
            '
310
      {
311
        ...A
312
        ...B
313
      }
314
      fragment A on Type {
315
        x: a
316
      }
317
      fragment B on Type {
318
        x: b
319
      }
320
        ',
321
            [
322
                FormattedError::create(
323
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
324
                    [new SourceLocation(7, 9), new SourceLocation(10, 9)]
325
                ),
326
            ]
327
        );
328
    }
329
330
    /**
331
     * @see it('reports each conflict once')
332
     */
333
    public function testReportsEachConflictOnce() : void
334
    {
335
        $this->expectFailsRule(
336
            new OverlappingFieldsCanBeMerged(),
337
            '
338
      {
339
        f1 {
340
          ...A
341
          ...B
342
        }
343
        f2 {
344
          ...B
345
          ...A
346
        }
347
        f3 {
348
          ...A
349
          ...B
350
          x: c
351
        }
352
      }
353
      fragment A on Type {
354
        x: a
355
      }
356
      fragment B on Type {
357
        x: b
358
      }
359
    ',
360
            [
361
                FormattedError::create(
362
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
363
                    [new SourceLocation(18, 9), new SourceLocation(21, 9)]
364
                ),
365
                FormattedError::create(
366
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and a are different fields'),
367
                    [new SourceLocation(14, 11), new SourceLocation(18, 9)]
368
                ),
369
                FormattedError::create(
370
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and b are different fields'),
371
                    [new SourceLocation(14, 11), new SourceLocation(21, 9)]
372
                ),
373
            ]
374
        );
375
    }
376
377
    /**
378
     * @see it('deep conflict')
379
     */
380
    public function testDeepConflict() : void
381
    {
382
        $this->expectFailsRule(
383
            new OverlappingFieldsCanBeMerged(),
384
            '
385
      {
386
        field {
387
          x: a
388
        },
389
        field {
390
          x: b
391
        }
392
      }
393
        ',
394
            [
395
                FormattedError::create(
396
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
397
                        'field',
398
                        [['x', 'a and b are different fields']]
399
                    ),
400
                    [
401
                        new SourceLocation(3, 9),
402
                        new SourceLocation(4, 11),
403
                        new SourceLocation(6, 9),
404
                        new SourceLocation(7, 11),
405
                    ]
406
                ),
407
            ]
408
        );
409
    }
410
411
    /**
412
     * @see it('deep conflict with multiple issues')
413
     */
414
    public function testDeepConflictWithMultipleIssues() : void
415
    {
416
        $this->expectFailsRule(
417
            new OverlappingFieldsCanBeMerged(),
418
            '
419
      {
420
        field {
421
          x: a
422
          y: c
423
        },
424
        field {
425
          x: b
426
          y: d
427
        }
428
      }
429
        ',
430
            [
431
                FormattedError::create(
432
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
433
                        'field',
434
                        [
435
                            ['x', 'a and b are different fields'],
436
                            ['y', 'c and d are different fields'],
437
                        ]
438
                    ),
439
                    [
440
                        new SourceLocation(3, 9),
441
                        new SourceLocation(4, 11),
442
                        new SourceLocation(5, 11),
443
                        new SourceLocation(7, 9),
444
                        new SourceLocation(8, 11),
445
                        new SourceLocation(9, 11),
446
                    ]
447
                ),
448
            ]
449
        );
450
    }
451
452
    /**
453
     * @see it('very deep conflict')
454
     */
455
    public function testVeryDeepConflict() : void
456
    {
457
        $this->expectFailsRule(
458
            new OverlappingFieldsCanBeMerged(),
459
            '
460
      {
461
        field {
462
          deepField {
463
            x: a
464
          }
465
        },
466
        field {
467
          deepField {
468
            x: b
469
          }
470
        }
471
      }
472
        ',
473
            [
474
                FormattedError::create(
475
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
476
                        'field',
477
                        [['deepField', [['x', 'a and b are different fields']]]]
478
                    ),
479
                    [
480
                        new SourceLocation(3, 9),
481
                        new SourceLocation(4, 11),
482
                        new SourceLocation(5, 13),
483
                        new SourceLocation(8, 9),
484
                        new SourceLocation(9, 11),
485
                        new SourceLocation(10, 13),
486
                    ]
487
                ),
488
            ]
489
        );
490
    }
491
492
    /**
493
     * @see it('reports deep conflict to nearest common ancestor')
494
     */
495
    public function testReportsDeepConflictToNearestCommonAncestor() : void
496
    {
497
        $this->expectFailsRule(
498
            new OverlappingFieldsCanBeMerged(),
499
            '
500
      {
501
        field {
502
          deepField {
503
            x: a
504
          }
505
          deepField {
506
            x: b
507
          }
508
        },
509
        field {
510
          deepField {
511
            y
512
          }
513
        }
514
      }
515
        ',
516
            [
517
                FormattedError::create(
518
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
519
                        'deepField',
520
                        [['x', 'a and b are different fields']]
521
                    ),
522
                    [
523
                        new SourceLocation(4, 11),
524
                        new SourceLocation(5, 13),
525
                        new SourceLocation(7, 11),
526
                        new SourceLocation(8, 13),
527
                    ]
528
                ),
529
            ]
530
        );
531
    }
532
533
    /**
534
     * @see it('reports deep conflict to nearest common ancestor in fragments')
535
     */
536
    public function testReportsDeepConflictToNearestCommonAncestorInFragments() : void
537
    {
538
        $this->expectFailsRule(
539
            new OverlappingFieldsCanBeMerged(),
540
            '
541
      {
542
        field {
543
          ...F
544
        }
545
        field {
546
          ...F
547
        }
548
      }
549
      fragment F on T {
550
        deepField {
551
          deeperField {
552
            x: a
553
          }
554
          deeperField {
555
            x: b
556
          }
557
        }
558
        deepField {
559
          deeperField {
560
            y
561
          }
562
        }
563
      }
564
        ',
565
            [
566
                FormattedError::create(
567
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
568
                        'deeperField',
569
                        [['x', 'a and b are different fields']]
570
                    ),
571
                    [
572
                        new SourceLocation(12, 11),
573
                        new SourceLocation(13, 13),
574
                        new SourceLocation(15, 11),
575
                        new SourceLocation(16, 13),
576
                    ]
577
                ),
578
            ]
579
        );
580
    }
581
582
    /**
583
     * @see it('reports deep conflict in nested fragments')
584
     */
585
    public function testReportsDeepConflictInNestedFragments() : void
586
    {
587
        $this->expectFailsRule(
588
            new OverlappingFieldsCanBeMerged(),
589
            '
590
      {
591
        field {
592
          ...F
593
        }
594
        field {
595
          ...I
596
        }
597
      }
598
      fragment F on T {
599
        x: a
600
        ...G
601
      }
602
      fragment G on T {
603
        y: c
604
      }
605
      fragment I on T {
606
        y: d
607
        ...J
608
      }
609
      fragment J on T {
610
        x: b
611
      }
612
        ',
613
            [
614
                FormattedError::create(
615
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
616
                        'field',
617
                        [
618
                            ['x', 'a and b are different fields'],
619
                            ['y', 'c and d are different fields'],
620
                        ]
621
                    ),
622
                    [
623
                        new SourceLocation(3, 9),
624
                        new SourceLocation(11, 9),
625
                        new SourceLocation(15, 9),
626
                        new SourceLocation(6, 9),
627
                        new SourceLocation(22, 9),
628
                        new SourceLocation(18, 9),
629
                    ]
630
                ),
631
            ]
632
        );
633
    }
634
635
    /**
636
     * @see it('ignores unknown fragments')
637
     */
638
    public function testIgnoresUnknownFragments() : void
639
    {
640
        $this->expectPassesRule(
641
            new OverlappingFieldsCanBeMerged(),
642
            '
643
      {
644
        field {
645
          ...Unknown
646
          ...Known
647
        }
648
      }
649
      fragment Known on T {
650
        field
651
        ...OtherUnknown
652
      }
653
        '
654
        );
655
    }
656
657
    // Describe: return types must be unambiguous
658
659
    /**
660
     * @see it('conflicting return types which potentially overlap')
661
     */
662
    public function testConflictingReturnTypesWhichPotentiallyOverlap() : void
663
    {
664
        // This is invalid since an object could potentially be both the Object
665
        // type IntBox and the interface type NonNullStringBox1. While that
666
        // condition does not exist in the current schema, the schema could
667
        // expand in the future to allow this. Thus it is invalid.
668
        $this->expectFailsRuleWithSchema(
669
            $this->getSchema(),
670
            new OverlappingFieldsCanBeMerged(),
671
            '
672
        {
673
          someBox {
674
            ...on IntBox {
675
              scalar
676
            }
677
            ...on NonNullStringBox1 {
678
              scalar
679
            }
680
          }
681
        }
682
        ',
683
            [
684
                FormattedError::create(
685
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
686
                        'scalar',
687
                        'they return conflicting types Int and String!'
688
                    ),
689
                    [
690
                        new SourceLocation(5, 15),
691
                        new SourceLocation(8, 15),
692
                    ]
693
                ),
694
            ]
695
        );
696
    }
697
698
    private function getSchema()
699
    {
700
        $StringBox = null;
701
        $IntBox    = null;
702
        $SomeBox   = null;
703
704
        $SomeBox = new InterfaceType([
705
            'name'   => 'SomeBox',
706
            'fields' => static function () use (&$SomeBox) {
707
                return [
708
                    'deepBox'        => ['type' => $SomeBox],
709
                    'unrelatedField' => ['type' => Type::string()],
710
                ];
711
            },
712
        ]);
713
714
        $StringBox = new ObjectType([
715
            'name'       => 'StringBox',
716
            'interfaces' => [$SomeBox],
717
            'fields'     => static function () use (&$StringBox, &$IntBox) {
718
                return [
719
                    'scalar'         => ['type' => Type::string()],
720
                    'deepBox'        => ['type' => $StringBox],
721
                    'unrelatedField' => ['type' => Type::string()],
722
                    'listStringBox'  => ['type' => Type::listOf($StringBox)],
723
                    'stringBox'      => ['type' => $StringBox],
724
                    'intBox'         => ['type' => $IntBox],
725
                ];
726
            },
727
        ]);
728
729
        $IntBox = new ObjectType([
730
            'name'       => 'IntBox',
731
            'interfaces' => [$SomeBox],
732
            'fields'     => static function () use (&$StringBox, &$IntBox) {
733
                return [
734
                    'scalar'         => ['type' => Type::int()],
735
                    'deepBox'        => ['type' => $IntBox],
736
                    'unrelatedField' => ['type' => Type::string()],
737
                    'listStringBox'  => ['type' => Type::listOf($StringBox)],
738
                    'stringBox'      => ['type' => $StringBox],
739
                    'intBox'         => ['type' => $IntBox],
740
                ];
741
            },
742
        ]);
743
744
        $NonNullStringBox1 = new InterfaceType([
745
            'name'   => 'NonNullStringBox1',
746
            'fields' => [
747
                'scalar' => ['type' => Type::nonNull(Type::string())],
748
            ],
749
        ]);
750
751
        $NonNullStringBox1Impl = new ObjectType([
752
            'name'       => 'NonNullStringBox1Impl',
753
            'interfaces' => [$SomeBox, $NonNullStringBox1],
754
            'fields'     => [
755
                'scalar'         => ['type' => Type::nonNull(Type::string())],
756
                'unrelatedField' => ['type' => Type::string()],
757
                'deepBox'        => ['type' => $SomeBox],
758
            ],
759
        ]);
760
761
        $NonNullStringBox2 = new InterfaceType([
762
            'name'   => 'NonNullStringBox2',
763
            'fields' => [
764
                'scalar' => ['type' => Type::nonNull(Type::string())],
765
            ],
766
        ]);
767
768
        $NonNullStringBox2Impl = new ObjectType([
769
            'name'       => 'NonNullStringBox2Impl',
770
            'interfaces' => [$SomeBox, $NonNullStringBox2],
771
            'fields'     => [
772
                'scalar'         => ['type' => Type::nonNull(Type::string())],
773
                'unrelatedField' => ['type' => Type::string()],
774
                'deepBox'        => ['type' => $SomeBox],
775
            ],
776
        ]);
777
778
        $Connection = new ObjectType([
779
            'name'   => 'Connection',
780
            'fields' => [
781
                'edges' => [
782
                    'type' => Type::listOf(new ObjectType([
783
                        'name'   => 'Edge',
784
                        'fields' => [
785
                            'node' => [
786
                                'type' => new ObjectType([
787
                                    'name'   => 'Node',
788
                                    'fields' => [
789
                                        'id'   => ['type' => Type::id()],
790
                                        'name' => ['type' => Type::string()],
791
                                    ],
792
                                ]),
793
                            ],
794
                        ],
795
                    ])),
796
                ],
797
            ],
798
        ]);
799
800
        return new Schema([
801
            'query' => new ObjectType([
802
                'name'   => 'QueryRoot',
803
                'fields' => [
804
                    'someBox'    => ['type' => $SomeBox],
805
                    'connection' => ['type' => $Connection],
806
                ],
807
            ]),
808
            'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl],
809
        ]);
810
    }
811
812
    /**
813
     * @see it('compatible return shapes on different return types')
814
     */
815
    public function testCompatibleReturnShapesOnDifferentReturnTypes() : void
816
    {
817
        // In this case `deepBox` returns `SomeBox` in the first usage, and
818
        // `StringBox` in the second usage. These return types are not the same!
819
        // however this is valid because the return *shapes* are compatible.
820
        $this->expectPassesRuleWithSchema(
821
            $this->getSchema(),
822
            new OverlappingFieldsCanBeMerged(),
823
            '
824
      {
825
        someBox {
826
          ... on SomeBox {
827
            deepBox {
828
              unrelatedField
829
            }
830
          }
831
          ... on StringBox {
832
            deepBox {
833
              unrelatedField
834
            }
835
          }
836
        }
837
      }
838
        '
839
        );
840
    }
841
842
    /**
843
     * @see it('disallows differing return types despite no overlap')
844
     */
845
    public function testDisallowsDifferingReturnTypesDespiteNoOverlap() : void
846
    {
847
        $this->expectFailsRuleWithSchema(
848
            $this->getSchema(),
849
            new OverlappingFieldsCanBeMerged(),
850
            '
851
        {
852
          someBox {
853
            ... on IntBox {
854
              scalar
855
            }
856
            ... on StringBox {
857
              scalar
858
            }
859
          }
860
        }
861
        ',
862
            [
863
                FormattedError::create(
864
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
865
                        'scalar',
866
                        'they return conflicting types Int and String'
867
                    ),
868
                    [
869
                        new SourceLocation(5, 15),
870
                        new SourceLocation(8, 15),
871
                    ]
872
                ),
873
            ]
874
        );
875
    }
876
877
    /**
878
     * @see it('reports correctly when a non-exclusive follows an exclusive')
879
     */
880
    public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive() : void
881
    {
882
        $this->expectFailsRuleWithSchema(
883
            $this->getSchema(),
884
            new OverlappingFieldsCanBeMerged(),
885
            '
886
        {
887
          someBox {
888
            ... on IntBox {
889
              deepBox {
890
                ...X
891
              }
892
            }
893
          }
894
          someBox {
895
            ... on StringBox {
896
              deepBox {
897
                ...Y
898
              }
899
            }
900
          }
901
          memoed: someBox {
902
            ... on IntBox {
903
              deepBox {
904
                ...X
905
              }
906
            }
907
          }
908
          memoed: someBox {
909
            ... on StringBox {
910
              deepBox {
911
                ...Y
912
              }
913
            }
914
          }
915
          other: someBox {
916
            ...X
917
          }
918
          other: someBox {
919
            ...Y
920
          }
921
        }
922
        fragment X on SomeBox {
923
          scalar
924
        }
925
        fragment Y on SomeBox {
926
          scalar: unrelatedField
927
        }
928
        ',
929
            [
930
                FormattedError::create(
931
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
932
                        'other',
933
                        [['scalar', 'scalar and unrelatedField are different fields']]
934
                    ),
935
                    [
936
                        new SourceLocation(31, 11),
937
                        new SourceLocation(39, 11),
938
                        new SourceLocation(34, 11),
939
                        new SourceLocation(42, 11),
940
                    ]
941
                ),
942
            ]
943
        );
944
    }
945
946
    /**
947
     * @see it('disallows differing return type nullability despite no overlap')
948
     */
949
    public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap() : void
950
    {
951
        $this->expectFailsRuleWithSchema(
952
            $this->getSchema(),
953
            new OverlappingFieldsCanBeMerged(),
954
            '
955
        {
956
          someBox {
957
            ... on NonNullStringBox1 {
958
              scalar
959
            }
960
            ... on StringBox {
961
              scalar
962
            }
963
          }
964
        }
965
        ',
966
            [
967
                FormattedError::create(
968
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
969
                        'scalar',
970
                        'they return conflicting types String! and String'
971
                    ),
972
                    [
973
                        new SourceLocation(5, 15),
974
                        new SourceLocation(8, 15),
975
                    ]
976
                ),
977
            ]
978
        );
979
    }
980
981
    /**
982
     * @see it('disallows differing return type list despite no overlap')
983
     */
984
    public function testDisallowsDifferingReturnTypeListDespiteNoOverlap() : void
985
    {
986
        $this->expectFailsRuleWithSchema(
987
            $this->getSchema(),
988
            new OverlappingFieldsCanBeMerged(),
989
            '
990
        {
991
          someBox {
992
            ... on IntBox {
993
              box: listStringBox {
994
                scalar
995
              }
996
            }
997
            ... on StringBox {
998
              box: stringBox {
999
                scalar
1000
              }
1001
            }
1002
          }
1003
        }
1004
        ',
1005
            [
1006
                FormattedError::create(
1007
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1008
                        'box',
1009
                        'they return conflicting types [StringBox] and StringBox'
1010
                    ),
1011
                    [
1012
                        new SourceLocation(5, 15),
1013
                        new SourceLocation(10, 15),
1014
                    ]
1015
                ),
1016
            ]
1017
        );
1018
1019
        $this->expectFailsRuleWithSchema(
1020
            $this->getSchema(),
1021
            new OverlappingFieldsCanBeMerged(),
1022
            '
1023
        {
1024
          someBox {
1025
            ... on IntBox {
1026
              box: stringBox {
1027
                scalar
1028
              }
1029
            }
1030
            ... on StringBox {
1031
              box: listStringBox {
1032
                scalar
1033
              }
1034
            }
1035
          }
1036
        }
1037
        ',
1038
            [
1039
                FormattedError::create(
1040
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1041
                        'box',
1042
                        'they return conflicting types StringBox and [StringBox]'
1043
                    ),
1044
                    [
1045
                        new SourceLocation(5, 15),
1046
                        new SourceLocation(10, 15),
1047
                    ]
1048
                ),
1049
            ]
1050
        );
1051
    }
1052
1053
    public function testDisallowsDifferingSubfields() : void
1054
    {
1055
        $this->expectFailsRuleWithSchema(
1056
            $this->getSchema(),
1057
            new OverlappingFieldsCanBeMerged(),
1058
            '
1059
        {
1060
          someBox {
1061
            ... on IntBox {
1062
              box: stringBox {
1063
                val: scalar
1064
                val: unrelatedField
1065
              }
1066
            }
1067
            ... on StringBox {
1068
              box: stringBox {
1069
                val: scalar
1070
              }
1071
            }
1072
          }
1073
        }
1074
        ',
1075
            [
1076
1077
                FormattedError::create(
1078
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1079
                        'val',
1080
                        'scalar and unrelatedField are different fields'
1081
                    ),
1082
                    [
1083
                        new SourceLocation(6, 17),
1084
                        new SourceLocation(7, 17),
1085
                    ]
1086
                ),
1087
            ]
1088
        );
1089
    }
1090
1091
    /**
1092
     * @see it('disallows differing deep return types despite no overlap')
1093
     */
1094
    public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap() : void
1095
    {
1096
        $this->expectFailsRuleWithSchema(
1097
            $this->getSchema(),
1098
            new OverlappingFieldsCanBeMerged(),
1099
            '
1100
        {
1101
          someBox {
1102
            ... on IntBox {
1103
              box: stringBox {
1104
                scalar
1105
              }
1106
            }
1107
            ... on StringBox {
1108
              box: intBox {
1109
                scalar
1110
              }
1111
            }
1112
          }
1113
        }
1114
        ',
1115
            [
1116
                FormattedError::create(
1117
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1118
                        'box',
1119
                        [['scalar', 'they return conflicting types String and Int']]
1120
                    ),
1121
                    [
1122
                        new SourceLocation(5, 15),
1123
                        new SourceLocation(6, 17),
1124
                        new SourceLocation(10, 15),
1125
                        new SourceLocation(11, 17),
1126
                    ]
1127
                ),
1128
            ]
1129
        );
1130
    }
1131
1132
    /**
1133
     * @see it('allows non-conflicting overlaping types')
1134
     */
1135
    public function testAllowsNonConflictingOverlapingTypes() : void
1136
    {
1137
        $this->expectPassesRuleWithSchema(
1138
            $this->getSchema(),
1139
            new OverlappingFieldsCanBeMerged(),
1140
            '
1141
        {
1142
          someBox {
1143
            ... on IntBox {
1144
              scalar: unrelatedField
1145
            }
1146
            ... on StringBox {
1147
              scalar
1148
            }
1149
          }
1150
        }
1151
        '
1152
        );
1153
    }
1154
1155
    /**
1156
     * @see it('same wrapped scalar return types')
1157
     */
1158
    public function testSameWrappedScalarReturnTypes() : void
1159
    {
1160
        $this->expectPassesRuleWithSchema(
1161
            $this->getSchema(),
1162
            new OverlappingFieldsCanBeMerged(),
1163
            '
1164
        {
1165
          someBox {
1166
            ...on NonNullStringBox1 {
1167
              scalar
1168
            }
1169
            ...on NonNullStringBox2 {
1170
              scalar
1171
            }
1172
          }
1173
        }
1174
        '
1175
        );
1176
    }
1177
1178
    /**
1179
     * @see it('allows inline typeless fragments')
1180
     */
1181
    public function testAllowsInlineTypelessFragments() : void
1182
    {
1183
        $this->expectPassesRuleWithSchema(
1184
            $this->getSchema(),
1185
            new OverlappingFieldsCanBeMerged(),
1186
            '
1187
        {
1188
          a
1189
          ... {
1190
            a
1191
          }
1192
        }
1193
        '
1194
        );
1195
    }
1196
1197
    /**
1198
     * @see it('compares deep types including list')
1199
     */
1200
    public function testComparesDeepTypesIncludingList() : void
1201
    {
1202
        $this->expectFailsRuleWithSchema(
1203
            $this->getSchema(),
1204
            new OverlappingFieldsCanBeMerged(),
1205
            '
1206
        {
1207
          connection {
1208
            ...edgeID
1209
            edges {
1210
              node {
1211
                id: name
1212
              }
1213
            }
1214
          }
1215
        }
1216
1217
        fragment edgeID on Connection {
1218
          edges {
1219
            node {
1220
              id
1221
            }
1222
          }
1223
        }
1224
      ',
1225
            [
1226
                FormattedError::create(
1227
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1228
                        'edges',
1229
                        [['node', [['id', 'name and id are different fields']]]]
1230
                    ),
1231
                    [
1232
                        new SourceLocation(5, 13),
1233
                        new SourceLocation(6, 15),
1234
                        new SourceLocation(7, 17),
1235
                        new SourceLocation(14, 11),
1236
                        new SourceLocation(15, 13),
1237
                        new SourceLocation(16, 15),
1238
                    ]
1239
                ),
1240
            ]
1241
        );
1242
    }
1243
1244
    /**
1245
     * @see it('ignores unknown types')
1246
     */
1247
    public function testIgnoresUnknownTypes() : void
1248
    {
1249
        $this->expectPassesRuleWithSchema(
1250
            $this->getSchema(),
1251
            new OverlappingFieldsCanBeMerged(),
1252
            '
1253
        {
1254
          someBox {
1255
            ...on UnknownType {
1256
              scalar
1257
            }
1258
            ...on NonNullStringBox2 {
1259
              scalar
1260
            }
1261
          }
1262
        }
1263
        '
1264
        );
1265
    }
1266
1267
    /**
1268
     * @see it('error message contains hint for alias conflict')
1269
     */
1270
    public function testErrorMessageContainsHintForAliasConflict() : void
1271
    {
1272
        // The error template should end with a hint for the user to try using
1273
        // different aliases.
1274
        $error = OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields');
1275
        $hint  = 'Use different aliases on the fields to fetch both if this was intentional.';
1276
1277
        self::assertStringEndsWith($hint, $error);
1278
    }
1279
1280
    /**
1281
     * @see it('does not infinite loop on recursive fragment')
1282
     */
1283
    public function testDoesNotInfiniteLoopOnRecursiveFragment() : void
1284
    {
1285
        $this->expectPassesRule(
1286
            new OverlappingFieldsCanBeMerged(),
1287
            '
1288
        fragment fragA on Human { name, relatives { name, ...fragA } }
1289
        '
1290
        );
1291
    }
1292
1293
    /**
1294
     * @see it('does not infinite loop on immediately recursive fragment')
1295
     */
1296
    public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment() : void
1297
    {
1298
        $this->expectPassesRule(
1299
            new OverlappingFieldsCanBeMerged(),
1300
            '
1301
        fragment fragA on Human { name, ...fragA }
1302
        '
1303
        );
1304
    }
1305
1306
    /**
1307
     * @see it('does not infinite loop on transitively recursive fragment')
1308
     */
1309
    public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment() : void
1310
    {
1311
        $this->expectPassesRule(
1312
            new OverlappingFieldsCanBeMerged(),
1313
            '
1314
        fragment fragA on Human { name, ...fragB }
1315
        fragment fragB on Human { name, ...fragC }
1316
        fragment fragC on Human { name, ...fragA }
1317
        '
1318
        );
1319
    }
1320
1321
    /**
1322
     * @see it('find invalid case even with immediately recursive fragment')
1323
     */
1324
    public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment() : void
1325
    {
1326
        $this->expectFailsRule(
1327
            new OverlappingFieldsCanBeMerged(),
1328
            '
1329
      fragment sameAliasesWithDifferentFieldTargets on Dob {
1330
        ...sameAliasesWithDifferentFieldTargets
1331
        fido: name
1332
        fido: nickname
1333
      }
1334
        ',
1335
            [
1336
                FormattedError::create(
1337
                    OverlappingFieldsCanBeMerged::fieldsConflictMessage(
1338
                        'fido',
1339
                        'name and nickname are different fields'
1340
                    ),
1341
                    [
1342
                        new SourceLocation(4, 9),
1343
                        new SourceLocation(5, 9),
1344
                    ]
1345
                ),
1346
            ]
1347
        );
1348
    }
1349
}
1350