Completed
Pull Request — master (#69)
by Christoffer
02:11
created

collectConflictsBetweenFragments()   C

Complexity

Conditions 9
Paths 9

Size

Total Lines 69
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 34
nc 9
nop 4
dl 0
loc 69
rs 6.2192
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Digia\GraphQL\Validation\Conflict;
4
5
use Digia\GraphQL\Error\InvalidTypeException;
6
use Digia\GraphQL\Language\Node\FieldNode;
7
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
8
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
9
use Digia\GraphQL\Language\Node\InlineFragmentNode;
10
use Digia\GraphQL\Language\Node\SelectionSetNode;
11
use Digia\GraphQL\Type\Definition\InterfaceType;
12
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
13
use Digia\GraphQL\Type\Definition\ObjectType;
14
use Digia\GraphQL\Validation\ValidationContextInterface;
15
use function Digia\GraphQL\Type\getNamedType;
16
use function Digia\GraphQL\Util\typeFromAST;
17
use function Digia\GraphQL\Validation\compareArguments;
18
use function Digia\GraphQL\Validation\compareTypes;
19
20
/**
21
 * Algorithm:
22
 *
23
 * Conflicts occur when two fields exist in a query which will produce the same
24
 * response name, but represent differing values, thus creating a conflict.
25
 * The algorithm below finds all conflicts via making a series of comparisons
26
 * between fields. In order to compare as few fields as possible, this makes
27
 * a series of comparisons "within" sets of fields and "between" sets of fields.
28
 *
29
 * Given any selection set, a collection produces both a set of fields by
30
 * also including all inline fragments, as well as a list of fragments
31
 * referenced by fragment spreads.
32
 *
33
 * A) Each selection set represented in the document first compares "within" its
34
 * collected set of fields, finding any conflicts between every pair of
35
 * overlapping fields.
36
 * Note: This is the *only time* that a the fields "within" a set are compared
37
 * to each other. After this only fields "between" sets are compared.
38
 *
39
 * B) Also, if any fragment is referenced in a selection set, then a
40
 * comparison is made "between" the original set of fields and the
41
 * referenced fragment.
42
 *
43
 * C) Also, if multiple fragments are referenced, then comparisons
44
 * are made "between" each referenced fragment.
45
 *
46
 * D) When comparing "between" a set of fields and a referenced fragment, first
47
 * a comparison is made between each field in the original set of fields and
48
 * each field in the the referenced set of fields.
49
 *
50
 * E) Also, if any fragment is referenced in the referenced selection set,
51
 * then a comparison is made "between" the original set of fields and the
52
 * referenced fragment (recursively referring to step D).
53
 *
54
 * F) When comparing "between" two fragments, first a comparison is made between
55
 * each field in the first referenced set of fields and each field in the the
56
 * second referenced set of fields.
57
 *
58
 * G) Also, any fragments referenced by the first must be compared to the
59
 * second, and any fragments referenced by the second must be compared to the
60
 * first (recursively referring to step F).
61
 *
62
 * H) When comparing two fields, if both have selection sets, then a comparison
63
 * is made "between" both selection sets, first comparing the set of fields in
64
 * the first selection set with the set of fields in the second.
65
 *
66
 * I) Also, if any fragment is referenced in either selection set, then a
67
 * comparison is made "between" the other set of fields and the
68
 * referenced fragment.
69
 *
70
 * J) Also, if two fragments are referenced in both selection sets, then a
71
 * comparison is made "between" the two fragments.
72
 *
73
 */
74
trait FindsConflictsTrait
75
{
76
    /**
77
     * A cache for the "field map" and list of fragment names found in any given
78
     * selection set. Selection sets may be asked for this information multiple
79
     * times, so this improves the performance of this validator.
80
     *
81
     * @var Map
82
     */
83
    protected $cachedFieldsAndFragmentNames;
84
85
    /**
86
     * A memoization for when two fragments are compared "between" each other for
87
     * conflicts. Two fragments may be compared many times, so memoizing this can
88
     * dramatically improve the performance of this validator.
89
     *
90
     * @var PairSet
91
     */
92
    protected $comparedFragmentPairs;
93
94
    /**
95
     * @return ValidationContextInterface
96
     */
97
    abstract public function getValidationContext(): ValidationContextInterface;
98
99
    /**
100
     * @param SelectionSetNode        $selectionSet
101
     * @param NamedTypeInterface|null $parentType
102
     * @return array|Conflict[]
103
     * @throws InvalidTypeException
104
     */
105
    protected function findConflictsWithinSelectionSet(
106
        SelectionSetNode $selectionSet,
107
        ?NamedTypeInterface $parentType = null
108
    ): array {
109
        $this->cachedFieldsAndFragmentNames = new Map();
110
        $this->comparedFragmentPairs        = new PairSet();
111
112
        $context = $this->getFieldsAndFragmentNames($selectionSet, $parentType);
113
114
        // (A) Find find all conflicts "within" the fields of this selection set.
115
        // Note: this is the *only place* `collectConflictsWithin` is called.
116
        $this->collectConflictsWithin($context);
117
118
        $fieldMap      = $context->getFieldMap();
119
        $fragmentNames = $context->getFragmentNames();
120
121
        // (B) Then collect conflicts between these fields and those represented by
122
        // each spread fragment name found.
123
        if (!empty($fragmentNames)) {
124
            $fragmentNamesCount = \count($fragmentNames);
125
            $comparedFragments  = [];
126
127
            /** @noinspection ForeachInvariantsInspection */
128
            for ($i = 0; $i < $fragmentNamesCount; $i++) {
129
                $this->collectConflictsBetweenFieldsAndFragment(
130
                    $context,
131
                    $comparedFragments,
132
                    $fieldMap,
133
                    $fragmentNames[$i],
134
                    false/* $areMutuallyExclusive */
135
                );
136
137
                // (C) Then compare this fragment with all other fragments found in this
138
                // selection set to collect conflicts between fragments spread together.
139
                // This compares each item in the list of fragment names to every other
140
                // item in that same list (except for itself).
141
                for ($j = $i + 1; $j < $fragmentNamesCount; $j++) {
142
                    $this->collectConflictsBetweenFragments(
143
                        $context,
144
                        $fragmentNames[$i],
145
                        $fragmentNames[$j],
146
                        false/* $areMutuallyExclusive */
147
                    );
148
                }
149
            }
150
        }
151
152
        return $context->getConflicts();
153
    }
154
155
    /**
156
     * Collect all conflicts found between a set of fields and a fragment reference
157
     * including via spreading in any nested fragments.
158
     *
159
     * @param ComparisonContext $context
160
     * @param array             $comparedFragments
161
     * @param array             $fieldMap
162
     * @param string            $fragmentName
163
     * @param bool              $areMutuallyExclusive
164
     * @throws InvalidTypeException
165
     */
166
    protected function collectConflictsBetweenFieldsAndFragment(
167
        ComparisonContext $context,
168
        array &$comparedFragments,
169
        array $fieldMap,
170
        string $fragmentName,
171
        bool $areMutuallyExclusive
172
    ): void {
173
        // Memoize so a fragment is not compared for conflicts more than once.
174
        if (isset($comparedFragments[$fragmentName])) {
175
            return;
176
        }
177
178
        $comparedFragments[$fragmentName] = true;
179
180
        $fragment = $this->getValidationContext()->getFragment($fragmentName);
181
182
        if (null === $fragment) {
183
            return;
184
        }
185
186
        $contextB = $this->getReferencedFieldsAndFragmentNames($fragment);
187
188
        $fieldMapB = $contextB->getFieldMap();
189
190
        // Do not compare a fragment's fieldMap to itself.
191
        if ($fieldMap === $fieldMapB) {
192
            return;
193
        }
194
195
        // (D) First collect any conflicts between the provided collection of fields
196
        // and the collection of fields represented by the given fragment.
197
        $this->collectConflictsBetween(
198
            $context,
199
            $fieldMap,
200
            $fieldMapB,
201
            $areMutuallyExclusive
202
        );
203
204
        $fragmentNamesB = $contextB->getFragmentNames();
205
206
        // (E) Then collect any conflicts between the provided collection of fields
207
        // and any fragment names found in the given fragment.
208
        if (!empty($fragmentNamesB)) {
209
            $fragmentNamesBCount = \count($fragmentNamesB);
210
211
            /** @noinspection ForeachInvariantsInspection */
212
            for ($i = 0; $i < $fragmentNamesBCount; $i++) {
213
                $this->collectConflictsBetweenFieldsAndFragment(
214
                    $context,
215
                    $comparedFragments,
216
                    $fieldMap,
217
                    $fragmentNamesB[$i],
218
                    $areMutuallyExclusive
219
                );
220
            }
221
        }
222
    }
223
224
    /**
225
     * Collect all conflicts found between two fragments, including via spreading in
226
     * any nested fragments.
227
     *
228
     * @param ComparisonContext $context
229
     * @param string            $fragmentNameA
230
     * @param string            $fragmentNameB
231
     * @param bool              $areMutuallyExclusive
232
     * @throws InvalidTypeException
233
     */
234
    protected function collectConflictsBetweenFragments(
235
        ComparisonContext $context,
236
        string $fragmentNameA,
237
        string $fragmentNameB,
238
        bool $areMutuallyExclusive
239
    ): void {
240
        // No need to compare a fragment to itself.
241
        if ($fragmentNameA === $fragmentNameB) {
242
            return;
243
        }
244
245
        // Memoize so two fragments are not compared for conflicts more than once.
246
        if ($this->comparedFragmentPairs->has($fragmentNameA, $fragmentNameB, $areMutuallyExclusive)) {
247
            return;
248
        }
249
250
        $this->comparedFragmentPairs->add($fragmentNameA, $fragmentNameB, $areMutuallyExclusive);
251
252
        $fragmentA = $this->getValidationContext()->getFragment($fragmentNameA);
253
        $fragmentB = $this->getValidationContext()->getFragment($fragmentNameB);
254
255
        if (null === $fragmentA || null === $fragmentB) {
256
            return;
257
        }
258
259
        $contextA = $this->getReferencedFieldsAndFragmentNames($fragmentA);
260
        $contextB = $this->getReferencedFieldsAndFragmentNames($fragmentB);
261
262
        // (F) First, collect all conflicts between these two collections of fields
263
        // (not including any nested fragments).
264
        $this->collectConflictsBetween(
265
            $context,
266
            $contextA->getFieldMap(),
267
            $contextB->getFieldMap(),
268
            $areMutuallyExclusive
269
        );
270
271
        $fragmentNamesB = $contextB->getFragmentNames();
272
273
        // (G) Then collect conflicts between the first fragment and any nested
274
        // fragments spread in the second fragment.
275
        if (!empty($fragmentNamesB)) {
276
            $fragmentNamesBCount = \count($fragmentNamesB);
277
278
            /** @noinspection ForeachInvariantsInspection */
279
            for ($j = 0; $j < $fragmentNamesBCount; $j++) {
280
                $this->collectConflictsBetweenFragments(
281
                    $context,
282
                    $fragmentNameA,
283
                    $fragmentNamesB[$j],
284
                    $areMutuallyExclusive
285
                );
286
            }
287
        }
288
289
        $fragmentNamesA = $contextA->getFragmentNames();
290
291
        // (G) Then collect conflicts between the second fragment and any nested
292
        // fragments spread in the first fragment.
293
        if (!empty($fragmentNamesA)) {
294
            $fragmentNamesACount = \count($fragmentNamesA);
295
296
            /** @noinspection ForeachInvariantsInspection */
297
            for ($i = 0; $i < $fragmentNamesACount; $i++) {
298
                $this->collectConflictsBetweenFragments(
299
                    $context,
300
                    $fragmentNamesA[$i],
301
                    $fragmentNameB,
302
                    $areMutuallyExclusive
303
                );
304
            }
305
        }
306
    }
307
308
    /**
309
     * Find all conflicts found between two selection sets, including those found
310
     * via spreading in fragments. Called when determining if conflicts exist
311
     * between the sub-fields of two overlapping fields.
312
     *
313
     * @param NamedTypeInterface|null $parentTypeA
314
     * @param SelectionSetNode        $selectionSetA
315
     * @param NamedTypeInterface|null $parentTypeB
316
     * @param SelectionSetNode        $selectionSetB
317
     * @param bool                    $areMutuallyExclusive
318
     * @return Conflict[]
319
     * @throws InvalidTypeException
320
     */
321
    protected function findConflictsBetweenSubSelectionSets(
322
        ?NamedTypeInterface $parentTypeA,
323
        SelectionSetNode $selectionSetA,
324
        ?NamedTypeInterface $parentTypeB,
325
        SelectionSetNode $selectionSetB,
326
        bool $areMutuallyExclusive
327
    ): array {
328
        $context = new ComparisonContext();
329
330
        $contextA            = $this->getFieldsAndFragmentNames($selectionSetA, $parentTypeA);
331
        $contextB            = $this->getFieldsAndFragmentNames($selectionSetB, $parentTypeB);
332
        $fieldMapA           = $contextA->getFieldMap();
333
        $fieldMapB           = $contextB->getFieldMap();
334
        $fragmentNamesA      = $contextA->getFragmentNames();
335
        $fragmentNamesB      = $contextB->getFragmentNames();
336
        $fragmentNamesACount = \count($fragmentNamesA);
337
        $fragmentNamesBCount = \count($fragmentNamesB);
338
339
        // (H) First, collect all conflicts between these two collections of field.
340
        $this->collectConflictsBetween(
341
            $context,
342
            $fieldMapA,
343
            $fieldMapB,
344
            $areMutuallyExclusive
345
        );
346
347
        // (I) Then collect conflicts between the first collection of fields and
348
        // those referenced by each fragment name associated with the second.
349
        if (!empty($fragmentNamesB)) {
350
            $comparedFragments = [];
351
352
            /** @noinspection ForeachInvariantsInspection */
353
            for ($j = 0; $j < $fragmentNamesBCount; $j++) {
354
                $this->collectConflictsBetweenFieldsAndFragment(
355
                    $context,
356
                    $comparedFragments,
357
                    $fieldMapA,
358
                    $fragmentNamesB[$j],
359
                    $areMutuallyExclusive
360
                );
361
            }
362
        }
363
364
        // (I) Then collect conflicts between the second collection of fields and
365
        // those referenced by each fragment name associated with the first.
366
        if (!empty($fragmentNamesA)) {
367
            $comparedFragments = [];
368
369
            /** @noinspection ForeachInvariantsInspection */
370
            for ($i = 0; $i < $fragmentNamesACount; $i++) {
371
                $this->collectConflictsBetweenFieldsAndFragment(
372
                    $context,
373
                    $comparedFragments,
374
                    $fieldMapB,
375
                    $fragmentNamesA[$i],
376
                    $areMutuallyExclusive
377
                );
378
            }
379
        }
380
381
        /** @noinspection ForeachInvariantsInspection */
382
        for ($i = 0; $i < $fragmentNamesACount; $i++) {
383
            /** @noinspection ForeachInvariantsInspection */
384
            for ($j = 0; $j < $fragmentNamesBCount; $j++) {
385
                $this->collectConflictsBetweenFragments(
386
                    $context,
387
                    $fragmentNamesA[$i],
388
                    $fragmentNamesB[$j],
389
                    $areMutuallyExclusive
390
                );
391
            }
392
        }
393
394
        return $context->getConflicts();
395
    }
396
397
    /**
398
     * Collect all Conflicts "within" one collection of fields.
399
     *
400
     * @param ComparisonContext $context
401
     * @throws InvalidTypeException
402
     */
403
    protected function collectConflictsWithin(ComparisonContext $context): void
404
    {
405
        // A field map is a keyed collection, where each key represents a response
406
        // name and the value at that key is a list of all fields which provide that
407
        // response name. For every response name, if there are multiple fields, they
408
        // must be compared to find a potential conflict.
409
        foreach ($context->getFieldMap() as $responseName => $fields) {
410
            $fieldsCount = \count($fields);
411
412
            // This compares every field in the list to every other field in this list
413
            // (except to itself). If the list only has one item, nothing needs to
414
            // be compared.
415
            if ($fieldsCount > 1) {
416
                /** @noinspection ForeachInvariantsInspection */
417
                for ($i = 0; $i < $fieldsCount; $i++) {
418
                    for ($j = $i + 1; $j < $fieldsCount; $j++) {
419
                        $conflict = $this->findConflict(
420
                            $responseName,
421
                            $fields[$i],
422
                            $fields[$j],
423
                            // within one collection is never mutually exclusive
424
                            false/* $areMutuallyExclusive */
425
                        );
426
427
                        if (null !== $conflict) {
428
                            $context->reportConflict($conflict);
429
                        }
430
                    }
431
                }
432
            }
433
        }
434
    }
435
436
    /**
437
     * Collect all Conflicts between two collections of fields. This is similar to,
438
     * but different from the `collectConflictsWithin` function above. This check
439
     * assumes that `collectConflictsWithin` has already been called on each
440
     * provided collection of fields. This is true because this validator traverses
441
     * each individual selection set.
442
     *
443
     * @param ComparisonContext $context
444
     * @param array             $fieldMapA
445
     * @param array             $fieldMapB
446
     * @param bool              $parentFieldsAreMutuallyExclusive
447
     * @throws InvalidTypeException
448
     */
449
    protected function collectConflictsBetween(
450
        ComparisonContext $context,
451
        array $fieldMapA,
452
        array $fieldMapB,
453
        bool $parentFieldsAreMutuallyExclusive
454
    ): void {
455
        // A field map is a keyed collection, where each key represents a response
456
        // name and the value at that key is a list of all fields which provide that
457
        // response name. For any response name which appears in both provided field
458
        // maps, each field from the first field map must be compared to every field
459
        // in the second field map to find potential conflicts.
460
        foreach ($fieldMapA as $responseName => $fieldA) {
461
            $fieldB = $fieldMapB[$responseName];
462
463
            if (null !== $fieldB) {
464
                $fieldsACount = \count($fieldA);
465
                $fieldsBCount = \count($fieldB);
466
                /** @noinspection ForeachInvariantsInspection */
467
                for ($i = 0; $i < $fieldsACount; $i++) {
468
                    /** @noinspection ForeachInvariantsInspection */
469
                    for ($j = 0; $j < $fieldsBCount; $j++) {
470
                        $conflict = $this->findConflict(
471
                            $responseName,
472
                            $fieldA[$i],
473
                            $fieldB[$j],
474
                            $parentFieldsAreMutuallyExclusive
475
                        );
476
477
                        if (null !== $conflict) {
478
                            $context->reportConflict($conflict);
479
                        }
480
                    }
481
                }
482
            }
483
        }
484
    }
485
486
    /**
487
     * Determines if there is a conflict between two particular fields, including
488
     * comparing their sub-fields.
489
     *
490
     * @param string       $responseName
491
     * @param FieldContext $fieldA
492
     * @param FieldContext $fieldB
493
     * @param bool         $parentFieldsAreMutuallyExclusive
494
     * @return Conflict|null
495
     * @throws InvalidTypeException
496
     */
497
    protected function findConflict(
498
        string $responseName,
499
        FieldContext $fieldA,
500
        FieldContext $fieldB,
501
        bool $parentFieldsAreMutuallyExclusive
502
    ): ?Conflict {
503
        $parentTypeA = $fieldA->getParentType();
504
        $parentTypeB = $fieldB->getParentType();
505
506
        // If it is known that two fields could not possibly apply at the same
507
        // time, due to the parent types, then it is safe to permit them to diverge
508
        // in aliased field or arguments used as they will not present any ambiguity
509
        // by differing.
510
        // It is known that two parent types could never overlap if they are
511
        // different Object types. Interface or Union types might overlap - if not
512
        // in the current state of the schema, then perhaps in some future version,
513
        // thus may not safely diverge.
514
        $areMutuallyExclusive = $parentFieldsAreMutuallyExclusive
515
            || ($parentTypeA !== $parentTypeB
516
                && $parentTypeA instanceof ObjectType
517
                && $parentTypeB instanceof ObjectType);
518
519
        $nodeA = $fieldA->getNode();
520
        $nodeB = $fieldB->getNode();
521
522
        $definitionA = $fieldA->getDefinition();
523
        $definitionB = $fieldB->getDefinition();
524
525
        if (!$areMutuallyExclusive) {
526
            // Two aliases must refer to the same field.
527
            $nameA = $nodeA->getNameValue();
528
            $nameB = $nodeB->getNameValue();
529
530
            if ($nameA !== $nameB) {
531
                return new Conflict(
532
                    $responseName,
533
                    sprintf('%s and %s are different fields', $nameA, $nameB),
534
                    [$nodeA],
535
                    [$nodeB]
536
                );
537
            }
538
539
            // Two field calls must have the same arguments.
540
            if (!compareArguments($nodeA->getArguments(), $nodeB->getArguments())) {
541
                return new Conflict(
542
                    $responseName,
543
                    'they have differing arguments',
544
                    [$nodeA],
545
                    [$nodeB]
546
                );
547
            }
548
        }
549
550
        // The return type for each field.
551
        $typeA = null !== $definitionA ? $definitionA->getType() : null;
552
        $typeB = null !== $definitionB ? $definitionB->getType() : null;
553
554
        if (null !== $typeA && null !== $typeB && compareTypes($typeA, $typeB)) {
555
            return new Conflict(
556
                $responseName,
557
                sprintf('they return conflicting types %s and %s', (string)$typeA, (string)$typeB),
558
                [$nodeA],
559
                [$nodeB]
560
            );
561
        }
562
563
        // Collect and compare sub-fields. Use the same "visited fragment names" list
564
        // for both collections so fields in a fragment reference are never
565
        // compared to themselves.
566
        $selectionSetA = $nodeA->getSelectionSet();
567
        $selectionSetB = $nodeB->getSelectionSet();
568
569
        if (null !== $selectionSetA && null !== $selectionSetB) {
570
            $conflicts = $this->findConflictsBetweenSubSelectionSets(
571
                getNamedType($typeA),
572
                $selectionSetA,
573
                getNamedType($typeB),
574
                $selectionSetB,
575
                $areMutuallyExclusive
576
            );
577
578
            return $this->subfieldConflicts($conflicts, $responseName, $nodeA, $nodeB);
579
        }
580
581
        return null;
582
    }
583
584
    /**
585
     * Given a selection set, return the collection of fields (a mapping of response
586
     * name to field nodes and definitions) as well as a list of fragment names
587
     * referenced via fragment spreads.
588
     *
589
     * @param SelectionSetNode        $selectionSet
590
     * @param NamedTypeInterface|null $parentType
591
     * @return ComparisonContext
592
     * @throws InvalidTypeException
593
     */
594
    protected function getFieldsAndFragmentNames(
595
        SelectionSetNode $selectionSet,
596
        ?NamedTypeInterface $parentType
597
    ): ComparisonContext {
598
        $cached = $this->cachedFieldsAndFragmentNames->get($selectionSet) ?? null;
599
600
        if (null === $cached) {
601
            $cached = new ComparisonContext();
602
603
            $this->collectFieldsAndFragmentNames($cached, $selectionSet, $parentType);
604
605
            $this->cachedFieldsAndFragmentNames->set($selectionSet, $cached);
606
        }
607
608
        return $cached;
609
    }
610
611
    /**
612
     * Given a reference to a fragment, return the represented collection of fields
613
     * as well as a list of nested fragment names referenced via fragment spreads.
614
     *
615
     * @param FragmentDefinitionNode $fragment
616
     * @return ComparisonContext
617
     * @throws InvalidTypeException
618
     */
619
    protected function getReferencedFieldsAndFragmentNames(FragmentDefinitionNode $fragment): ComparisonContext
620
    {
621
        $cached = $this->cachedFieldsAndFragmentNames->get($fragment) ?? null;
622
623
        if (null !== $cached) {
624
            return $cached;
625
        }
626
627
        /** @var NamedTypeInterface $fragmentType */
628
        $fragmentType = typeFromAST($this->getValidationContext()->getSchema(), $fragment->getTypeCondition());
629
630
        return $this->getFieldsAndFragmentNames($fragment->getSelectionSet(), $fragmentType);
631
    }
632
633
    /**
634
     * @param ComparisonContext       $context
635
     * @param SelectionSetNode        $selectionSet
636
     * @param NamedTypeInterface|null $parentType
637
     * @throws InvalidTypeException
638
     */
639
    protected function collectFieldsAndFragmentNames(
640
        ComparisonContext $context,
641
        SelectionSetNode $selectionSet,
642
        ?NamedTypeInterface $parentType
643
    ): void {
644
        foreach ($selectionSet->getSelections() as $selection) {
645
            if ($selection instanceof FieldNode) {
646
                $definition = ($parentType instanceof ObjectType || $parentType instanceof InterfaceType)
647
                    ? $parentType->getFields()[$selection->getNameValue()]
648
                    : null;
649
650
                $context->registerField(new FieldContext($parentType, $selection, $definition));
0 ignored issues
show
Bug introduced by
It seems like $parentType can also be of type Digia\GraphQL\Type\Definition\NamedTypeInterface; however, parameter $parentType of Digia\GraphQL\Validation...dContext::__construct() does only seem to accept null|Digia\GraphQL\Type\...\CompositeTypeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

650
                $context->registerField(new FieldContext(/** @scrutinizer ignore-type */ $parentType, $selection, $definition));
Loading history...
651
            } elseif ($selection instanceof FragmentSpreadNode) {
652
                $context->registerFragment($selection);
653
            } elseif ($selection instanceof InlineFragmentNode) {
654
                $typeCondition = $selection->getTypeCondition();
655
656
                $inlineFragmentType = null !== $typeCondition
657
                    ? typeFromAST($this->getValidationContext()->getSchema(), $typeCondition)
658
                    : $parentType;
659
660
                $this->collectFieldsAndFragmentNames($context, $selection->getSelectionSet(), $inlineFragmentType);
0 ignored issues
show
Bug introduced by
It seems like $inlineFragmentType can also be of type Digia\GraphQL\Type\Definition\TypeInterface; however, parameter $parentType of Digia\GraphQL\Validation...ieldsAndFragmentNames() does only seem to accept null|Digia\GraphQL\Type\...tion\NamedTypeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

660
                $this->collectFieldsAndFragmentNames($context, $selection->getSelectionSet(), /** @scrutinizer ignore-type */ $inlineFragmentType);
Loading history...
661
            }
662
        }
663
    }
664
665
    /**
666
     * Given a series of Conflicts which occurred between two sub-fields, generate
667
     * a single Conflict.
668
     *
669
     * @param array|Conflict[] $conflicts
670
     * @param string           $responseName
671
     * @param FieldNode        $nodeA
672
     * @param FieldNode        $nodeB
673
     * @return Conflict|null
674
     */
675
    protected function subfieldConflicts(
676
        array $conflicts,
677
        string $responseName,
678
        FieldNode $nodeA,
679
        FieldNode $nodeB
680
    ): ?Conflict {
681
        if (empty($conflicts)) {
682
            return null;
683
        }
684
685
        return new Conflict(
686
            $responseName,
687
            array_map(function (Conflict $conflict) {
688
                return $conflict->getReason();
689
            }, $conflicts),
690
            array_reduce($conflicts, function ($allFields, Conflict $conflict) {
691
                return array_merge($allFields, $conflict->getFieldsA());
692
            }, [$nodeA]),
693
            array_reduce($conflicts, function ($allFields, Conflict $conflict) {
694
                return array_merge($allFields, $conflict->getFieldsB());
695
            }, [$nodeB])
696
        );
697
    }
698
}
699