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.
Passed
Pull Request — master (#1)
by Šimon
04:44 queued 13s
created

findConflictsBetweenSubSelectionSets()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 79
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 7.059

Importance

Changes 0
Metric Value
dl 0
loc 79
ccs 42
cts 47
cp 0.8936
rs 6.5649
c 0
b 0
f 0
cc 7
eloc 46
nc 12
nop 6
crap 7.059

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace GraphQL\Validator\Rules;
3
4
use GraphQL\Error\Error;
5
use GraphQL\Language\AST\ArgumentNode;
6
use GraphQL\Language\AST\FieldNode;
7
use GraphQL\Language\AST\FragmentDefinitionNode;
8
use GraphQL\Language\AST\FragmentSpreadNode;
9
use GraphQL\Language\AST\InlineFragmentNode;
10
use GraphQL\Language\AST\Node;
11
use GraphQL\Language\AST\NodeKind;
12
use GraphQL\Language\AST\SelectionSetNode;
13
use GraphQL\Language\Printer;
14
use GraphQL\Type\Definition\CompositeType;
15
use GraphQL\Type\Definition\InterfaceType;
16
use GraphQL\Type\Definition\ListOfType;
17
use GraphQL\Type\Definition\NonNull;
18
use GraphQL\Type\Definition\ObjectType;
19
use GraphQL\Type\Definition\OutputType;
20
use GraphQL\Type\Definition\Type;
21
use GraphQL\Utils\PairSet;
22
use GraphQL\Utils\TypeInfo;
23
use GraphQL\Validator\ValidationContext;
24
25
class OverlappingFieldsCanBeMerged extends AbstractValidationRule
26
{
27 23
    static function fieldsConflictMessage($responseName, $reason)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
28
    {
29 23
        $reasonMessage = self::reasonMessage($reason);
30 23
        return "Fields \"$responseName\" conflict because $reasonMessage. Use different aliases on the fields to fetch both if this was intentional.";
31
    }
32
33 23
    static function reasonMessage($reason)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

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

625
                /** @scrutinizer ignore-type */ Type::getNamedType($type1),
Loading history...
626 12
                $selectionSet1,
627 12
                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\ScalarType and GraphQL\Type\Definition\InputObjectType and GraphQL\Type\Definition\EnumType; 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

627
                /** @scrutinizer ignore-type */ Type::getNamedType($type2),
Loading history...
628 12
                $selectionSet2
629
            );
630 12
            return $this->subfieldConflicts(
631 12
                $conflicts,
632 12
                $responseName,
633 12
                $ast1,
634 12
                $ast2
635
            );
636
        }
637
638 18
        return null;
639
    }
640
641
    /**
642
     * @param ArgumentNode[] $arguments1
643
     * @param ArgumentNode[] $arguments2
644
     *
645
     * @return bool
646
     */
647 24
    private function sameArguments($arguments1, $arguments2)
648
    {
649 24
        if (count($arguments1) !== count($arguments2)) {
650 2
            return false;
651
        }
652 22
        foreach ($arguments1 as $argument1) {
653 2
            $argument2 = null;
654 2
            foreach ($arguments2 as $argument) {
655 2
                if ($argument->name->value === $argument1->name->value) {
656 2
                    $argument2 = $argument;
657 2
                    break;
658
                }
659
            }
660 2
            if (!$argument2) {
661
                return false;
662
            }
663
664 2
            if (!$this->sameValue($argument1->value, $argument2->value)) {
665 2
                return false;
666
            }
667
        }
668
669 21
        return true;
670
    }
671
672
    /**
673
     * @param Node $value1
674
     * @param Node $value2
675
     * @return bool
676
     */
677 2
    private function sameValue(Node $value1, Node $value2)
678
    {
679 2
        return (!$value1 && !$value2) || (Printer::doPrint($value1) === Printer::doPrint($value2));
680
    }
