OverlappingFieldsCanBeMerged::getVisitor()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 19
ccs 12
cts 12
cp 1
rs 9.8333
c 0
b 0
f 0
cc 2
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Validator\Rules;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\ArgumentNode;
9
use GraphQL\Language\AST\FieldNode;
10
use GraphQL\Language\AST\FragmentDefinitionNode;
11
use GraphQL\Language\AST\FragmentSpreadNode;
12
use GraphQL\Language\AST\InlineFragmentNode;
13
use GraphQL\Language\AST\Node;
14
use GraphQL\Language\AST\NodeKind;
15
use GraphQL\Language\AST\SelectionSetNode;
16
use GraphQL\Language\Printer;
17
use GraphQL\Type\Definition\CompositeType;
18
use GraphQL\Type\Definition\InterfaceType;
19
use GraphQL\Type\Definition\ListOfType;
20
use GraphQL\Type\Definition\NonNull;
21
use GraphQL\Type\Definition\ObjectType;
22
use GraphQL\Type\Definition\Type;
23
use GraphQL\Utils\PairSet;
24
use GraphQL\Utils\TypeInfo;
25
use GraphQL\Validator\ValidationContext;
26
use SplObjectStorage;
27
use function array_keys;
28
use function array_map;
29
use function array_merge;
30
use function array_reduce;
31
use function count;
32
use function implode;
33
use function is_array;
34
use function sprintf;
35
36
class OverlappingFieldsCanBeMerged extends ValidationRule
37
{
38
    /**
39
     * A memoization for when two fragments are compared "between" each other for
40
     * conflicts. Two fragments may be compared many times, so memoizing this can
41
     * dramatically improve the performance of this validator.
42
     *
43
     * @var PairSet
44
     */
45
    private $comparedFragmentPairs;
46
47
    /**
48
     * A cache for the "field map" and list of fragment names found in any given
49
     * selection set. Selection sets may be asked for this information multiple
50
     * times, so this improves the performance of this validator.
51
     *
52
     * @var SplObjectStorage
53
     */
54
    private $cachedFieldsAndFragmentNames;
55
56 159
    public function getVisitor(ValidationContext $context)
57
    {
58 159
        $this->comparedFragmentPairs        = new PairSet();
59 159
        $this->cachedFieldsAndFragmentNames = new SplObjectStorage();
60
61
        return [
62
            NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) {
63 159
                $conflicts = $this->findConflictsWithinSelectionSet(
64 159
                    $context,
65 159
                    $context->getParentType(),
66 159
                    $selectionSet
67
                );
68
69 159
                foreach ($conflicts as $conflict) {
70 22
                    [[$responseName, $reason], $fields1, $fields2] = $conflict;
71
72 22
                    $context->reportError(new Error(
73 22
                        self::fieldsConflictMessage($responseName, $reason),
74 22
                        array_merge($fields1, $fields2)
75
                    ));
76
                }
77 159
            },
78
        ];
79
    }
80
81
    /**
82
     * Find all conflicts found "within" a selection set, including those found
83
     * via spreading in fragments. Called when visiting each SelectionSet in the
84
     * GraphQL Document.
85
     *
86
     * @param CompositeType $parentType
87
     *
88
     * @return mixed[]
89
     */
