Completed
Pull Request — master (#45)
by Christoffer
02:13
created

FindsConflictsTrait::collectConflictsWithin()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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