681
682
    /**
683
     * Two types conflict if both types could not apply to a value simultaneously.
684
     * Composite types are ignored as their individual field types will be compared
685
     * later recursively. However List and Non-Null types must match.
686
     *
687
     * @param OutputType $type1
688
     * @param OutputType $type2
689
     * @return bool
690
     */
691 23
    private function doTypesConflict(OutputType $type1, OutputType $type2)
692
    {
693 23
        if ($type1 instanceof ListOfType) {
694 3
            return $type2 instanceof ListOfType ?
695 2
                $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
0 ignored issues
show
Bug introduced by
It seems like $type1->getWrappedType() can also be of type GraphQL\Type\Definition\InputObjectType; however, parameter $type1 of GraphQL\Validator\Rules\...rged::doTypesConflict() does only seem to accept GraphQL\Type\Definition\OutputType, 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

695
                $this->doTypesConflict(/** @scrutinizer ignore-type */ $type1->getWrappedType(), $type2->getWrappedType()) :
Loading history...
Bug introduced by
It seems like $type2->getWrappedType() can also be of type GraphQL\Type\Definition\InputObjectType; however, parameter $type2 of GraphQL\Validator\Rules\...rged::doTypesConflict() does only seem to accept GraphQL\Type\Definition\OutputType, 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

695
                $this->doTypesConflict($type1->getWrappedType(), /** @scrutinizer ignore-type */ $type2->getWrappedType()) :
Loading history...
696 3
                true;
697
        }
698 23
        if ($type2 instanceof ListOfType) {
699 1
            return $type1 instanceof ListOfType ?
700
                $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
701 1
                true;
702
        }
703 22
        if ($type1 instanceof NonNull) {
704 2
            return $type2 instanceof NonNull ?
705 1
                $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
706 2
                true;
707
        }
708 21
        if ($type2 instanceof NonNull) {
709 1
            return $type1 instanceof NonNull ?
710
                $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
711 1
                true;
712
        }
713 20
        if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
714 17
            return $type1 !== $type2;
715
        }
716 6
        return false;
717
    }
718
719
    /**
720
     * Given a selection set, return the collection of fields (a mapping of response
721
     * name to field ASTs and definitions) as well as a list of fragment names
722
     * referenced via fragment spreads.
723
     *
724
     * @param ValidationContext $context
725
     * @param CompositeType $parentType
726
     * @param SelectionSetNode $selectionSet
727
     * @return array
728
     */
729 141
    private function getFieldsAndFragmentNames(
730
        ValidationContext $context,
731
        $parentType,
732
        SelectionSetNode $selectionSet
733
    ) {
734 141
        if (!isset($this->cachedFieldsAndFragmentNames[$selectionSet])) {
735 141
            $astAndDefs = [];
736 141
            $fragmentNames = [];
737
738 141
            $this->internalCollectFieldsAndFragmentNames(
739 141
                $context,
740 141
                $parentType,
741 141
                $selectionSet,
742 141
                $astAndDefs,
743 141
                $fragmentNames
744
            );
745 141
            $cached = [$astAndDefs, array_keys($fragmentNames)];
746 141
            $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached;
747
        } else {
748 23
            $cached = $this->cachedFieldsAndFragmentNames[$selectionSet];
749
        }
750 141
        return $cached;
751
    }
752
753
    /**
754
     * Given a reference to a fragment, return the represented collection of fields
755
     * as well as a list of nested fragment names referenced via fragment spreads.
756
     *
757
     * @param ValidationContext $context
758
     * @param FragmentDefinitionNode $fragment
759
     * @return array|object
760
     */
761 19
    private function getReferencedFieldsAndFragmentNames(
762
        ValidationContext $context,
763
        FragmentDefinitionNode $fragment
764
    )
765
    {
766
        // Short-circuit building a type from the AST if possible.
767 19
        if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) {
768 14
            return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet];
769
        }
770
771 16
        $fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition);
