5
5
from ...language .printer import print_ast
6
6
from ...pyutils .default_ordered_dict import DefaultOrderedDict
7
7
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 )
10
11
from ...utils .type_comparators import is_equal_type
11
12
from ...utils .type_from_ast import type_from_ast
12
13
from .base import ValidationRule
@@ -19,7 +20,7 @@ def __init__(self, context):
19
20
super (OverlappingFieldsCanBeMerged , self ).__init__ (context )
20
21
self .compared_set = PairSet ()
21
22
22
- def find_conflicts (self , field_map ):
23
+ def find_conflicts (self , parent_fields_are_mutually_exclusive , field_map ):
23
24
conflicts = []
24
25
for response_name , fields in field_map .items ():
25
26
field_len = len (fields )
@@ -28,64 +29,98 @@ def find_conflicts(self, field_map):
28
29
29
30
for field_a in fields :
30
31
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
+ )
32
38
if conflict :
33
39
conflicts .append (conflict )
34
40
35
41
return conflicts
36
42
37
- def find_conflict (self , response_name , field1 , field2 ):
43
+ def find_conflict (self , parent_fields_are_mutually_exclusive , response_name , field1 , field2 ):
38
44
parent_type1 , ast1 , def1 = field1
39
45
parent_type2 , ast2 , def2 = field2
40
46
41
47
# Not a pair
42
48
if ast1 is ast2 :
43
49
return
44
50
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
56
65
57
66
if self .compared_set .has (ast1 , ast2 ):
58
67
return
59
68
60
69
self .compared_set .add (ast1 , ast2 )
61
70
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
- )
71
71
72
+ # The return type for each field.
72
73
type1 = def1 and def1 .type
73
74
type2 = def2 and def2 .type
74
75
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 )
80
90
)
91
+ )
92
+
93
+ if not fields_are_mutually_exclusive :
94
+ name1 = ast1 .name .value
95
+ name2 = ast2 .name .value
81
96
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 ):
83
112
return (
84
- (response_name , 'they have differing arguments' ),
113
+ (response_name , 'they return conflicting types {} and {}' . format ( type1 , type2 ) ),
85
114
[ast1 ],
86
115
[ast2 ]
87
116
)
88
117
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 ):
89
124
selection_set1 = ast1 .selection_set
90
125
selection_set2 = ast2 .selection_set
91
126
@@ -104,22 +139,26 @@ def find_conflict(self, response_name, field1, field2):
104
139
visited_fragment_names ,
105
140
subfield_map
106
141
)
142
+ return subfield_map
107
143
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
+ )
115
151
116
152
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.
117
156
field_map = self .collect_field_asts_and_defs (
118
157
self .context .get_parent_type (),
119
158
node
120
159
)
121
160
122
- conflicts = self .find_conflicts (field_map )
161
+ conflicts = self .find_conflicts (False , field_map )
123
162
if conflicts :
124
163
for (reason_name , reason ), fields1 , fields2 in conflicts :
125
164
self .context .report_error (
@@ -228,3 +267,30 @@ def reason_message(cls, reason):
228
267
for reason_name , sub_reason in reason )
229
268
230
269
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