90 159
    private function findConflictsWithinSelectionSet(
91
        ValidationContext $context,
92
        $parentType,
93
        SelectionSetNode $selectionSet
94
    ) {
95 159
        [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames(
96 159
            $context,
97 159
            $parentType,
98 159
            $selectionSet
99
        );
100
101 159
        $conflicts = [];
102
103
        // (A) Find find all conflicts "within" the fields of this selection set.
104
        // Note: this is the *only place* `collectConflictsWithin` is called.
105 159
        $this->collectConflictsWithin(
106 159
            $context,
107 159
            $conflicts,
108 159
            $fieldMap
109
        );
110
111 159
        $fragmentNamesLength = count($fragmentNames);
112 159
        if ($fragmentNamesLength !== 0) {
113
            // (B) Then collect conflicts between these fields and those represented by
114
            // each spread fragment name found.
115 24
            $comparedFragments = [];
116 24
            for ($i = 0; $i < $fragmentNamesLength; $i++) {
117 24
                $this->collectConflictsBetweenFieldsAndFragment(
118 24
                    $context,
119 24
                    $conflicts,
120 24
                    $comparedFragments,
121 24
                    false,
122 24
                    $fieldMap,
123 24
                    $fragmentNames[$i]
124
                );
125
                // (C) Then compare this fragment with all other fragments found in this
126
                // selection set to collect conflicts between fragments spread together.
127
                // This compares each item in the list of fragment names to every other item
128
                // in that same list (except for itself).
129 24
                for ($j = $i + 1; $j < $fragmentNamesLength; $j++) {
130 5
                    $this->collectConflictsBetweenFragments(
131 5
                        $context,
132 5
                        $conflicts,
133 5
                        false,
134 5
                        $fragmentNames[$i],
135 5
                        $fragmentNames[$j]
136
                    );
137
                }
138
            }
139
        }
140
141 159
        return $conflicts;
142
    }
143
144
    /**
145
     * Given a selection set, return the collection of fields (a mapping of response
146
     * name to field ASTs and definitions) as well as a list of fragment names
147
     * referenced via fragment spreads.
148
     *
149
     * @param CompositeType $parentType
150
     *
151
     * @return mixed[]|SplObjectStorage
152
     */
153 159
    private function getFieldsAndFragmentNames(
154
        ValidationContext $context,
155
        $parentType,
156
        SelectionSetNode $selectionSet
157
    ) {
158 159
        if (isset($this->cachedFieldsAndFragmentNames[$selectionSet])) {
159 28
            $cached = $this->cachedFieldsAndFragmentNames[$selectionSet];
160
        } else {
161 159
            $astAndDefs    = [];
162 159
            $fragmentNames = [];
163
164 159
            $this->internalCollectFieldsAndFragmentNames(
165 159
                $context,
166 159
                $parentType,
167 159
                $selectionSet,
168 159
                $astAndDefs,
169 159
                $fragmentNames
170
            );
171 159
            $cached                                            = [$astAndDefs, array_keys($fragmentNames)];
172 159
            $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached;
173
        }
174
175 159
        return $cached;
176
    }
177
178
    /**
179
     * Algorithm:
180
     *
181
     * Conflicts occur when two fields exist in a query which will produce the same
182
     * response name, but represent differing values, thus creating a conflict.
183
     * The algorithm below finds all conflicts via making a series of comparisons
184
     * between fields. In order to compare as few fields as possible, this makes
185
     * a series of comparisons "within" sets of fields and "between" sets of fields.
186
     *
187
     * Given any selection set, a collection produces both a set of fields by
188
     * also including all inline fragments, as well as a list of fragments
189
     * referenced by fragment spreads.
190
     *
191
     * A) Each selection set represented in the document first compares "within" its
192
     * collected set of fields, finding any conflicts between every pair of
193
     * overlapping fields.
194
     * Note: This is the *only time* that a the fields "within" a set are compared
195
     * to each other. After this only fields "between" sets are compared.
196
     *
197
     * B) Also, if any fragment is referenced in a selection set, then a
198
     * comparison is made "between" the original set of fields and the
199
     * referenced fragment.
200
     *
201
     * C) Also, if multiple fragments are referenced, then comparisons
202
     * are made "between" each referenced fragment.
203
     *
204
     * D) When comparing "between" a set of fields and a referenced fragment, first
205
     * a comparison is made between each field in the original set of fields and
206
     * each field in the the referenced set of fields.
207
     *
208
     * E) Also, if any fragment is referenced in the referenced selection set,
209
     * then a comparison is made "between" the original set of fields and the
210
     * referenced fragment (recursively referring to step D).
211
     *
212
     * F) When comparing "between" two fragments, first a comparison is made between
213
     * each field in the first referenced set of fields and each field in the the
214
     * second referenced set of fields.
215
     *
216
     * G) Also, any fragments referenced by the first must be compared to the
217
     * second, and any fragments referenced by the second must be compared to the
218
     * first (recursively referring to step F).
219
     *
220
     * H) When comparing two fields, if both have selection sets, then a comparison
221
     * is made "between" both selection sets, first comparing the set of fields in
222
     * the first selection set with the set of fields in the second.
223
     *
224
     * I) Also, if any fragment is referenced in either selection set, then a
225
     * comparison is made "between" the other set of fields and the
226
     * referenced fragment.
227
     *
228
     * J) Also, if two fragments are referenced in both selection sets, then a
229
     * comparison is made "between" the two fragments.
230
     */
231
232
    /**
233
     * Given a reference to a fragment, return the represented collection of fields
234
     * as well as a list of nested fragment names referenced via fragment spreads.
235
     *
236
     * @param CompositeType $parentType
237
     * @param mixed[][][]   $astAndDefs
238
     * @param bool[]        $fragmentNames
239
     */
240 159
    private function internalCollectFieldsAndFragmentNames(
241
        ValidationContext $context,
242
        $parentType,
243
        SelectionSetNode $selectionSet,
244
        array &$astAndDefs,
245
        array &$fragmentNames
246
    ) {
247 159
        foreach ($selectionSet->selections as $selection) {
248
            switch (true) {
249 159
                case $selection instanceof FieldNode:
250 159
                    $fieldName = $selection->name->value;
251 159
                    $fieldDef  = null;
252 159
                    if ($parentType instanceof ObjectType ||
253 159
                        $parentType instanceof InterfaceType) {
254 157
                        $tmp = $parentType->getFields();
255 157
                        if (isset($tmp[$fieldName])) {
256 143
                            $fieldDef = $tmp[$fieldName];
257
                        }
258
                    }
259 159
                    $responseName = $selection->alias ? $selection->alias->value : $fieldName;
260
261 159
                    if (! isset($astAndDefs[$responseName])) {
262 159
                        $astAndDefs[$responseName] = [];
263
                    }
264 159
                    $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
265 159
                    break;
266 53
                case $selection instanceof FragmentSpreadNode:
267 24
                    $fragmentNames[$selection->name->value] = true;
268 24
                    break;
269 36
                case $selection instanceof InlineFragmentNode:
270 36
                    $typeCondition      = $selection->typeCondition;
271 36
                    $inlineFragmentType = $typeCondition
272 35
                        ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition)
273 36
                        : $parentType;
274
275 36
                    $this->internalCollectFieldsAndFragmentNames(
276 36
                        $context,
277 36
                        $inlineFragmentType,
0 ignored issues
show
Bug introduced by
It seems like $inlineFragmentType can also be of type GraphQL\Type\Definition\Type; however, parameter $parentType of GraphQL\Validator\Rules\...ieldsAndFragmentNames() does only seem to accept GraphQL\Type\Definition\CompositeType, 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

277
                        /** @scrutinizer ignore-type */ $inlineFragmentType,
Loading history...
278 36
                        $selection->selectionSet,
279 36
                        $astAndDefs,
280 36
                        $fragmentNames
281
                    );
282 159
                    break;
283
            }
284
        }
285 159
    }
