GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

collectConflictsBetweenFieldsAndFragment()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 5.0012

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 48
ccs 26
cts 27
cp 0.963
rs 9.1928
cc 5
nc 5
nop 6
crap 5.0012
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