findConflictsBetweenSubSelectionSets()   B
last analyzed

Complexity

Conditions 7
Paths 12

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

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

652
        $fragmentType = TypeASTConverter::convert($this->getContext()->getSchema(), /** @scrutinizer ignore-type */ $fragment->getTypeCondition());
Loading history...
653
654
        return $this->getFieldsAndFragmentNames($fragment->getSelectionSet(), $fragmentType);
0 ignored issues
show
Bug introduced by
It seems like $fragment->getSelectionSet() can also be of type null; however, parameter $selectionSet of Digia\GraphQL\Validation...ieldsAndFragmentNames() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode, 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

654
        return $this->getFieldsAndFragmentNames(/** @scrutinizer ignore-type */ $fragment->getSelectionSet(), $fragmentType);
Loading history...
655
    }
656
657
    /**
658
     * @param ComparisonContext       $context
659
     * @param SelectionSetNode        $selectionSet
660
     * @param NamedTypeInterface|null $parentType
661
     * @throws InvalidTypeException
662
     * @throws InvariantException
663
     * @throws ConversionException
664
     */
665
    protected function collectFieldsAndFragmentNames(
666
        ComparisonContext $context,
667
        SelectionSetNode $selectionSet,
668
        ?NamedTypeInterface $parentType
669
    ): void {
670
        foreach ($selectionSet->getSelections() as $selection) {
671
            if ($selection instanceof FieldNode) {
672
                $definition = ($parentType instanceof ObjectType || $parentType instanceof InterfaceType)
673
                    ? ($parentType->getFields()[$selection->getNameValue()] ?? null)
674
                    : null;
675
676
                $context->registerField(new FieldContext($parentType, $selection, $definition));
677
            } elseif ($selection instanceof FragmentSpreadNode) {
678
                $context->registerFragment($selection);
679
            } elseif ($selection instanceof InlineFragmentNode) {
680
                $typeCondition = $selection->getTypeCondition();
681
682
                $inlineFragmentType = null !== $typeCondition
683
                    ? TypeASTConverter::convert($this->getContext()->getSchema(), $typeCondition)
684
                    : $parentType;
685
686
                $this->collectFieldsAndFragmentNames($context, $selection->getSelectionSet(), $inlineFragmentType);
0 ignored issues
show
Bug introduced by
It seems like $selection->getSelectionSet() can also be of type null; however, parameter $selectionSet of Digia\GraphQL\Validation...ieldsAndFragmentNames() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode, 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

686
                $this->collectFieldsAndFragmentNames($context, /** @scrutinizer ignore-type */ $selection->getSelectionSet(), $inlineFragmentType);
Loading history...
687
            }
688
        }
689
    }
690
691
    /**
692
     * Given a series of Conflicts which occurred between two sub-fields, generate
693
     * a single Conflict.
694
     *
695
     * @param array|Conflict[] $conflicts
696
     * @param string           $responseName
697
     * @param FieldNode        $nodeA
698
     * @param FieldNode        $nodeB
699
     * @return Conflict|null
700
     */
701
    protected function subfieldConflicts(
702
        array $conflicts,
703
        string $responseName,
704
        FieldNode $nodeA,
705
        FieldNode $nodeB
706
    ): ?Conflict {
707
        if (empty($conflicts)) {
708
            return null;
709
        }
710
711
        return new Conflict(
712
            $responseName,
713
            array_map(function (Conflict $conflict) {
714
                return [$conflict->getResponseName(), $conflict->getReason()];
715
            }, $conflicts),
716
            array_reduce($conflicts, function ($allFields, Conflict $conflict) {
717
                return array_merge($allFields, $conflict->getFieldsA());
718
            }, [$nodeA]),
719
            array_reduce($conflicts, function ($allFields, Conflict $conflict) {
720
                return array_merge($allFields, $conflict->getFieldsB());
721
            }, [$nodeB])
722
        );
723
    }
724
}
725