286
287
    /**
288
     * Collect all Conflicts "within" one collection of fields.
289
     *
290
     * @param mixed[][] $conflicts
291
     * @param mixed[][] $fieldMap
292
     */
293 159
    private function collectConflictsWithin(
294
        ValidationContext $context,
295
        array &$conflicts,
296
        array $fieldMap
297
    ) {
298
        // A field map is a keyed collection, where each key represents a response
299
        // name and the value at that key is a list of all fields which provide that
300
        // response name. For every response name, if there are multiple fields, they
301
        // must be compared to find a potential conflict.
302 159
        foreach ($fieldMap as $responseName => $fields) {
303
            // This compares every field in the list to every other field in this list
304
            // (except to itself). If the list only has one item, nothing needs to
305
            // be compared.
306 159
            $fieldsLength = count($fields);
307 159
            if ($fieldsLength <= 1) {
308 149
                continue;
309
            }
310
311 34
            for ($i = 0; $i < $fieldsLength; $i++) {
312 34
                for ($j = $i + 1; $j < $fieldsLength; $j++) {
313 34
                    $conflict = $this->findConflict(
314 34
                        $context,
315 34
                        false, // within one collection is never mutually exclusive
316 34
                        $responseName,
317 34
                        $fields[$i],
318 34
                        $fields[$j]
319
                    );
320 34
                    if (! $conflict) {
321 19
                        continue;
322
                    }
323
324 19
                    $conflicts[] = $conflict;
325
                }
326
            }
327
        }
328 159
    }