772 16
        return $this->getFieldsAndFragmentNames(
773 16
            $context,
774 16
            $fragmentType,
0 ignored issues
show
Bug introduced by
$fragmentType of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\CompositeType expected by parameter $parentType of GraphQL\Validator\Rules\...ieldsAndFragmentNames(). ( Ignorable by Annotation )

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

774
            /** @scrutinizer ignore-type */ $fragmentType,
Loading history...
775 16
            $fragment->selectionSet
776
        );
777
    }
778
779
    /**
780
     * Given a reference to a fragment, return the represented collection of fields
781
     * as well as a list of nested fragment names referenced via fragment spreads.
782
     *
783
     * @param ValidationContext $context
784
     * @param CompositeType $parentType
785
     * @param SelectionSetNode $selectionSet
786
     * @param array $astAndDefs
787
     * @param array $fragmentNames
788
     */
789 141
    private function internalCollectFieldsAndFragmentNames(
790
        ValidationContext $context,
791
        $parentType,
792
        SelectionSetNode $selectionSet,
793
        array &$astAndDefs,
794
        array &$fragmentNames
795
    )
796
    {
797 141
        $selectionSetLength = count($selectionSet->selections);
798 141
        for ($i = 0; $i < $selectionSetLength; $i++) {
799 141
            $selection = $selectionSet->selections[$i];
800
801
            switch (true) {
802 141
                case $selection instanceof FieldNode:
803 141
                    $fieldName = $selection->name->value;
804 141
                    $fieldDef = null;
805 141
                    if ($parentType instanceof ObjectType ||
806 141
                        $parentType instanceof InterfaceType) {
807 139
                        $tmp = $parentType->getFields();
808 139
                        if (isset($tmp[$fieldName])) {
809 126
                            $fieldDef = $tmp[$fieldName];
810
                        }
811
                    }
812 141
                    $responseName = $selection->alias ? $selection->alias->value : $fieldName;
813
814 141
                    if (!isset($astAndDefs[$responseName])) {
815 141
                        $astAndDefs[$responseName] = [];
816
                    }
817 141
                    $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
818 141
                    break;
819 46
                case $selection instanceof FragmentSpreadNode:
820 19
                    $fragmentNames[$selection->name->value] = true;
821 19
                    break;
822 30
                case $selection instanceof InlineFragmentNode:
823 30
                    $typeCondition = $selection->typeCondition;
824 30
                    $inlineFragmentType = $typeCondition
825 29
                        ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition)
826 30
                        : $parentType;
827
828 30
                    $this->internalcollectFieldsAndFragmentNames(
829 30
                        $context,
830 30
                        $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

830
                        /** @scrutinizer ignore-type */ $inlineFragmentType,
Loading history...
831 30
                        $selection->selectionSet,
832
                        $astAndDefs,
833 30
                        $fragmentNames
834
                    );
835 30
                    break;
836
            }
837
        }
838 141
    }
839
840
    /**
841
     * Given a series of Conflicts which occurred between two sub-fields, generate
842
     * a single Conflict.
843
     *
844
     * @param array $conflicts
845
     * @param string $responseName
846
     * @param FieldNode $ast1
847
     * @param FieldNode $ast2
848
     * @return array|null
849
     */
850 12
    private function subfieldConflicts(
851
        array $conflicts,
852
        $responseName,
853
        FieldNode $ast1,
854
        FieldNode $ast2
855
    )
856
    {
857 12
        if (count($conflicts) > 0) {
858
            return [
859
                [
860 9
                    $responseName,
861
                    array_map(function ($conflict) {
862 9
                        return $conflict[0];
863 9
                    }, $conflicts),
864
                ],
865 9
                array_reduce(
866 9
                    $conflicts,
867
                    function ($allFields, $conflict) {
868 9
                        return array_merge($allFields, $conflict[1]);
869 9
                    },
870 9
                    [$ast1]
871
                ),
872 9
                array_reduce(
873 9
                    $conflicts,
874 9
                    function ($allFields, $conflict) {
875 9
                        return array_merge($allFields, $conflict[2]);
876 9
                    },
877 9
                    [$ast2]
878
                ),
879
            ];
880
        }
881 6
    }
882
}
883