55from ...language .printer import print_ast
66from ...pyutils .default_ordered_dict import DefaultOrderedDict
77from ...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 )
1011from ...utils .type_comparators import is_equal_type
1112from ...utils .type_from_ast import type_from_ast
1213from .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