329
330
    /**
331
     * Determines if there is a conflict between two particular fields, including
332
     * comparing their sub-fields.
333
     *
334
     * @param bool    $parentFieldsAreMutuallyExclusive
335
     * @param string  $responseName
336
     * @param mixed[] $field1
337
     * @param mixed[] $field2
338
     *
339
     * @return mixed[]|null
340
     */
341 41
    private function findConflict(
342
        ValidationContext $context,
343
        $parentFieldsAreMutuallyExclusive,
344
        $responseName,
345
        array $field1,
346
        array $field2
347
    ) {
348 41
        [$parentType1, $ast1, $def1] = $field1;
349 41
        [$parentType2, $ast2, $def2] = $field2;
350
351
        // If it is known that two fields could not possibly apply at the same
352
        // time, due to the parent types, then it is safe to permit them to diverge
353
        // in aliased field or arguments used as they will not present any ambiguity
354
        // by differing.
355
        // It is known that two parent types could never overlap if they are
356
        // different Object types. Interface or Union types might overlap - if not
357
        // in the current state of the schema, then perhaps in some future version,
358
        // thus may not safely diverge.
359
        $areMutuallyExclusive =
360 41
            $parentFieldsAreMutuallyExclusive ||
361
            (
362 41
                $parentType1 !== $parentType2 &&
363 41
                $parentType1 instanceof ObjectType &&
364 41
                $parentType2 instanceof ObjectType
365
            );
366
367
        // The return type for each field.
368 41
        $type1 = $def1 === null ? null : $def1->getType();
369 41
        $type2 = $def2 === null ? null : $def2->getType();
370
371 41
        if (! $areMutuallyExclusive) {
372
            // Two aliases must refer to the same field.
373 31
            $name1 = $ast1->name->value;
374 31
            $name2 = $ast2->name->value;
375 31
            if ($name1 !== $name2) {
376
                return [
377 14
                    [$responseName, sprintf('%s and %s are different fields', $name1, $name2)],
378 14
                    [$ast1],
379 14
                    [$ast2],
380
                ];
381
            }
382
383 25
            if (! $this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) {
384
                return [
385 3
                    [$responseName, 'they have differing arguments'],
386 3
                    [$ast1],
387 3
                    [$ast2],
388
                ];
389
            }
390
        }
391
392 33
        if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) {
393
            return [
394 5
                [$responseName, sprintf('they return conflicting types %s and %s', $type1, $type2)],
395 5
                [$ast1],
396 5
                [$ast2],
397
            ];
398
        }
399
400
        // Collect and compare sub-fields. Use the same "visited fragment names" list
401
        // for both collections so fields in a fragment reference are never
402
        // compared to themselves.
403 29
        $selectionSet1 = $ast1->selectionSet;
404 29
        $selectionSet2 = $ast2->selectionSet;
405 29
        if ($selectionSet1 && $selectionSet2) {
406 13
            $conflicts = $this->findConflictsBetweenSubSelectionSets(
407 13
                $context,
408 13
                $areMutuallyExclusive,
409 13
                Type::getNamedType($type1),
0 ignored issues
show
Bug introduced by
It seems like GraphQL\Type\Definition\Type::getNamedType($type1) can also be of type GraphQL\Type\Definition\Type; however, parameter $parentType1 of GraphQL\Validator\Rules\...tweenSubSelectionSets() does only seem to accept GraphQL\Type\Definition\CompositeType, 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

409
                /** @scrutinizer ignore-type */ Type::getNamedType($type1),
Loading history...
410 13
                $selectionSet1,
411 13
                Type::getNamedType($type2),
0 ignored issues
show
Bug introduced by
It seems like GraphQL\Type\Definition\Type::getNamedType($type2) can also be of type GraphQL\Type\Definition\Type; however, parameter $parentType2 of GraphQL\Validator\Rules\...tweenSubSelectionSets() does only seem to accept GraphQL\Type\Definition\CompositeType, 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

411
                /** @scrutinizer ignore-type */ Type::getNamedType($type2),
Loading history...
412 13
                $selectionSet2
413
            );
414
415 13
            return $this->subfieldConflicts(
416 13
                $conflicts,
417 13
                $responseName,
418 13
                $ast1,
419 13
                $ast2
420
            );
421
        }
