Skip to content

Commit f010b92

Browse files
committed
RFC: Return type overlap validation
Related GraphQL-js commit: graphql/graphql-js@c034de9
1 parent 0a4c098 commit f010b92

File tree

2 files changed

+317
-60
lines changed

2 files changed

+317
-60
lines changed

graphql/validation/rules/overlapping_fields_can_be_merged.py

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from ...language.printer import print_ast
66
from ...pyutils.default_ordered_dict import DefaultOrderedDict
77
from ...pyutils.pair_set import PairSet
8-
from ...type.definition import (GraphQLInterfaceType, GraphQLObjectType,
9-
get_named_type)
8+
from ...type.definition import (GraphQLInterfaceType, GraphQLList,
9+
GraphQLNonNull, GraphQLObjectType,
10+
get_named_type, is_leaf_type)
1011
from ...utils.type_comparators import is_equal_type
1112
from ...utils.type_from_ast import type_from_ast
1213
from .base import ValidationRule
@@ -19,7 +20,7 @@ def __init__(self, context):
1920
super(OverlappingFieldsCanBeMerged, self).__init__(context)
2021
self.compared_set = PairSet()
2122

22-
def find_conflicts(self, field_map):
23+
def find_conflicts(self, parent_fields_are_mutually_exclusive, field_map):
2324
conflicts = []
2425
for response_name, fields in field_map.items():
2526
field_len = len(fields)
@@ -28,64 +29,98 @@ def find_conflicts(self, field_map):
2829

2930
for field_a in fields:
3031
for field_b in fields:
31-
conflict = self.find_conflict(response_name, field_a, field_b)
32+
conflict = self.find_conflict(
33+
parent_fields_are_mutually_exclusive,
34+
response_name,
35+
field_a,
36+
field_b
37+
)
3238
if conflict:
3339
conflicts.append(conflict)
3440

3541
return conflicts
3642

37-
def find_conflict(self, response_name, field1, field2):
43+
def find_conflict(self, parent_fields_are_mutually_exclusive, response_name, field1, field2):
3844
parent_type1, ast1, def1 = field1
3945
parent_type2, ast2, def2 = field2
4046

4147
# Not a pair
4248
if ast1 is ast2:
4349
return
4450

45-
# If the statically known parent types could not possibly apply at the same
46-
# time, then it is safe to permit them to diverge as they will not present
47-
# any ambiguity by differing.
48-
# It is known that two parent types could never overlap if they are
49-
# different Object types. Interface or Union types might overlap - if not
50-
# in the current state of the schema, then perhaps in some future version,
51-
# thus may not safely diverge.
52-
if parent_type1 != parent_type2 and \
53-
isinstance(parent_type1, GraphQLObjectType) and \
54-
isinstance(parent_type2, GraphQLObjectType):
55-
return
51+
# Memoize, do not report the same issue twice.
52+
# Note: Two overlapping ASTs could be encountered both when
53+
# `parentFieldsAreMutuallyExclusive` is true and is false, which could
54+
# produce different results (when `true` being a subset of `false`).
55+
# However we do not need to include this piece of information when
56+
# memoizing since this rule visits leaf fields before their parent fields,
57+
# ensuring that `parentFieldsAreMutuallyExclusive` is `false` the first
58+
# time two overlapping fields are encountered, ensuring that the full
59+
# set of validation rules are always checked when necessary.
60+
61+
# if parent_type1 != parent_type2 and \
62+
# isinstance(parent_type1, GraphQLObjectType) and \
63+
# isinstance(parent_type2, GraphQLObjectType):
64+
# return
5665

5766
if self.compared_set.has(ast1, ast2):
5867
return
5968

6069
self.compared_set.add(ast1, ast2)
6170

62-
name1 = ast1.name.value
63-
name2 = ast2.name.value
64-
65-
if name1 != name2:
66-
return (
67-
(response_name, '{} and {} are different fields'.format(name1, name2)),
68-
[ast1],
69-
[ast2]
70-
)
7171

72+
# The return type for each field.
7273
type1 = def1 and def1.type
7374
type2 = def2 and def2.type
7475

