@@ -58,9 +58,10 @@ class OverlappingFieldsCanBeMergedRule(ValidationRule):
5858
5959 def __init__ (self , context : ValidationContext ) -> None :
6060 super ().__init__ (context )
61- # A memoization for when two fragments are compared "between" each other for
62- # conflicts. Two fragments may be compared many times, so memoizing this can
63- # dramatically improve the performance of this validator.
61+ # A memoization for when fields and a fragment or two fragments are compared
62+ # "between" each other for conflicts. Comparisons may be made many times, so
63+ # memoizing this can dramatically improve the performance of this validator.
64+ self .compared_fields_and_fragment_pairs = OrderedPairSet ()
6465 self .compared_fragment_pairs = PairSet ()
6566
6667 # A cache for the "field map" and list of fragment names found in any given
@@ -72,6 +73,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N
7273 conflicts = find_conflicts_within_selection_set (
7374 self .context ,
7475 self .cached_fields_and_fragment_names ,
76+ self .compared_fields_and_fragment_pairs ,
7577 self .compared_fragment_pairs ,
7678 self .context .get_parent_type (),
7779 selection_set ,
@@ -155,6 +157,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N
155157def find_conflicts_within_selection_set (
156158 context : ValidationContext ,
157159 cached_fields_and_fragment_names : dict ,
160+ compared_fields_and_fragment_pairs : OrderedPairSet ,
158161 compared_fragment_pairs : PairSet ,
159162 parent_type : GraphQLNamedType | None ,
160163 selection_set : SelectionSetNode ,
@@ -178,6 +181,7 @@ def find_conflicts_within_selection_set(
178181 context ,
179182 conflicts ,
180183 cached_fields_and_fragment_names ,
184+ compared_fields_and_fragment_pairs ,
181185 compared_fragment_pairs ,
182186 field_map ,
183187 )
@@ -190,6 +194,7 @@ def find_conflicts_within_selection_set(
190194 context ,
191195 conflicts ,
192196 cached_fields_and_fragment_names ,
197+ compared_fields_and_fragment_pairs ,
193198 compared_fragment_pairs ,
194199 False ,
195200 field_map ,
@@ -204,6 +209,7 @@ def find_conflicts_within_selection_set(
204209 context ,
205210 conflicts ,
206211 cached_fields_and_fragment_names ,
212+ compared_fields_and_fragment_pairs ,
207213 compared_fragment_pairs ,
208214 False ,
209215 fragment_name ,
@@ -217,6 +223,7 @@ def collect_conflicts_between_fields_and_fragment(
217223 context : ValidationContext ,
218224 conflicts : list [Conflict ],
219225 cached_fields_and_fragment_names : dict ,
226+ compared_fields_and_fragment_pairs : OrderedPairSet ,
220227 compared_fragment_pairs : PairSet ,
221228 are_mutually_exclusive : bool ,
222229 field_map : NodeAndDefCollection ,
@@ -227,6 +234,16 @@ def collect_conflicts_between_fields_and_fragment(
227234 Collect all conflicts found between a set of fields and a fragment reference
228235 including via spreading in any nested fragments.
229236 """
237+ # Memoize so the fields and fragments are not compared for conflicts more
238+ # than once.
239+ if compared_fields_and_fragment_pairs .has (
240+ field_map , fragment_name , are_mutually_exclusive
241+ ):
242+ return
243+ compared_fields_and_fragment_pairs .add (
244+ field_map , fragment_name , are_mutually_exclusive
245+ )
246+
230247 fragment = context .get_fragment (fragment_name )
231248 if not fragment :
232249 return
@@ -245,6 +262,7 @@ def collect_conflicts_between_fields_and_fragment(
245262 context ,
246263 conflicts ,
247264 cached_fields_and_fragment_names ,
265+ compared_fields_and_fragment_pairs ,
248266 compared_fragment_pairs ,
249267 are_mutually_exclusive ,
250268 field_map ,
@@ -254,19 +272,11 @@ def collect_conflicts_between_fields_and_fragment(
254272 # (E) Then collect any conflicts between the provided collection of fields and any
255273 # fragment names found in the given fragment.
256274 for referenced_fragment_name in referenced_fragment_names :
257- # Memoize so two fragments are not compared for conflicts more than once.
258- if compared_fragment_pairs .has (
259- referenced_fragment_name , fragment_name , are_mutually_exclusive
260- ):
261- continue
262- compared_fragment_pairs .add (
263- referenced_fragment_name , fragment_name , are_mutually_exclusive
264- )
265-
266275 collect_conflicts_between_fields_and_fragment (
267276 context ,
268277 conflicts ,
269278 cached_fields_and_fragment_names ,
279+ compared_fields_and_fragment_pairs ,
270280 compared_fragment_pairs ,
271281 are_mutually_exclusive ,
272282 field_map ,
@@ -278,6 +288,7 @@ def collect_conflicts_between_fragments(
278288 context : ValidationContext ,
279289 conflicts : list [Conflict ],
280290 cached_fields_and_fragment_names : dict ,
291+ compared_fields_and_fragment_pairs : OrderedPairSet ,
281292 compared_fragment_pairs : PairSet ,
282293 are_mutually_exclusive : bool ,
283294 fragment_name1 : str ,
@@ -318,6 +329,7 @@ def collect_conflicts_between_fragments(
318329 context ,
319330 conflicts ,
320331 cached_fields_and_fragment_names ,
332+ compared_fields_and_fragment_pairs ,
321333 compared_fragment_pairs ,
322334 are_mutually_exclusive ,
323335 field_map1 ,
@@ -331,6 +343,7 @@ def collect_conflicts_between_fragments(
331343 context ,
332344 conflicts ,
333345 cached_fields_and_fragment_names ,
346+ compared_fields_and_fragment_pairs ,
334347 compared_fragment_pairs ,
335348 are_mutually_exclusive ,
336349 fragment_name1 ,
@@ -344,6 +357,7 @@ def collect_conflicts_between_fragments(
344357 context ,
345358 conflicts ,
346359 cached_fields_and_fragment_names ,
360+ compared_fields_and_fragment_pairs ,
347361 compared_fragment_pairs ,
348362 are_mutually_exclusive ,
349363 referenced_fragment_name1 ,
@@ -354,6 +368,7 @@ def collect_conflicts_between_fragments(
354368def find_conflicts_between_sub_selection_sets (
355369 context : ValidationContext ,
356370 cached_fields_and_fragment_names : dict ,
371+ compared_fields_and_fragment_pairs : OrderedPairSet ,
357372 compared_fragment_pairs : PairSet ,
358373 are_mutually_exclusive : bool ,
359374 parent_type1 : GraphQLNamedType | None ,
@@ -381,6 +396,7 @@ def find_conflicts_between_sub_selection_sets(
381396 context ,
382397 conflicts ,
383398 cached_fields_and_fragment_names ,
399+ compared_fields_and_fragment_pairs ,
384400 compared_fragment_pairs ,
385401 are_mutually_exclusive ,
386402 field_map1 ,
@@ -395,6 +411,7 @@ def find_conflicts_between_sub_selection_sets(
395411 context ,
396412 conflicts ,
397413 cached_fields_and_fragment_names ,
414+ compared_fields_and_fragment_pairs ,
398415 compared_fragment_pairs ,
399416 are_mutually_exclusive ,
400417 field_map1 ,
@@ -409,6 +426,7 @@ def find_conflicts_between_sub_selection_sets(
409426 context ,
410427 conflicts ,
411428 cached_fields_and_fragment_names ,
429+ compared_fields_and_fragment_pairs ,
412430 compared_fragment_pairs ,
413431 are_mutually_exclusive ,
414432 field_map2 ,
@@ -424,6 +442,7 @@ def find_conflicts_between_sub_selection_sets(
424442 context ,
425443 conflicts ,
426444 cached_fields_and_fragment_names ,
445+ compared_fields_and_fragment_pairs ,
427446 compared_fragment_pairs ,
428447 are_mutually_exclusive ,
429448 fragment_name1 ,
@@ -437,6 +456,7 @@ def collect_conflicts_within(
437456 context : ValidationContext ,
438457 conflicts : list [Conflict ],
439458 cached_fields_and_fragment_names : dict ,
459+ compared_fields_and_fragment_pairs : OrderedPairSet ,
440460 compared_fragment_pairs : PairSet ,
441461 field_map : NodeAndDefCollection ,
442462) -> None :
@@ -455,6 +475,7 @@ def collect_conflicts_within(
455475 conflict = find_conflict (
456476 context ,
457477 cached_fields_and_fragment_names ,
478+ compared_fields_and_fragment_pairs ,
458479 compared_fragment_pairs ,
459480 # within one collection is never mutually exclusive
460481 False ,
@@ -470,6 +491,7 @@ def collect_conflicts_between(
470491 context : ValidationContext ,
471492 conflicts : list [Conflict ],
472493 cached_fields_and_fragment_names : dict ,
494+ compared_fields_and_fragment_pairs : OrderedPairSet ,
473495 compared_fragment_pairs : PairSet ,
474496 parent_fields_are_mutually_exclusive : bool ,
475497 field_map1 : NodeAndDefCollection ,
@@ -495,6 +517,7 @@ def collect_conflicts_between(
495517 conflict = find_conflict (
496518 context ,
497519 cached_fields_and_fragment_names ,
520+ compared_fields_and_fragment_pairs ,
498521 compared_fragment_pairs ,
499522 parent_fields_are_mutually_exclusive ,
500523 response_name ,
@@ -508,6 +531,7 @@ def collect_conflicts_between(
508531def find_conflict (
509532 context : ValidationContext ,
510533 cached_fields_and_fragment_names : dict ,
534+ compared_fields_and_fragment_pairs : OrderedPairSet ,
511535 compared_fragment_pairs : PairSet ,
512536 parent_fields_are_mutually_exclusive : bool ,
513537 response_name : str ,
@@ -578,6 +602,7 @@ def find_conflict(
578602 conflicts = find_conflicts_between_sub_selection_sets (
579603 context ,
580604 cached_fields_and_fragment_names ,
605+ compared_fields_and_fragment_pairs ,
581606 compared_fragment_pairs ,
582607 are_mutually_exclusive ,
583608 get_named_type (type1 ),
@@ -773,6 +798,43 @@ def subfield_conflicts(
773798 return None # no conflict
774799
775800
801+ class OrderedPairSet :
802+ """Ordered pair set
803+
804+ A way to keep track of pairs of things where the ordering of the pair matters.
805+
806+ Provides a third argument for has/add to allow flagging the pair as weakly or
807+ strongly present within the collection.
808+
809+ The first element is matched by object identity (its ``id``), since field maps
810+ are unhashable mappings that are kept alive for the duration of the validation.
811+ """
812+
813+ __slots__ = ("_data" ,)
814+
815+ _data : dict [int , dict [str , bool ]]
816+
817+ def __init__ (self ) -> None :
818+ self ._data = {}
819+
820+ def has (self , a : NodeAndDefCollection , b : str , weakly_present : bool ) -> bool :
821+ map_ = self ._data .get (id (a ))
822+ if map_ is None :
823+ return False
824+ result = map_ .get (b )
825+ if result is None :
826+ return False
827+
828+ return True if weakly_present else weakly_present == result
829+
830+ def add (self , a : NodeAndDefCollection , b : str , weakly_present : bool ) -> None :
831+ map_ = self ._data .get (id (a ))
832+ if map_ is None :
833+ self ._data [id (a )] = {b : weakly_present }
834+ else :
835+ map_ [b ] = weakly_present
836+
837+
776838class PairSet :
777839 """Pair set
778840
0 commit comments