422
423 19
        return null;
424
    }
425
426
    /**
427
     * @param ArgumentNode[] $arguments1
428
     * @param ArgumentNode[] $arguments2
429
     *
430
     * @return bool
431
     */
432 25
    private function sameArguments($arguments1, $arguments2)
433
    {
434 25
        if (count($arguments1) !== count($arguments2)) {
435 2
            return false;
436
        }
437 23
        foreach ($arguments1 as $argument1) {
438 2
            $argument2 = null;
439 2
            foreach ($arguments2 as $argument) {
440 2
                if ($argument->name->value === $argument1->name->value) {
441 2
                    $argument2 = $argument;
442 2
                    break;
443
                }
444
            }
445 2
            if (! $argument2) {
446
                return false;
447
            }
448
449 2
            if (! $this->sameValue($argument1->value, $argument2->value)) {
450 2
                return false;
451
            }
452
        }
453
454 22
        return true;
455
    }
456
457
    /**
458
     * @return bool
459
     */
460 2
    private function sameValue(Node $value1, Node $value2)
461
    {
462 2
        return (! $value1 && ! $value2) || (Printer::doPrint($value1) === Printer::doPrint($value2));
463
    }
464
465
    /**
466
     * Two types conflict if both types could not apply to a value simultaneously.
467
     * Composite types are ignored as their individual field types will be compared
468
     * later recursively. However List and Non-Null types must match.
469
     */
470 25
    private function doTypesConflict(Type $type1, Type $type2) : bool
471
    {
472 25
        if ($type1 instanceof ListOfType) {
473 4
            return $type2 instanceof ListOfType
474 3
                ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
475 4
                : true;
476
        }
477 25
        if ($type2 instanceof ListOfType) {
478 1
            return $type1 instanceof ListOfType
479
                ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
480 1
                : true;
481
        }
482 24
        if ($type1 instanceof NonNull) {
483 2
            return $type2 instanceof NonNull
484 1
                ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
485 2
                : true;
486
        }
487 23
        if ($type2 instanceof NonNull) {
488 1
            return $type1 instanceof NonNull
489
                ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
490 1
                : true;
491
        }
492 22
        if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
493 18
            return $type1 !== $type2;
494
        }
495
496 7
        return false;
497
    }
498
499
    /**
500
     * Find all conflicts found between two selection sets, including those found
501
     * via spreading in fragments. Called when determining if conflicts exist
502
     * between the sub-fields of two overlapping fields.
503
     *
504
     * @param bool          $areMutuallyExclusive
505
     * @param CompositeType $parentType1
506
     * @param CompositeType $parentType2
507
     *
508
     * @return mixed[][]
509
     */
