Completed
Push — master ( 0e0bd4...d314c1 )
by Christoffer
03:35 queued 01:20
created

FindsConflictsTrait   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 629
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 66
dl 0
loc 629
rs 3.0063
c 0
b 0
f 0

11 Methods

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

How to fix   Complexity   

Complex Class

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

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

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