1 | <?php |
||||
2 | |||||
3 | namespace Digia\GraphQL\Validation\Conflict; |
||||
4 | |||||
5 | use Digia\GraphQL\Util\ConversionException; |
||||
6 | use Digia\GraphQL\Error\InvalidTypeException; |
||||
7 | use Digia\GraphQL\Error\InvariantException; |
||||
8 | use Digia\GraphQL\Language\Node\FieldNode; |
||||
9 | use Digia\GraphQL\Language\Node\FragmentDefinitionNode; |
||||
10 | use Digia\GraphQL\Language\Node\FragmentSpreadNode; |
||||
11 | use Digia\GraphQL\Language\Node\InlineFragmentNode; |
||||
12 | use Digia\GraphQL\Language\Node\SelectionSetNode; |
||||
13 | use Digia\GraphQL\Type\Definition\InterfaceType; |
||||
14 | use Digia\GraphQL\Type\Definition\NamedTypeInterface; |
||||
15 | use Digia\GraphQL\Type\Definition\ObjectType; |
||||
16 | use Digia\GraphQL\Util\TypeASTConverter; |
||||
17 | use Digia\GraphQL\Util\TypeHelper; |
||||
18 | use Digia\GraphQL\Util\ValueHelper; |
||||
19 | use Digia\GraphQL\Validation\ValidationContextAwareTrait; |
||||
20 | use function Digia\GraphQL\Type\getNamedType; |
||||
21 | |||||
22 | /** |
||||
23 | * Algorithm: |
||||
24 | * |
||||
25 | * Conflicts occur when two fields exist in a query which will produce the same |
||||
26 | * response name, but represent differing values, thus creating a conflict. |
||||
27 | * The algorithm below finds all conflicts via making a series of comparisons |
||||
28 | * between fields. In order to compare as few fields as possible, this makes |
||||
29 | * a series of comparisons "within" sets of fields and "between" sets of fields. |
||||
30 | * |
||||
31 | * Given any selection set, a collection produces both a set of fields by |
||||
32 | * also including all inline fragments, as well as a list of fragments |
||||
33 | * referenced by fragment spreads. |
||||
34 | * |
||||
35 | * A) Each selection set represented in the document first compares "within" its |
||||
36 | * collected set of fields, finding any conflicts between every pair of |
||||
37 | * overlapping fields. |
||||
38 | * Note: This is the *only time* that a the fields "within" a set are compared |
||||
39 | * to each other. After this only fields "between" sets are compared. |
||||
40 | * |
||||
41 | * B) Also, if any fragment is referenced in a selection set, then a |
||||
42 | * comparison is made "between" the original set of fields and the |
||||
43 | * referenced fragment. |
||||
44 | * |
||||
45 | * C) Also, if multiple fragments are referenced, then comparisons |
||||
46 | * are made "between" each referenced fragment. |
||||
47 | * |
||||
48 | * D) When comparing "between" a set of fields and a referenced fragment, first |
||||
49 | * a comparison is made between each field in the original set of fields and |
||||
50 | * each field in the the referenced set of fields. |
||||
51 | * |
||||
52 | * E) Also, if any fragment is referenced in the referenced selection set, |
||||
53 | * then a comparison is made "between" the original set of fields and the |
||||
54 | * referenced fragment (recursively referring to step D). |
||||
55 | * |
||||
56 | * F) When comparing "between" two fragments, first a comparison is made between |
||||
57 | * each field in the first referenced set of fields and each field in the the |
||||
58 | * second referenced set of fields. |
||||
59 | * |
||||
60 | * G) Also, any fragments referenced by the first must be compared to the |
||||
61 | * second, and any fragments referenced by the second must be compared to the |
||||
62 | * first (recursively referring to step F). |
||||
63 | * |
||||
64 | * H) When comparing two fields, if both have selection sets, then a comparison |
||||
65 | * is made "between" both selection sets, first comparing the set of fields in |
||||
66 | * the first selection set with the set of fields in the second. |
||||
67 | * |
||||
68 | * I) Also, if any fragment is referenced in either selection set, then a |
||||
69 | * comparison is made "between" the other set of fields and the |
||||
70 | * referenced fragment. |
||||
71 | * |
||||
72 | * J) Also, if two fragments are referenced in both selection sets, then a |
||||
73 | * comparison is made "between" the two fragments. |
||||
74 | * |
||||
75 | * Class ConflictFinder |
||||
76 | * @package Digia\GraphQL\Validation\Conflict |
||||
77 | */ |
||||
78 | class ConflictFinder |
||||
79 | { |
||||
80 | use ValidationContextAwareTrait; |
||||
81 | |||||
82 | /** |
||||
83 | * A cache for the "field map" and list of fragment names found in any given |
||||
84 | * selection set. Selection sets may be asked for this information multiple |
||||
85 | * times, so this improves the performance of this validator. |
||||
86 | * |
||||
87 | * @var \SplObjectStorage |
||||
88 | */ |
||||
89 | protected $cachedFieldsAndFragmentNames; |
||||
90 | |||||
91 | /** |
||||
92 | * A memoization for when two fragments are compared "between" each other for |
||||
93 | * conflicts. Two fragments may be compared many times, so memoizing this can |
||||
94 | * dramatically improve the performance of this validator. |
||||
95 | * |
||||
96 | * @var PairSet |
||||
97 | */ |
||||
98 | protected $comparedFragmentPairs; |
||||
99 | |||||
100 | /** |
||||
101 | * ConflictFinder constructor. |
||||
102 | */ |
||||
103 | public function __construct() |
||||
104 | { |
||||
105 | $this->cachedFieldsAndFragmentNames = new \SplObjectStorage(); |
||||
106 | $this->comparedFragmentPairs = new PairSet(); |
||||
107 | } |
||||
108 | |||||
109 | /** |
||||
110 | * @param SelectionSetNode $selectionSet |
||||
111 | * @param NamedTypeInterface|null $parentType |
||||
112 | * @return array|Conflict[] |
||||
113 | * @throws InvalidTypeException |
||||
114 | * @throws InvariantException |
||||
115 | * @throws ConversionException |
||||
116 | */ |
||||
117 | public function findConflictsWithinSelectionSet( |
||||
118 | SelectionSetNode $selectionSet, |
||||
119 | ?NamedTypeInterface $parentType = null |
||||
120 | ): array { |
||||
121 | $context = $this->getFieldsAndFragmentNames($selectionSet, $parentType); |
||||
122 | |||||
123 | // (A) Find find all conflicts "within" the fields of this selection set. |
||||
124 | // Note: this is the *only place* `collectConflictsWithin` is called. |
||||
125 | $this->collectConflictsWithin($context); |
||||
126 | |||||
127 | $fieldMap = $context->getFieldMap(); |
||||
128 | $fragmentNames = $context->getFragmentNames(); |
||||
129 | |||||
130 | // (B) Then collect conflicts between these fields and those represented by |
||||
131 | // each spread fragment name found. |
||||
132 | if (!empty($fragmentNames)) { |
||||
133 | $fragmentNamesCount = \count($fragmentNames); |
||||
134 | $comparedFragments = []; |
||||
135 | |||||
136 | /** @noinspection ForeachInvariantsInspection */ |
||||
137 | for ($i = 0; $i < $fragmentNamesCount; $i++) { |
||||
138 | $this->collectConflictsBetweenFieldsAndFragment( |
||||
139 | $context, |
||||
140 | $comparedFragments, |
||||
141 | $fieldMap, |
||||
142 | $fragmentNames[$i], |
||||
143 | false/* $areMutuallyExclusive */ |
||||
144 | ); |
||||
145 | |||||
146 | // (C) Then compare this fragment with all other fragments found in this |
||||
147 | // selection set to collect conflicts between fragments spread together. |
||||
148 | // This compares each item in the list of fragment names to every other |
||||
149 | // item in that same list (except for itself). |
||||
150 | for ($j = $i + 1; $j < $fragmentNamesCount; $j++) { |
||||
151 | $this->collectConflictsBetweenFragments( |
||||
152 | $context, |
||||
153 | $fragmentNames[$i], |
||||
154 | $fragmentNames[$j], |
||||
155 | false/* $areMutuallyExclusive */ |
||||
156 | ); |
||||
157 | } |
||||
158 | } |
||||
159 | } |
||||
160 | |||||
161 | return $context->getConflicts(); |
||||
162 | } |
||||
163 | |||||
164 | /** |
||||
165 | * Collect all conflicts found between a set of fields and a fragment reference |
||||
166 | * including via spreading in any nested fragments. |
||||
167 | * |
||||
168 | * @param ComparisonContext $context |
||||
169 | * @param array $comparedFragments |
||||
170 | * @param array $fieldMap |
||||
171 | * @param string $fragmentName |
||||
172 | * @param bool $areMutuallyExclusive |
||||
173 | * @throws ConversionException |
||||
174 | * @throws InvalidTypeException |
||||
175 | * @throws InvariantException |
||||
176 | */ |
||||
177 | protected function collectConflictsBetweenFieldsAndFragment( |
||||
178 | ComparisonContext $context, |
||||
179 | array &$comparedFragments, |
||||
180 | array $fieldMap, |
||||
181 | string $fragmentName, |
||||
182 | bool $areMutuallyExclusive |
||||
183 | ): void { |
||||
184 | // Memoize so a fragment is not compared for conflicts more than once. |
||||
185 | if (isset($comparedFragments[$fragmentName])) { |
||||
186 | return; |
||||
187 | } |
||||
188 | |||||
189 | $comparedFragments[$fragmentName] = true; |
||||
190 | |||||
191 | $fragment = $this->getContext()->getFragment($fragmentName); |
||||
192 | |||||
193 | if (null === $fragment) { |
||||
194 | return; |
||||
195 | } |
||||
196 | |||||
197 | $contextB = $this->getReferencedFieldsAndFragmentNames($fragment); |
||||
198 | |||||
199 | $fieldMapB = $contextB->getFieldMap(); |
||||
200 | |||||
201 | // Do not compare a fragment's fieldMap to itself. |
||||
202 | if ($fieldMap == $fieldMapB) { |
||||
203 | return; |
||||
204 | } |
||||
205 | |||||
206 | // (D) First collect any conflicts between the provided collection of fields |
||||
207 | // and the collection of fields represented by the given fragment. |
||||
208 | $this->collectConflictsBetween( |
||||
209 | $context, |
||||
210 | $fieldMap, |
||||
211 | $fieldMapB, |
||||
212 | $areMutuallyExclusive |
||||
213 | ); |
||||
214 | |||||
215 | $fragmentNamesB = $contextB->getFragmentNames(); |
||||
216 | |||||
217 | // (E) Then collect any conflicts between the provided collection of fields |
||||
218 | // and any fragment names found in the given fragment. |
||||
219 | if (!empty($fragmentNamesB)) { |
||||
220 | $fragmentNamesBCount = \count($fragmentNamesB); |
||||
221 | |||||
222 | /** @noinspection ForeachInvariantsInspection */ |
||||
223 | for ($i = 0; $i < $fragmentNamesBCount; $i++) { |
||||
224 | $this->collectConflictsBetweenFieldsAndFragment( |
||||
225 | $context, |
||||
226 | $comparedFragments, |
||||
227 | $fieldMap, |
||||
228 | $fragmentNamesB[$i], |
||||
229 | $areMutuallyExclusive |
||||
230 | ); |
||||
231 | } |
||||
232 | } |
||||
233 | } |
||||
234 | |||||
235 | /** |
||||
236 | * Collect all conflicts found between two fragments, including via spreading in |
||||
237 | * any nested fragments. |
||||
238 | * |
||||
239 | * @param ComparisonContext $context |
||||
240 | * @param string $fragmentNameA |
||||
241 | * @param string $fragmentNameB |
||||
242 | * @param bool $areMutuallyExclusive |
||||
243 | * @throws ConversionException |
||||
244 | * @throws InvalidTypeException |
||||
245 | * @throws InvariantException |
||||
246 | */ |
||||
247 | protected function collectConflictsBetweenFragments( |
||||
248 | ComparisonContext $context, |
||||
249 | string $fragmentNameA, |
||||
250 | string $fragmentNameB, |
||||
251 | bool $areMutuallyExclusive |
||||
252 | ): void { |
||||
253 | // No need to compare a fragment to itself. |
||||
254 | if ($fragmentNameA === $fragmentNameB) { |
||||
255 | return; |
||||
256 | } |
||||
257 | |||||
258 | // Memoize so two fragments are not compared for conflicts more than once. |
||||
259 | if ($this->comparedFragmentPairs->has($fragmentNameA, $fragmentNameB, $areMutuallyExclusive)) { |
||||
260 | return; |
||||
261 | } |
||||
262 | |||||
263 | $this->comparedFragmentPairs->add($fragmentNameA, $fragmentNameB, $areMutuallyExclusive); |
||||
264 | |||||
265 | $fragmentA = $this->getContext()->getFragment($fragmentNameA); |
||||
266 | $fragmentB = $this->getContext()->getFragment($fragmentNameB); |
||||
267 | |||||
268 | if (null === $fragmentA || null === $fragmentB) { |
||||
269 | return; |
||||
270 | } |
||||
271 | |||||
272 | $contextA = $this->getReferencedFieldsAndFragmentNames($fragmentA); |
||||
273 | $contextB = $this->getReferencedFieldsAndFragmentNames($fragmentB); |
||||
274 | |||||
275 | // (F) First, collect all conflicts between these two collections of fields |
||||
276 | // (not including any nested fragments). |
||||
277 | $this->collectConflictsBetween( |
||||
278 | $context, |
||||
279 | $contextA->getFieldMap(), |
||||
280 | $contextB->getFieldMap(), |
||||
281 | $areMutuallyExclusive |
||||
282 | ); |
||||
283 | |||||
284 | $fragmentNamesB = $contextB->getFragmentNames(); |
||||
285 | |||||
286 | // (G) Then collect conflicts between the first fragment and any nested |
||||
287 | // fragments spread in the second fragment. |
||||
288 | if (!empty($fragmentNamesB)) { |
||||
289 | $fragmentNamesBCount = \count($fragmentNamesB); |
||||
290 | |||||
291 | /** @noinspection ForeachInvariantsInspection */ |
||||
292 | for ($j = 0; $j < $fragmentNamesBCount; $j++) { |
||||
293 | $this->collectConflictsBetweenFragments( |
||||
294 | $context, |
||||
295 | $fragmentNameA, |
||||
296 | $fragmentNamesB[$j], |
||||
297 | $areMutuallyExclusive |
||||
298 | ); |
||||
299 | } |
||||
300 | } |
||||
301 | |||||
302 | $fragmentNamesA = $contextA->getFragmentNames(); |
||||
303 | |||||
304 | // (G) Then collect conflicts between the second fragment and any nested |
||||
305 | // fragments spread in the first fragment. |
||||
306 | if (!empty($fragmentNamesA)) { |
||||
307 | $fragmentNamesACount = \count($fragmentNamesA); |
||||
308 | |||||
309 | /** @noinspection ForeachInvariantsInspection */ |
||||
310 | for ($i = 0; $i < $fragmentNamesACount; $i++) { |
||||
311 | $this->collectConflictsBetweenFragments( |
||||
312 | $context, |
||||
313 | $fragmentNamesA[$i], |
||||
314 | $fragmentNameB, |
||||
315 | $areMutuallyExclusive |
||||
316 | ); |
||||
317 | } |
||||
318 | } |
||||
319 | } |
||||
320 | |||||
321 | /** |
||||
322 | * Find all conflicts found between two selection sets, including those found |
||||
323 | * via spreading in fragments. Called when determining if conflicts exist |
||||
324 | * between the sub-fields of two overlapping fields. |
||||
325 | * |
||||
326 | * @param NamedTypeInterface|null $parentTypeA |
||||
327 | * @param SelectionSetNode $selectionSetA |
||||
328 | * @param NamedTypeInterface|null $parentTypeB |
||||
329 | * @param SelectionSetNode $selectionSetB |
||||
330 | * @param bool $areMutuallyExclusive |
||||
331 | * @return Conflict[] |
||||
332 | * @throws InvalidTypeException |
||||
333 | * @throws InvariantException |
||||
334 | * @throws ConversionException |
||||
335 | */ |
||||
336 | protected function findConflictsBetweenSubSelectionSets( |
||||
337 | ?NamedTypeInterface $parentTypeA, |
||||
338 | SelectionSetNode $selectionSetA, |
||||
339 | ?NamedTypeInterface $parentTypeB, |
||||
340 | SelectionSetNode $selectionSetB, |
||||
341 | bool $areMutuallyExclusive |
||||
342 | ): array { |
||||
343 | $context = new ComparisonContext(); |
||||
344 | |||||
345 | $contextA = $this->getFieldsAndFragmentNames($selectionSetA, $parentTypeA); |
||||
346 | $contextB = $this->getFieldsAndFragmentNames($selectionSetB, $parentTypeB); |
||||
347 | |||||
348 | $fieldMapA = $contextA->getFieldMap(); |
||||
349 | $fieldMapB = $contextB->getFieldMap(); |
||||
350 | |||||
351 | $fragmentNamesA = $contextA->getFragmentNames(); |
||||
352 | $fragmentNamesB = $contextB->getFragmentNames(); |
||||
353 | |||||
354 | $fragmentNamesACount = \count($fragmentNamesA); |
||||
355 | $fragmentNamesBCount = \count($fragmentNamesB); |
||||
356 | |||||
357 | // (H) First, collect all conflicts between these two collections of field. |
||||
358 | $this->collectConflictsBetween( |
||||
359 | $context, |
||||
360 | $fieldMapA, |
||||
361 | $fieldMapB, |
||||
362 | $areMutuallyExclusive |
||||
363 | ); |
||||
364 | |||||
365 | // (I) Then collect conflicts between the first collection of fields and |
||||
366 | // those referenced by each fragment name associated with the second. |
||||
367 | if (!empty($fragmentNamesB)) { |
||||
368 | $comparedFragments = []; |
||||
369 | |||||
370 | /** @noinspection ForeachInvariantsInspection */ |
||||
371 | for ($j = 0; $j < $fragmentNamesBCount; $j++) { |
||||
372 | $this->collectConflictsBetweenFieldsAndFragment( |
||||
373 | $context, |
||||
374 | $comparedFragments, |
||||
375 | $fieldMapA, |
||||
376 | $fragmentNamesB[$j], |
||||
377 | $areMutuallyExclusive |
||||
378 | ); |
||||
379 | } |
||||
380 | } |
||||
381 | |||||
382 | // (I) Then collect conflicts between the second collection of fields and |
||||
383 | // those referenced by each fragment name associated with the first. |
||||
384 | if (!empty($fragmentNamesA)) { |
||||
385 | $comparedFragments = []; |
||||
386 | |||||
387 | /** @noinspection ForeachInvariantsInspection */ |
||||
388 | for ($i = 0; $i < $fragmentNamesACount; $i++) { |
||||
389 | $this->collectConflictsBetweenFieldsAndFragment( |
||||
390 | $context, |
||||
391 | $comparedFragments, |
||||
392 | $fieldMapB, |
||||
393 | $fragmentNamesA[$i], |
||||
394 | $areMutuallyExclusive |
||||
395 | ); |
||||
396 | } |
||||
397 | } |
||||
398 | |||||
399 | /** @noinspection ForeachInvariantsInspection */ |
||||
400 | for ($i = 0; $i < $fragmentNamesACount; $i++) { |
||||
401 | /** @noinspection ForeachInvariantsInspection */ |
||||
402 | for ($j = 0; $j < $fragmentNamesBCount; $j++) { |
||||
403 | $this->collectConflictsBetweenFragments( |
||||
404 | $context, |
||||
405 | $fragmentNamesA[$i], |
||||
406 | $fragmentNamesB[$j], |
||||
407 | $areMutuallyExclusive |
||||
408 | ); |
||||
409 | } |
||||
410 | } |
||||
411 | |||||
412 | return $context->getConflicts(); |
||||
413 | } |
||||
414 | |||||
415 | /** |
||||
416 | * Collect all Conflicts "within" one collection of fields. |
||||
417 | * |
||||
418 | * @param ComparisonContext $context |
||||
419 | * @throws ConversionException |
||||
420 | * @throws InvalidTypeException |
||||
421 | * @throws InvariantException |
||||
422 | */ |
||||
423 | protected function collectConflictsWithin(ComparisonContext $context): void |
||||
424 | { |
||||
425 | // A field map is a keyed collection, where each key represents a response |
||||
426 | // name and the value at that key is a list of all fields which provide that |
||||
427 | // response name. For every response name, if there are multiple fields, they |
||||
428 | // must be compared to find a potential conflict. |
||||
429 | foreach ($context->getFieldMap() as $responseName => $fields) { |
||||
430 | $fieldsCount = \count($fields); |
||||
431 | |||||
432 | // This compares every field in the list to every other field in this list |
||||
433 | // (except to itself). If the list only has one item, nothing needs to |
||||
434 | // be compared. |
||||
435 | if ($fieldsCount > 1) { |
||||
436 | /** @noinspection ForeachInvariantsInspection */ |
||||
437 | for ($i = 0; $i < $fieldsCount; $i++) { |
||||
438 | for ($j = $i + 1; $j < $fieldsCount; $j++) { |
||||
439 | $conflict = $this->findConflict( |
||||
440 | $responseName, |
||||
441 | $fields[$i], |
||||
442 | $fields[$j], |
||||
443 | // within one collection is never mutually exclusive |
||||
444 | false/* $areMutuallyExclusive */ |
||||
445 | ); |
||||
446 | |||||
447 | if (null !== $conflict) { |
||||
448 | $context->reportConflict($conflict); |
||||
449 | } |
||||
450 | } |
||||
451 | } |
||||
452 | } |
||||
453 | } |
||||
454 | } |
||||
455 | |||||
456 | /** |
||||
457 | * Collect all Conflicts between two collections of fields. This is similar to, |
||||
458 | * but different from the `collectConflictsWithin` function above. This check |
||||
459 | * assumes that `collectConflictsWithin` has already been called on each |
||||
460 | * provided collection of fields. This is true because this validator traverses |
||||
461 | * each individual selection set. |
||||
462 | * |
||||
463 | * @param ComparisonContext $context |
||||
464 | * @param array $fieldMapA |
||||
465 | * @param array $fieldMapB |
||||
466 | * @param bool $parentFieldsAreMutuallyExclusive |
||||
467 | * @throws ConversionException |
||||
468 | * @throws InvalidTypeException |
||||
469 | * @throws InvariantException |
||||
470 | */ |
||||
471 | protected function collectConflictsBetween( |
||||
472 | ComparisonContext $context, |
||||
473 | array $fieldMapA, |
||||
474 | array $fieldMapB, |
||||
475 | bool $parentFieldsAreMutuallyExclusive |
||||
476 | ): void { |
||||
477 | // A field map is a keyed collection, where each key represents a response |
||||
478 | // name and the value at that key is a list of all fields which provide that |
||||
479 | // response name. For any response name which appears in both provided field |
||||
480 | // maps, each field from the first field map must be compared to every field |
||||
481 | // in the second field map to find potential conflicts. |
||||
482 | foreach ($fieldMapA as $responseName => $fieldsA) { |
||||
483 | $fieldsB = $fieldMapB[$responseName] ?? null; |
||||
484 | |||||
485 | if (null !== $fieldsB) { |
||||
486 | $fieldsACount = \count($fieldsA); |
||||
487 | $fieldsBCount = \count($fieldsB); |
||||
488 | /** @noinspection ForeachInvariantsInspection */ |
||||
489 | for ($i = 0; $i < $fieldsACount; $i++) { |
||||
490 | /** @noinspection ForeachInvariantsInspection */ |
||||
491 | for ($j = 0; $j < $fieldsBCount; $j++) { |
||||
492 | $conflict = $this->findConflict( |
||||
493 | $responseName, |
||||
494 | $fieldsA[$i], |
||||
495 | $fieldsB[$j], |
||||
496 | $parentFieldsAreMutuallyExclusive |
||||
497 | ); |
||||
498 | |||||
499 | if (null !== $conflict) { |
||||
500 | $context->reportConflict($conflict); |
||||
501 | } |
||||
502 | } |
||||
503 | } |
||||
504 | } |
||||
505 | } |
||||
506 | } |
||||
507 | |||||
508 | /** |
||||
509 | * Determines if there is a conflict between two particular fields, including |
||||
510 | * comparing their sub-fields. |
||||
511 | * |
||||
512 | * @param string $responseName |
||||
513 | * @param FieldContext $fieldA |
||||
514 | * @param FieldContext $fieldB |
||||
515 | * @param bool $parentFieldsAreMutuallyExclusive |
||||
516 | * @return Conflict|null |
||||
517 | * @throws ConversionException |
||||
518 | * @throws InvalidTypeException |
||||
519 | * @throws InvariantException |
||||
520 | */ |
||||
521 | protected function findConflict( |
||||
522 | string $responseName, |
||||
523 | FieldContext $fieldA, |
||||
524 | FieldContext $fieldB, |
||||
525 | bool $parentFieldsAreMutuallyExclusive |
||||
526 | ): ?Conflict { |
||||
527 | $parentTypeA = $fieldA->getParentType(); |
||||
528 | $parentTypeB = $fieldB->getParentType(); |
||||
529 | |||||
530 | // If it is known that two fields could not possibly apply at the same |
||||
531 | // time, due to the parent types, then it is safe to permit them to diverge |
||||
532 | // in aliased field or arguments used as they will not present any ambiguity |
||||
533 | // by differing. |
||||
534 | // It is known that two parent types could never overlap if they are |
||||
535 | // different Object types. Interface or Union types might overlap - if not |
||||
536 | // in the current state of the schema, then perhaps in some future version, |
||||
537 | // thus may not safely diverge. |
||||
538 | $areMutuallyExclusive = $parentFieldsAreMutuallyExclusive |
||||
539 | || ($parentTypeA !== $parentTypeB |
||||
540 | && $parentTypeA instanceof ObjectType |
||||
541 | && $parentTypeB instanceof ObjectType); |
||||
542 | |||||
543 | $nodeA = $fieldA->getNode(); |
||||
544 | $nodeB = $fieldB->getNode(); |
||||
545 | |||||
546 | $definitionA = $fieldA->getDefinition(); |
||||
547 | $definitionB = $fieldB->getDefinition(); |
||||
548 | |||||
549 | if (!$areMutuallyExclusive) { |
||||
550 | // Two aliases must refer to the same field. |
||||
551 | $nameA = $nodeA->getNameValue(); |
||||
552 | $nameB = $nodeB->getNameValue(); |
||||
553 | |||||
554 | if ($nameA !== $nameB) { |
||||
555 | return new Conflict( |
||||
556 | $responseName, |
||||
557 | sprintf('%s and %s are different fields', $nameA, $nameB), |
||||
558 | [$nodeA], |
||||
559 | [$nodeB] |
||||
560 | ); |
||||
561 | } |
||||
562 | |||||
563 | // Two field calls must have the same arguments. |
||||
564 | if (!ValueHelper::compareArguments($nodeA->getArguments(), $nodeB->getArguments())) { |
||||
565 | return new Conflict( |
||||
566 | $responseName, |
||||
567 | 'they have differing arguments', |
||||
568 | [$nodeA], |
||||
569 | [$nodeB] |
||||
570 | ); |
||||
571 | } |
||||
572 | } |
||||
573 | |||||
574 | // The return type for each field. |
||||
575 | $typeA = null !== $definitionA ? $definitionA->getType() : null; |
||||
576 | $typeB = null !== $definitionB ? $definitionB->getType() : null; |
||||
577 | |||||
578 | if (null !== $typeA && null !== $typeB && TypeHelper::compareTypes($typeA, $typeB)) { |
||||
579 | return new Conflict( |
||||
580 | $responseName, |
||||
581 | sprintf('they return conflicting types %s and %s', (string)$typeA, (string)$typeB), |
||||
582 | [$nodeA], |
||||
583 | [$nodeB] |
||||
584 | ); |
||||
585 | } |
||||
586 | |||||
587 | // Collect and compare sub-fields. Use the same "visited fragment names" list |
||||
588 | // for both collections so fields in a fragment reference are never |
||||
589 | // compared to themselves. |
||||
590 | $selectionSetA = $nodeA->getSelectionSet(); |
||||
591 | $selectionSetB = $nodeB->getSelectionSet(); |
||||
592 | |||||
593 | if (null !== $selectionSetA && null !== $selectionSetB) { |
||||
594 | $conflicts = $this->findConflictsBetweenSubSelectionSets( |
||||
595 | getNamedType($typeA), |
||||
596 | $selectionSetA, |
||||
597 | getNamedType($typeB), |
||||
598 | $selectionSetB, |
||||
599 | $areMutuallyExclusive |
||||
600 | ); |
||||
601 | |||||
602 | return $this->subfieldConflicts($conflicts, $responseName, $nodeA, $nodeB); |
||||
603 | } |
||||
604 | |||||
605 | return null; |
||||
606 | } |
||||
607 | |||||
608 | /** |
||||
609 | * Given a selection set, return the collection of fields (a mapping of response |
||||
610 | * name to field nodes and definitions) as well as a list of fragment names |
||||
611 | * referenced via fragment spreads. |
||||
612 | * |
||||
613 | * @param SelectionSetNode $selectionSet |
||||
614 | * @param NamedTypeInterface|null $parentType |
||||
615 | * @return ComparisonContext |
||||
616 | * @throws InvalidTypeException |
||||
617 | * @throws InvariantException |
||||
618 | * @throws ConversionException |
||||
619 | */ |
||||
620 | protected function getFieldsAndFragmentNames( |
||||
621 | SelectionSetNode $selectionSet, |
||||
622 | ?NamedTypeInterface $parentType |
||||
623 | ): ComparisonContext { |
||||
624 | if (!$this->cachedFieldsAndFragmentNames->offsetExists($selectionSet)) { |
||||
625 | $cached = new ComparisonContext(); |
||||
626 | |||||
627 | $this->collectFieldsAndFragmentNames($cached, $selectionSet, $parentType); |
||||
628 | |||||
629 | $this->cachedFieldsAndFragmentNames->offsetSet($selectionSet, $cached); |
||||
630 | } |
||||
631 | |||||
632 | return $this->cachedFieldsAndFragmentNames->offsetGet($selectionSet); |
||||
633 | } |
||||
634 | |||||
635 | /** |
||||
636 | * Given a reference to a fragment, return the represented collection of fields |
||||
637 | * as well as a list of nested fragment names referenced via fragment spreads. |
||||
638 | * |
||||
639 | * @param FragmentDefinitionNode $fragment |
||||
640 | * @return ComparisonContext |
||||
641 | * @throws InvalidTypeException |
||||
642 | * @throws ConversionException |
||||
643 | * @throws InvariantException |
||||
644 | */ |
||||
645 | protected function getReferencedFieldsAndFragmentNames(FragmentDefinitionNode $fragment): ComparisonContext |
||||
646 | { |
||||
647 | if ($this->cachedFieldsAndFragmentNames->offsetExists($fragment)) { |
||||
648 | return $this->cachedFieldsAndFragmentNames->offsetGet($fragment); |
||||
649 | } |
||||
650 | |||||
651 | /** @var NamedTypeInterface $fragmentType */ |
||||
652 | $fragmentType = TypeASTConverter::convert($this->getContext()->getSchema(), $fragment->getTypeCondition()); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
653 | |||||
654 | return $this->getFieldsAndFragmentNames($fragment->getSelectionSet(), $fragmentType); |
||||
0 ignored issues
–
show
It seems like
$fragment->getSelectionSet() can also be of type null ; however, parameter $selectionSet of Digia\GraphQL\Validation...ieldsAndFragmentNames() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode , 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
![]() |
|||||
655 | } |
||||
656 | |||||
657 | /** |
||||
658 | * @param ComparisonContext $context |
||||
659 | * @param SelectionSetNode $selectionSet |
||||
660 | * @param NamedTypeInterface|null $parentType |
||||
661 | * @throws InvalidTypeException |
||||
662 | * @throws InvariantException |
||||
663 | * @throws ConversionException |
||||
664 | */ |
||||
665 | protected function collectFieldsAndFragmentNames( |
||||
666 | ComparisonContext $context, |
||||
667 | SelectionSetNode $selectionSet, |
||||
668 | ?NamedTypeInterface $parentType |
||||
669 | ): void { |
||||
670 | foreach ($selectionSet->getSelections() as $selection) { |
||||
671 | if ($selection instanceof FieldNode) { |
||||
672 | $definition = ($parentType instanceof ObjectType || $parentType instanceof InterfaceType) |
||||
673 | ? ($parentType->getFields()[$selection->getNameValue()] ?? null) |
||||
674 | : null; |
||||
675 | |||||
676 | $context->registerField(new FieldContext($parentType, $selection, $definition)); |
||||
677 | } elseif ($selection instanceof FragmentSpreadNode) { |
||||
678 | $context->registerFragment($selection); |
||||
679 | } elseif ($selection instanceof InlineFragmentNode) { |
||||
680 | $typeCondition = $selection->getTypeCondition(); |
||||
681 | |||||
682 | $inlineFragmentType = null !== $typeCondition |
||||
683 | ? TypeASTConverter::convert($this->getContext()->getSchema(), $typeCondition) |
||||
684 | : $parentType; |
||||
685 | |||||
686 | $this->collectFieldsAndFragmentNames($context, $selection->getSelectionSet(), $inlineFragmentType); |
||||
0 ignored issues
–
show
It seems like
$selection->getSelectionSet() can also be of type null ; however, parameter $selectionSet of Digia\GraphQL\Validation...ieldsAndFragmentNames() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode , 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
![]() |
|||||
687 | } |
||||
688 | } |
||||
689 | } |
||||
690 | |||||
691 | /** |
||||
692 | * Given a series of Conflicts which occurred between two sub-fields, generate |
||||
693 | * a single Conflict. |
||||
694 | * |
||||
695 | * @param array|Conflict[] $conflicts |
||||
696 | * @param string $responseName |
||||
697 | * @param FieldNode $nodeA |
||||
698 | * @param FieldNode $nodeB |
||||
699 | * @return Conflict|null |
||||
700 | */ |
||||
701 | protected function subfieldConflicts( |
||||
702 | array $conflicts, |
||||
703 | string $responseName, |
||||
704 | FieldNode $nodeA, |
||||
705 | FieldNode $nodeB |
||||
706 | ): ?Conflict { |
||||
707 | if (empty($conflicts)) { |
||||
708 | return null; |
||||
709 | } |
||||
710 | |||||
711 | return new Conflict( |
||||
712 | $responseName, |
||||
713 | array_map(function (Conflict $conflict) { |
||||
714 | return [$conflict->getResponseName(), $conflict->getReason()]; |
||||
715 | }, $conflicts), |
||||
716 | array_reduce($conflicts, function ($allFields, Conflict $conflict) { |
||||
717 | return array_merge($allFields, $conflict->getFieldsA()); |
||||
718 | }, [$nodeA]), |
||||
719 | array_reduce($conflicts, function ($allFields, Conflict $conflict) { |
||||
720 | return array_merge($allFields, $conflict->getFieldsB()); |
||||
721 | }, [$nodeB]) |
||||
722 | ); |
||||
723 | } |
||||
724 | } |
||||
725 |