510 13
    private function findConflictsBetweenSubSelectionSets(
511
        ValidationContext $context,
512
        $areMutuallyExclusive,
513
        $parentType1,
514
        SelectionSetNode $selectionSet1,
515
        $parentType2,
516
        SelectionSetNode $selectionSet2
517
    ) {
518 13
        $conflicts = [];
519
520 13
        [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames(
521 13
            $context,
522 13
            $parentType1,
523 13
            $selectionSet1
524
        );
525 13
        [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames(
526 13
            $context,
527 13
            $parentType2,
528 13
            $selectionSet2
529
        );
530
531
        // (H) First, collect all conflicts between these two collections of field.
532 13
        $this->collectConflictsBetween(
533 13
            $context,
534 13
            $conflicts,
535 13
            $areMutuallyExclusive,
536 13
            $fieldMap1,
537 13
            $fieldMap2
538
        );
539
540
        // (I) Then collect conflicts between the first collection of fields and
541
        // those referenced by each fragment name associated with the second.
542 13
        $fragmentNames2Length = count($fragmentNames2);
543 13
        if ($fragmentNames2Length !== 0) {
544 3
            $comparedFragments = [];
545 3
            for ($j = 0; $j < $fragmentNames2Length; $j++) {
546 3
                $this->collectConflictsBetweenFieldsAndFragment(
547 3
                    $context,
548 3
                    $conflicts,
549 3
                    $comparedFragments,
550 3
                    $areMutuallyExclusive,
551 3
                    $fieldMap1,
552 3
                    $fragmentNames2[$j]
553
                );
554
            }
555
        }
556
557
        // (I) Then collect conflicts between the second collection of fields and
558
        // those referenced by each fragment name associated with the first.
559 13
        $fragmentNames1Length = count($fragmentNames1);
560 13
        if ($fragmentNames1Length !== 0) {
561 3
            $comparedFragments = [];
562 3
            for ($i = 0; $i < $fragmentNames1Length; $i++) {
563 3
                $this->collectConflictsBetweenFieldsAndFragment(
564 3
                    $context,
565 3
                    $conflicts,
566 3
                    $comparedFragments,
567 3
                    $areMutuallyExclusive,
568 3
                    $fieldMap2,
569 3
                    $fragmentNames1[$i]
570
                );
571
            }
572
        }
573
574
        // (J) Also collect conflicts between any fragment names by the first and
575
        // fragment names by the second. This compares each item in the first set of
576
        // names to each item in the second set of names.
577 13
        for ($i = 0; $i < $fragmentNames1Length; $i++) {
578 3
            for ($j = 0; $j < $fragmentNames2Length; $j++) {
579 3
                $this->collectConflictsBetweenFragments(
580 3
                    $context,
581 3
                    $conflicts,
582 3
                    $areMutuallyExclusive,
583 3
                    $fragmentNames1[$i],
584 3
                    $fragmentNames2[$j]
585
                );
586
            }
587
        }
588
589 13
        return $conflicts;
590
    }
591
592
    /**
593
     * Collect all Conflicts between two collections of fields. This is similar to,
594
     * but different from the `collectConflictsWithin` function above. This check
595
     * assumes that `collectConflictsWithin` has already been called on each
596
     * provided collection of fields. This is true because this validator traverses
597
     * each individual selection set.
598
     *
599
     * @param mixed[][] $conflicts
600
     * @param bool      $parentFieldsAreMutuallyExclusive
601
     * @param mixed[]   $fieldMap1
602
     * @param mixed[]   $fieldMap2
603
     */
604 29
    private function collectConflictsBetween(
605
        ValidationContext $context,
606
        array &$conflicts,
607
        $parentFieldsAreMutuallyExclusive,
608
        array $fieldMap1,
609
        array $fieldMap2
610
    ) {
611
        // A field map is a keyed collection, where each key represents a response
612
        // name and the value at that key is a list of all fields which provide that
613
        // response name. For any response name which appears in both provided field
614
        // maps, each field from the first field map must be compared to every field
615
        // in the second field map to find potential conflicts.
616 29
        foreach ($fieldMap1 as $responseName => $fields1) {
617 23
            if (! isset($fieldMap2[$responseName])) {
618 11
                continue;
619
            }
620
621 17
            $fields2       = $fieldMap2[$responseName];
622 17
            $fields1Length = count($fields1);
623 17
            $fields2Length = count($fields2);
624 17
            for ($i = 0; $i < $fields1Length; $i++) {
625 17
                for ($j = 0; $j < $fields2Length; $j++) {
626 17
                    $conflict = $this->findConflict(
627 17
                        $context,
628 17
                        $parentFieldsAreMutuallyExclusive,
629 17
                        $responseName,
630 17
                        $fields1[$i],
631 17
                        $fields2[$j]
632
                    );
633 17
                    if (! $conflict) {
634 9
                        continue;
635
                    }
636
637 11
                    $conflicts[] = $conflict;
638
                }
639
            }
640
        }
641 29
    }
642
643
    /**
644
     * Collect all conflicts found between a set of fields and a fragment reference
645
     * including via spreading in any nested fragments.
646
     *
647
     * @param mixed[][] $conflicts
648
     * @param bool[]    $comparedFragments
649
     * @param bool      $areMutuallyExclusive
650
     * @param mixed[][] $fieldMap
651
     * @param string    $fragmentName
652
     */
653 24
    private function collectConflictsBetweenFieldsAndFragment(
654
        ValidationContext $context,
655
        array &$conflicts,
656
        array &$comparedFragments,
657
        $areMutuallyExclusive,
658
        array $fieldMap,
659
        $fragmentName
660
    ) {
661 24
        if (isset($comparedFragments[$fragmentName])) {
662
            return;
663
        }
664 24
        $comparedFragments[$fragmentName] = true;
665
666 24
        $fragment = $context->getFragment($fragmentName);
667 24
        if (! $fragment) {
668 1
            return;
669
        }
670
671 24
        [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
672 24
            $context,
673 24
            $fragment
674
        );
675
676 24
        if ($fieldMap === $fieldMap2) {
677 3
            return;
678
        }
679
680
        // (D) First collect any conflicts between the provided collection of fields
681
        // and the collection of fields represented by the given fragment.
682 22
        $this->collectConflictsBetween(
683 22
            $context,
684 22
            $conflicts,
685 22
            $areMutuallyExclusive,
686 22
            $fieldMap,
687 22
            $fieldMap2
688
        );
689
690
        // (E) Then collect any conflicts between the provided collection of fields
691
        // and any fragment names found in the given fragment.
692 22
        $fragmentNames2Length = count($fragmentNames2);
693 22
        for ($i = 0; $i < $fragmentNames2Length; $i++) {
694 3
            $this->collectConflictsBetweenFieldsAndFragment(
695 3
                $context,
696 3
                $conflicts,
697 3
                $comparedFragments,
698 3
                $areMutuallyExclusive,
699 3
                $fieldMap,
700 3
                $fragmentNames2[$i]
701
            );
702
        }
703 22
    }
704
705
    /**
706
     * Given a reference to a fragment, return the represented collection of fields
707
     * as well as a list of nested fragment names referenced via fragment spreads.
708
     *
709
     * @return mixed[]|SplObjectStorage
710
     */
711 24
    private function getReferencedFieldsAndFragmentNames(
712
        ValidationContext $context,
713
        FragmentDefinitionNode $fragment
714
    ) {
715
        // Short-circuit building a type from the AST if possible.
716 24
        if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) {
717 16
            return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet];
718
        }
719
720 21
        $fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition);
