@@ -56,9 +56,10 @@ class OverlappingFieldsCanBeMergedRule(ValidationRule):
5656
5757 def __init__ (self , context : ValidationContext ):
5858 super ().__init__ (context )
59- # A memoization for when two fragments are compared "between" each other for
60- # conflicts. Two fragments may be compared many times, so memoizing this can
61- # dramatically improve the performance of this validator.
59+ # A memoization for when fields and a fragment or two fragments are compared
60+ # "between" each other for conflicts. Comparisons may be made many times, so
61+ # memoizing this can dramatically improve the performance of this validator.
62+ self .compared_fields_and_fragment_pairs = OrderedPairSet ()
6263 self .compared_fragment_pairs = PairSet ()
6364
6465 # A cache for the "field map" and list of fragment names found in any given
@@ -70,6 +71,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N
7071 conflicts = find_conflicts_within_selection_set (
7172 self .context ,
7273 self .cached_fields_and_fragment_names ,
74+ self .compared_fields_and_fragment_pairs ,
7375 self .compared_fragment_pairs ,
7476 self .context .get_parent_type (),
7577 selection_set ,
@@ -156,6 +158,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N
156158def find_conflicts_within_selection_set (
157159 context : ValidationContext ,
158160 cached_fields_and_fragment_names : Dict ,
161+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
159162 compared_fragment_pairs : "PairSet" ,
160163 parent_type : Optional [GraphQLNamedType ],
161164 selection_set : SelectionSetNode ,
@@ -179,6 +182,7 @@ def find_conflicts_within_selection_set(
179182 context ,
180183 conflicts ,
181184 cached_fields_and_fragment_names ,
185+ compared_fields_and_fragment_pairs ,
182186 compared_fragment_pairs ,
183187 field_map ,
184188 )
@@ -191,6 +195,7 @@ def find_conflicts_within_selection_set(
191195 context ,
192196 conflicts ,
193197 cached_fields_and_fragment_names ,
198+ compared_fields_and_fragment_pairs ,
194199 compared_fragment_pairs ,
195200 False ,
196201 field_map ,
@@ -205,6 +210,7 @@ def find_conflicts_within_selection_set(
205210 context ,
206211 conflicts ,
207212 cached_fields_and_fragment_names ,
213+ compared_fields_and_fragment_pairs ,
208214 compared_fragment_pairs ,
209215 False ,
210216 fragment_name ,
@@ -218,6 +224,7 @@ def collect_conflicts_between_fields_and_fragment(
218224 context : ValidationContext ,
219225 conflicts : List [Conflict ],
220226 cached_fields_and_fragment_names : Dict ,
227+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
221228 compared_fragment_pairs : "PairSet" ,
222229 are_mutually_exclusive : bool ,
223230 field_map : NodeAndDefCollection ,
@@ -228,6 +235,16 @@ def collect_conflicts_between_fields_and_fragment(
228235 Collect all conflicts found between a set of fields and a fragment reference
229236 including via spreading in any nested fragments.
230237 """
238+ # Memoize so the fields and fragments are not compared for conflicts more
239+ # than once.
240+ if compared_fields_and_fragment_pairs .has (
241+ field_map , fragment_name , are_mutually_exclusive
242+ ):
243+ return
244+ compared_fields_and_fragment_pairs .add (
245+ field_map , fragment_name , are_mutually_exclusive
246+ )
247+
231248 fragment = context .get_fragment (fragment_name )
232249 if not fragment :
233250 return None
@@ -246,6 +263,7 @@ def collect_conflicts_between_fields_and_fragment(
246263 context ,
247264 conflicts ,
248265 cached_fields_and_fragment_names ,
266+ compared_fields_and_fragment_pairs ,
249267 compared_fragment_pairs ,
250268 are_mutually_exclusive ,
251269 field_map ,
@@ -255,19 +273,11 @@ def collect_conflicts_between_fields_and_fragment(
255273 # (E) Then collect any conflicts between the provided collection of fields and any
256274 # fragment names found in the given fragment.
257275 for referenced_fragment_name in referenced_fragment_names :
258- # Memoize so two fragments are not compared for conflicts more than once.
259- if compared_fragment_pairs .has (
260- referenced_fragment_name , fragment_name , are_mutually_exclusive
261- ):
262- continue
263- compared_fragment_pairs .add (
264- referenced_fragment_name , fragment_name , are_mutually_exclusive
265- )
266-
267276 collect_conflicts_between_fields_and_fragment (
268277 context ,
269278 conflicts ,
270279 cached_fields_and_fragment_names ,
280+ compared_fields_and_fragment_pairs ,
271281 compared_fragment_pairs ,
272282 are_mutually_exclusive ,
273283 field_map ,
@@ -279,6 +289,7 @@ def collect_conflicts_between_fragments(
279289 context : ValidationContext ,
280290 conflicts : List [Conflict ],
281291 cached_fields_and_fragment_names : Dict ,
292+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
282293 compared_fragment_pairs : "PairSet" ,
283294 are_mutually_exclusive : bool ,
284295 fragment_name1 : str ,
@@ -319,6 +330,7 @@ def collect_conflicts_between_fragments(
319330 context ,
320331 conflicts ,
321332 cached_fields_and_fragment_names ,
333+ compared_fields_and_fragment_pairs ,
322334 compared_fragment_pairs ,
323335 are_mutually_exclusive ,
324336 field_map1 ,
@@ -332,6 +344,7 @@ def collect_conflicts_between_fragments(
332344 context ,
333345 conflicts ,
334346 cached_fields_and_fragment_names ,
347+ compared_fields_and_fragment_pairs ,
335348 compared_fragment_pairs ,
336349 are_mutually_exclusive ,
337350 fragment_name1 ,
@@ -345,6 +358,7 @@ def collect_conflicts_between_fragments(
345358 context ,
346359 conflicts ,
347360 cached_fields_and_fragment_names ,
361+ compared_fields_and_fragment_pairs ,
348362 compared_fragment_pairs ,
349363 are_mutually_exclusive ,
350364 referenced_fragment_name1 ,
@@ -355,6 +369,7 @@ def collect_conflicts_between_fragments(
355369def find_conflicts_between_sub_selection_sets (
356370 context : ValidationContext ,
357371 cached_fields_and_fragment_names : Dict ,
372+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
358373 compared_fragment_pairs : "PairSet" ,
359374 are_mutually_exclusive : bool ,
360375 parent_type1 : Optional [GraphQLNamedType ],
@@ -382,6 +397,7 @@ def find_conflicts_between_sub_selection_sets(
382397 context ,
383398 conflicts ,
384399 cached_fields_and_fragment_names ,
400+ compared_fields_and_fragment_pairs ,
385401 compared_fragment_pairs ,
386402 are_mutually_exclusive ,
387403 field_map1 ,
@@ -396,6 +412,7 @@ def find_conflicts_between_sub_selection_sets(
396412 context ,
397413 conflicts ,
398414 cached_fields_and_fragment_names ,
415+ compared_fields_and_fragment_pairs ,
399416 compared_fragment_pairs ,
400417 are_mutually_exclusive ,
401418 field_map1 ,
@@ -410,6 +427,7 @@ def find_conflicts_between_sub_selection_sets(
410427 context ,
411428 conflicts ,
412429 cached_fields_and_fragment_names ,
430+ compared_fields_and_fragment_pairs ,
413431 compared_fragment_pairs ,
414432 are_mutually_exclusive ,
415433 field_map2 ,
@@ -425,6 +443,7 @@ def find_conflicts_between_sub_selection_sets(
425443 context ,
426444 conflicts ,
427445 cached_fields_and_fragment_names ,
446+ compared_fields_and_fragment_pairs ,
428447 compared_fragment_pairs ,
429448 are_mutually_exclusive ,
430449 fragment_name1 ,
@@ -438,6 +457,7 @@ def collect_conflicts_within(
438457 context : ValidationContext ,
439458 conflicts : List [Conflict ],
440459 cached_fields_and_fragment_names : Dict ,
460+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
441461 compared_fragment_pairs : "PairSet" ,
442462 field_map : NodeAndDefCollection ,
443463) -> None :
@@ -456,6 +476,7 @@ def collect_conflicts_within(
456476 conflict = find_conflict (
457477 context ,
458478 cached_fields_and_fragment_names ,
479+ compared_fields_and_fragment_pairs ,
459480 compared_fragment_pairs ,
460481 # within one collection is never mutually exclusive
461482 False ,
@@ -471,6 +492,7 @@ def collect_conflicts_between(
471492 context : ValidationContext ,
472493 conflicts : List [Conflict ],
473494 cached_fields_and_fragment_names : Dict ,
495+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
474496 compared_fragment_pairs : "PairSet" ,
475497 parent_fields_are_mutually_exclusive : bool ,
476498 field_map1 : NodeAndDefCollection ,
@@ -496,6 +518,7 @@ def collect_conflicts_between(
496518 conflict = find_conflict (
497519 context ,
498520 cached_fields_and_fragment_names ,
521+ compared_fields_and_fragment_pairs ,
499522 compared_fragment_pairs ,
500523 parent_fields_are_mutually_exclusive ,
501524 response_name ,
@@ -509,6 +532,7 @@ def collect_conflicts_between(
509532def find_conflict (
510533 context : ValidationContext ,
511534 cached_fields_and_fragment_names : Dict ,
535+ compared_fields_and_fragment_pairs : "OrderedPairSet" ,
512536 compared_fragment_pairs : "PairSet" ,
513537 parent_fields_are_mutually_exclusive : bool ,
514538 response_name : str ,
@@ -570,6 +594,7 @@ def find_conflict(
570594 conflicts = find_conflicts_between_sub_selection_sets (
571595 context ,
572596 cached_fields_and_fragment_names ,
597+ compared_fields_and_fragment_pairs ,
573598 compared_fragment_pairs ,
574599 are_mutually_exclusive ,
575600 get_named_type (type1 ),
@@ -745,6 +770,43 @@ def subfield_conflicts(
745770 return None # no conflict
746771
747772
773+ class OrderedPairSet :
774+ """Ordered pair set
775+
776+ A way to keep track of pairs of things where the ordering of the pair matters.
777+
778+ Provides a third argument for has/add to allow flagging the pair as weakly or
779+ strongly present within the collection.
780+
781+ The first element is matched by object identity (its ``id``), since field maps
782+ are unhashable mappings that are kept alive for the duration of the validation.
783+ """
784+
785+ __slots__ = ("_data" ,)
786+
787+ _data : Dict [int , Dict [str , bool ]]
788+
789+ def __init__ (self ) -> None :
790+ self ._data = {}
791+
792+ def has (self , a : NodeAndDefCollection , b : str , weakly_present : bool ) -> bool :
793+ map_ = self ._data .get (id (a ))
794+ if map_ is None :
795+ return False
796+ result = map_ .get (b )
797+ if result is None :
798+ return False
799+
800+ return True if weakly_present else weakly_present == result
801+
802+ def add (self , a : NodeAndDefCollection , b : str , weakly_present : bool ) -> None :
803+ map_ = self ._data .get (id (a ))
804+ if map_ is None :
805+ self ._data [id (a )] = {b : weakly_present }
806+ else :
807+ map_ [b ] = weakly_present
808+
809+
748810class PairSet :
749811 """Pair set
750812
0 commit comments