75-
if type1 and type2 and not self.same_type(type1, type2):
76-
return (
77-
(response_name, 'they return differing types {} and {}'.format(type1, type2)),
78-
[ast1],
79-
[ast2]
76+
# If it is known that two fields could not possibly apply at the same
77+
# time, due to the parent types, then it is safe to permit them to diverge
78+
# in aliased field or arguments used as they will not present any ambiguity
79+
# by differing.
80+
# It is known that two parent types could never overlap if they are
81+
# different Object types. Interface or Union types might overlap - if not
82+
# in the current state of the schema, then perhaps in some future version,
83+
# thus may not safely diverge.
84+
85+
fields_are_mutually_exclusive = (
86+
parent_fields_are_mutually_exclusive or (
87+
parent_type1 != parent_type2 and
88+
isinstance(parent_type1, GraphQLObjectType) and
89+
isinstance(parent_type2, GraphQLObjectType)
8090
)
91+
)
92+
93+
if not fields_are_mutually_exclusive:
94+
name1 = ast1.name.value
95+
name2 = ast2.name.value
8196

82-
if not self.same_arguments(ast1.arguments, ast2.arguments):
97+
if name1 != name2:
98+
return (
99+
(response_name, '{} and {} are different fields'.format(name1, name2)),
100+
[ast1],
101+
[ast2]
102+
)
103+
104+
if not self.same_arguments(ast1.arguments, ast2.arguments):
105+
return (
106+
(response_name, 'they have differing arguments'),
107+
[ast1],
108+
[ast2]
109+
)
110+
111+
if type1 and type2 and do_types_conflict(type1, type2):
83112
return (
84-
(response_name, 'they have differing arguments'),
113+
(response_name, 'they return conflicting types {} and {}'.format(type1, type2)),
85114
[ast1],
86115
[ast2]
87116
)
88117

118+
subfield_map = self.get_subfield_map(ast1, type1, ast2, type2)
119+
if subfield_map:
120+
conflicts = self.find_conflicts(fields_are_mutually_exclusive, subfield_map)
121+
return self.subfield_conflicts(conflicts, response_name, ast1, ast2)
122+
123+
def get_subfield_map(self, ast1, type1, ast2, type2):
89124
selection_set1 = ast1.selection_set
90125
selection_set2 = ast2.selection_set
91126

@@ -104,22 +139,26 @@ def find_conflict(self, response_name, field1, field2):
104139
visited_fragment_names,
105140
subfield_map
106141
)
142+
return subfield_map
107143

108-
conflicts = self.find_conflicts(subfield_map)
109-
if conflicts:
110-
return (
111-
(response_name, [conflict[0] for conflict in conflicts]),
112-
tuple(itertools.chain([ast1], *[conflict[1] for conflict in conflicts])),
113-
tuple(itertools.chain([ast2], *[conflict[2] for conflict in conflicts]))
114-
)
144+
def subfield_conflicts(self, conflicts, response_name, ast1, ast2):
145+
if conflicts:
146+
return (
147+
(response_name, [conflict[0] for conflict in conflicts]),
148+
tuple(itertools.chain([ast1], *[conflict[1] for conflict in conflicts])),
149+
tuple(itertools.chain([ast2], *[conflict[2] for conflict in conflicts]))
150+
)
115151

116152
def leave_SelectionSet(self, node, key, parent, path, ancestors):
153+
# Note: we validate on the reverse traversal so deeper conflicts will be
154+
# caught first, for correct calculation of mutual exclusivity and for
155+
# clearer error messages.
117156
field_map = self.collect_field_asts_and_defs(
118157
self.context.get_parent_type(),
119158
node
120159
)
121160

122-
conflicts = self.find_conflicts(field_map)
161+
conflicts = self.find_conflicts(False, field_map)
123162
if conflicts:
124163
for (reason_name, reason), fields1, fields2 in conflicts:
125164
self.context.report_error(
@@ -228,3 +267,30 @@ def reason_message(cls, reason):
228267
for reason_name, sub_reason in reason)
229268

230269
return reason
270+
271+
272+
def do_types_conflict(type1, type2):
273+
if isinstance(type1, GraphQLList):
274+
if isinstance(type2, GraphQLList):
275+
return do_types_conflict(type1.of_type, type2.of_type)
276+
return True
277+
278+
if isinstance(type2, GraphQLList):
279+
if isinstance(type1, GraphQLList):
280+
return do_types_conflict(type1.of_type, type2.of_type)
281+
return True
282+
283+
if isinstance(type1, GraphQLNonNull):
284+
if isinstance(type2, GraphQLNonNull):
285+
return do_types_conflict(type1.of_type, type2.of_type)
286+
return True
287+
288+
if isinstance(type2, GraphQLNonNull):
289+
if isinstance(type1, GraphQLNonNull):
290+
return do_types_conflict(type1.of_type, type2.of_type)
291+
return True
292+
293+
if is_leaf_type(type1) or is_leaf_type(type2):
294+
return type1 != type2
295+
296+
return False

0 commit comments

Comments
 (0)