Passed
Pull Request — master (#225)
by Sam
02:13
created

ConflictFinder   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 627
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 627
rs 2.8959
c 0
b 0
f 0
wmc 67

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getFieldsAndFragmentNames() 0 13 2
B collectConflictsBetweenFieldsAndFragment() 0 53 6
B collectConflictsWithin() 0 26 6
D findConflict() 0 85 14
A __construct() 0 4 1
B collectConflictsBetween() 0 30 6
C findConflictsBetweenSubSelectionSets() 0 77 7
C collectConflictsBetweenFragments() 0 69 9
C collectFieldsAndFragmentNames() 0 22 8
B findConflictsWithinSelectionSet() 0 45 4
A getReferencedFieldsAndFragmentNames() 0 10 2
A subfieldConflicts() 0 21 2

How to fix   Complexity   

Complex Class

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

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

658
                $context->registerField(new FieldContext(/** @scrutinizer ignore-type */ $parentType, $selection, $definition));
Loading history...
659
            } elseif ($selection instanceof FragmentSpreadNode) {
660
                $context->registerFragment($selection);
661
            } elseif ($selection instanceof InlineFragmentNode) {
662
                $typeCondition = $selection->getTypeCondition();
663
664
                $inlineFragmentType = null !== $typeCondition
665
                    ? typeFromAST($this->getContext()->getSchema(), $typeCondition)
666
                    : $parentType;
667
668
                $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

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