721
722 21
        return $this->getFieldsAndFragmentNames(
723 21
            $context,
724 21
            $fragmentType,
0 ignored issues
show
Bug introduced by
It seems like $fragmentType can also be of type GraphQL\Type\Definition\Type; however, parameter $parentType of GraphQL\Validator\Rules\...ieldsAndFragmentNames() does only seem to accept GraphQL\Type\Definition\CompositeType, 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

724
            /** @scrutinizer ignore-type */ $fragmentType,
Loading history...
725 21
            $fragment->selectionSet
726
        );
727
    }
728
729
    /**
730
     * Collect all conflicts found between two fragments, including via spreading in
731
     * any nested fragments.
732
     *
733
     * @param mixed[][] $conflicts
734
     * @param bool      $areMutuallyExclusive
735
     * @param string    $fragmentName1
736
     * @param string    $fragmentName2
737
     */
738 8
    private function collectConflictsBetweenFragments(
739
        ValidationContext $context,
740
        array &$conflicts,
741
        $areMutuallyExclusive,
742
        $fragmentName1,
743
        $fragmentName2
744
    ) {
745
        // No need to compare a fragment to itself.
746 8
        if ($fragmentName1 === $fragmentName2) {
747 1
            return;
748
        }
749
750
        // Memoize so two fragments are not compared for conflicts more than once.
751 7
        if ($this->comparedFragmentPairs->has(
752 7
            $fragmentName1,
753 7
            $fragmentName2,
754 7
            $areMutuallyExclusive
755
        )
756
        ) {
757 3
            return;
758
        }
759 7
        $this->comparedFragmentPairs->add(
760 7
            $fragmentName1,
761 7
            $fragmentName2,
762 7
            $areMutuallyExclusive
763
        );
764
765 7
        $fragment1 = $context->getFragment($fragmentName1);
766 7
        $fragment2 = $context->getFragment($fragmentName2);
767 7
        if (! $fragment1 || ! $fragment2) {
768 1
            return;
769
        }
770
771 6
        [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames(
772 6
            $context,
773 6
            $fragment1
774
        );
775 6
        [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
776 6
            $context,
777 6
            $fragment2
778
        );
779
780
        // (F) First, collect all conflicts between these two collections of fields
781
        // (not including any nested fragments).
782 6
        $this->collectConflictsBetween(
783 6
            $context,
784 6
            $conflicts,
785 6
            $areMutuallyExclusive,
786 6
            $fieldMap1,
787 6
            $fieldMap2
788
        );
789
790
        // (G) Then collect conflicts between the first fragment and any nested
791
        // fragments spread in the second fragment.
792 6
        $fragmentNames2Length = count($fragmentNames2);
793 6
        for ($j = 0; $j < $fragmentNames2Length; $j++) {
794 1
            $this->collectConflictsBetweenFragments(
795 1
                $context,
796 1
                $conflicts,
797 1
                $areMutuallyExclusive,
798 1
                $fragmentName1,
799 1
                $fragmentNames2[$j]
800
            );
801
        }
802
803
        // (G) Then collect conflicts between the second fragment and any nested
804
        // fragments spread in the first fragment.
805 6
        $fragmentNames1Length = count($fragmentNames1);
806 6
        for ($i = 0; $i < $fragmentNames1Length; $i++) {
807 1
            $this->collectConflictsBetweenFragments(
808 1
                $context,
809 1
                $conflicts,
810 1
                $areMutuallyExclusive,
811 1
                $fragmentNames1[$i],
812 1
                $fragmentName2
813
            );
814
        }
815 6
    }
816
817
    /**
818
     * Given a series of Conflicts which occurred between two sub-fields, generate
819
     * a single Conflict.
820
     *
821
     * @param mixed[][] $conflicts
822
     * @param string    $responseName
823
     *
824
     * @return mixed[]|null
825
     */
826 13
    private function subfieldConflicts(
827
        array $conflicts,
828
        $responseName,
829
        FieldNode $ast1,
830
        FieldNode $ast2
831
    ) {
832 13
        if (count($conflicts) === 0) {
833 7
            return null;
834
        }
835
836
        return [
837
            [
838 9
                $responseName,
839 9
                array_map(
840
                    static function ($conflict) {
841 9
                        return $conflict[0];
842 9
                    },
843 9
                    $conflicts
844
                ),
845
            ],
846 9
            array_reduce(
847 9
                $conflicts,
848
                static function ($allFields, $conflict) {
849 9
                    return array_merge($allFields, $conflict[1]);
850 9
                },
851 9
                [$ast1]
852
            ),
853 9
            array_reduce(
854 9
                $conflicts,
855
                static function ($allFields, $conflict) {
856 9
                    return array_merge($allFields, $conflict[2]);
857 9
                },
858 9
                [$ast2]
859
            ),
860
        ];
861
    }
862
863
    /**
864
     * @param string $responseName
865
     * @param string $reason
866
     */
867 23
    public static function fieldsConflictMessage($responseName, $reason)
868
    {
869 23
        $reasonMessage = self::reasonMessage($reason);
870
871 23
        return sprintf(
872 23
            'Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.',
873 23
            $responseName,
874 23
            $reasonMessage
875
        );
876
    }
877
878 23
    public static function reasonMessage($reason)
879
    {
880 23
        if (is_array($reason)) {
881 9
            $tmp = array_map(
882
                static function ($tmp) {
883 9
                    [$responseName, $subReason] = $tmp;
884
885 9
                    $reasonMessage = self::reasonMessage($subReason);
886
887 9
                    return sprintf('subfields "%s" conflict because %s', $responseName, $reasonMessage);
888 9
                },
889 9
                $reason
890
            );
891
892 9
            return implode(' and ', $tmp);
893
        }
894
895 23
        return $reason;
896
    }
897
}
898