graphql 2.5.22 → 2.5.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/execution/interpreter/runtime.rb +3 -2
  3. data/lib/graphql/execution/interpreter.rb +6 -9
  4. data/lib/graphql/execution/lazy.rb +1 -1
  5. data/lib/graphql/execution/next/field_resolve_step.rb +93 -61
  6. data/lib/graphql/execution/next/load_argument_step.rb +5 -1
  7. data/lib/graphql/execution/next/prepare_object_step.rb +2 -2
  8. data/lib/graphql/execution/next/runner.rb +48 -26
  9. data/lib/graphql/execution/next.rb +3 -1
  10. data/lib/graphql/execution.rb +7 -4
  11. data/lib/graphql/execution_error.rb +5 -1
  12. data/lib/graphql/query/context.rb +1 -1
  13. data/lib/graphql/schema/field.rb +3 -4
  14. data/lib/graphql/schema/list.rb +1 -1
  15. data/lib/graphql/schema/member/has_fields.rb +5 -1
  16. data/lib/graphql/schema/non_null.rb +1 -1
  17. data/lib/graphql/schema/resolver.rb +18 -3
  18. data/lib/graphql/schema/subscription.rb +0 -2
  19. data/lib/graphql/schema/visibility/profile.rb +68 -49
  20. data/lib/graphql/schema/wrapper.rb +7 -1
  21. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  22. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  23. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  24. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  25. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  26. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  27. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -2
  28. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  29. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  30. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  31. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  32. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  33. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  34. data/lib/graphql/static_validation/validation_context.rb +1 -1
  35. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  36. data/lib/graphql/subscriptions/event.rb +1 -0
  37. data/lib/graphql/subscriptions.rb +20 -0
  38. data/lib/graphql/tracing/perfetto_trace.rb +2 -2
  39. data/lib/graphql/unauthorized_error.rb +4 -0
  40. data/lib/graphql/version.rb +1 -1
  41. metadata +3 -3
@@ -8,31 +8,103 @@ module GraphQL
8
8
  # fragments) either correspond to distinct response names or can be merged
9
9
  # without ambiguity.
10
10
  #
11
- # Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
11
+ # Optimized algorithm based on:
12
+ # https://tech.new-work.se/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01
13
+ #
14
+ # Instead of comparing fields, fields-vs-fragments, and fragments-vs-fragments
15
+ # separately (which leads to exponential recursion through nested fragments),
16
+ # we flatten all fragment spreads into a single field map and compare within it.
12
17
  NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
13
18
 
14
- Field = Struct.new(:node, :definition, :owner_type, :parents)
15
- FragmentSpread = Struct.new(:name, :parents)
19
+ class Field
20
+ attr_reader :node, :definition, :owner_type, :parents
21
+
22
+ def initialize(node, definition, owner_type, parents)
23
+ @node = node
24
+ @definition = definition
25
+ @owner_type = owner_type
26
+ @parents = parents
27
+ end
28
+
29
+ def return_type
30
+ @return_type ||= @definition&.type
31
+ end
32
+
33
+ def unwrapped_return_type
34
+ @unwrapped_return_type ||= return_type&.unwrap
35
+ end
36
+ end
16
37
 
17
38
  def initialize(*)
18
39
  super
19
- @visited_fragments = {}
20
- @compared_fragments = {}
21
40
  @conflict_count = 0
41
+ @max_errors = context.max_errors
42
+ @fragments = context.fragments
43
+ # Track which sub-selection node pairs have been compared to prevent
44
+ # infinite recursion with cyclic fragments
45
+ @compared_sub_selections = {}.compare_by_identity
46
+ # Cache mutually_exclusive? results for type pairs
47
+ @mutually_exclusive_cache = {}.compare_by_identity
48
+ # Cache collect_fields results for sub-selection comparison
49
+ @sub_fields_cache = {}.compare_by_identity
22
50
  end
23
51
 
24
52
  def on_operation_definition(node, _parent)
25
- setting_errors { conflicts_within_selection_set(node, type_definition) }
53
+ @conflicts = nil
54
+ conflicts_within_selection_set(node, type_definition)
55
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
26
56
  super
27
57
  end
28
58
 
29
59
  def on_field(node, _parent)
30
- setting_errors { conflicts_within_selection_set(node, type_definition) }
60
+ if !node.selections.empty? && selections_may_conflict?(node.selections)
61
+ @conflicts = nil
62
+ conflicts_within_selection_set(node, type_definition)
63
+ @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
64
+ end
31
65
  super
32
66
  end
33
67
 
34
68
  private
35
69
 
70
+ # Quick check: can the direct children of this selection set possibly conflict?
71
+ # If all direct selections are Fields with unique names and no aliases,
72
+ # and there are no fragments, then no response key can have >1 field,
73
+ # so there are no merge conflicts to check at this level.
74
+ def selections_may_conflict?(selections)
75
+ i = 0
76
+ len = selections.size
77
+ while i < len
78
+ sel = selections[i]
79
+ # Fragment spread or inline fragment — needs full check
80
+ return true unless sel.is_a?(GraphQL::Language::Nodes::Field)
81
+
82
+ # Aliased field — could create duplicate response key
83
+ return true if sel.alias
84
+
85
+ i += 1
86
+ end
87
+
88
+ # All are unaliased fields — check for duplicate names
89
+ # For small sets, O(n²) is cheaper than hash allocation
90
+ if len <= 8
91
+ i = 0
92
+ while i < len
93
+ j = i + 1
94
+ name_i = selections[i].name
95
+ while j < len
96
+ return true if selections[j].name == name_i
97
+ j += 1
98
+ end
99
+ i += 1
100
+ end
101
+
102
+ false
103
+ else
104
+ true # Assume potential conflicts for larger sets
105
+ end
106
+ end
107
+
36
108
  def conflicts
37
109
  @conflicts ||= Hash.new do |h, error_type|
38
110
  h[error_type] = Hash.new do |h2, field_name|
@@ -41,177 +113,201 @@ module GraphQL
41
113
  end
42
114
  end
43
115
 
44
- def setting_errors
45
- @conflicts = nil
46
- yield
47
- # don't initialize these if they weren't initialized in the block:
48
- @conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
49
- end
50
-
116
+ # Core algorithm: collect ALL fields (expanding fragments inline) into a flat
117
+ # map keyed by response key, then compare within each group.
51
118
  def conflicts_within_selection_set(node, parent_type)
52
119
  return if parent_type.nil?
120
+ return if node.selections.empty?
53
121
 
54
- fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: nil)
55
-
56
- # (A) Find find all conflicts "within" the fields of this selection set.
57
- find_conflicts_within(fields)
58
-
59
- fragment_spreads.each_with_index do |fragment_spread, i|
60
- are_mutually_exclusive = mutually_exclusive?(
61
- fragment_spread.parents,
62
- [parent_type]
63
- )
64
-
65
- # (B) Then find conflicts between these fields and those represented by
66
- # each spread fragment name found.
67
- find_conflicts_between_fields_and_fragment(
68
- fragment_spread,
69
- fields,
70
- mutually_exclusive: are_mutually_exclusive,
71
- )
72
-
73
- # (C) Then compare this fragment with all other fragments found in this
74
- # selection set to collect conflicts between fragments spread together.
75
- # This compares each item in the list of fragment names to every other
76
- # item in that same list (except for itself).
77
- fragment_spreads[i + 1..-1].each do |fragment_spread2|
78
- are_mutually_exclusive = mutually_exclusive?(
79
- fragment_spread.parents,
80
- fragment_spread2.parents
81
- )
82
-
83
- find_conflicts_between_fragments(
84
- fragment_spread,
85
- fragment_spread2,
86
- mutually_exclusive: are_mutually_exclusive,
87
- )
88
- end
89
- end
90
- end
91
-
92
- def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:)
93
- fragment_name1 = fragment_spread1.name
94
- fragment_name2 = fragment_spread2.name
95
- return if fragment_name1 == fragment_name2
96
-
97
- cache_key = compared_fragments_key(
98
- fragment_name1,
99
- fragment_name2,
100
- mutually_exclusive,
101
- )
102
- if @compared_fragments.key?(cache_key)
103
- return
104
- else
105
- @compared_fragments[cache_key] = true
106
- end
122
+ # Collect all fields from this selection set, expanding fragments transitively
123
+ response_keys = collect_fields(node.selections, owner_type: parent_type, parents: [])
107
124
 
108
- fragment1 = context.fragments[fragment_name1]
109
- fragment2 = context.fragments[fragment_name2]
125
+ # Find conflicts within each response key group
126
+ find_conflicts_within(response_keys)
127
+ end
110
128
 
111
- return if fragment1.nil? || fragment2.nil?
129
+ # Collect all fields from selections, expanding fragment spreads inline.
130
+ # Returns a Hash of { response_key => Field | [Field, ...] }
131
+ def collect_fields(selections, owner_type:, parents:)
132
+ response_keys = {}
133
+ collect_fields_inner(selections, owner_type: owner_type, parents: parents, response_keys: response_keys, visited_fragments: nil)
134
+ response_keys
135
+ end
112
136
 
113
- fragment_type1 = context.query.types.type(fragment1.type.name)
114
- fragment_type2 = context.query.types.type(fragment2.type.name)
137
+ def collect_fields_inner(selections, owner_type:, parents:, response_keys:, visited_fragments:)
138
+ deferred_spreads = nil
139
+ sel_idx = 0
140
+ sel_len = selections.size
115
141
 
116
- return if fragment_type1.nil? || fragment_type2.nil?
142
+ while sel_idx < sel_len
143
+ sel = selections[sel_idx]
117
144
 
118
- fragment_fields1, fragment_spreads1 = fields_and_fragments_from_selection(
119
- fragment1,
120
- owner_type: fragment_type1,
121
- parents: [*fragment_spread1.parents, fragment_type1]
122
- )
123
- fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection(
124
- fragment2,
125
- owner_type: fragment_type1,
126
- parents: [*fragment_spread2.parents, fragment_type2]
127
- )
128
-
129
- # (F) First, find all conflicts between these two collections of fields
130
- # (not including any nested fragments).
131
- find_conflicts_between(
132
- fragment_fields1,
133
- fragment_fields2,
134
- mutually_exclusive: mutually_exclusive,
135
- )
145
+ case sel
146
+ when GraphQL::Language::Nodes::Field
147
+ definition = @types.field(owner_type, sel.name)
148
+ key = sel.alias || sel.name
149
+ field = Field.new(sel, definition, owner_type, parents)
150
+ existing = response_keys[key]
151
+
152
+ if existing.nil?
153
+ response_keys[key] = field
154
+ elsif existing.is_a?(Field)
155
+ response_keys[key] = [existing, field]
156
+ else
157
+ existing << field
158
+ end
159
+ when GraphQL::Language::Nodes::InlineFragment
160
+ frag_type = sel.type ? @types.type(sel.type.name) : owner_type
136
161
 
137
- # (G) Then collect conflicts between the first fragment and any nested
138
- # fragments spread in the second fragment.
139
- fragment_spreads2.each do |fragment_spread|
140
- find_conflicts_between_fragments(
141
- fragment_spread1,
142
- fragment_spread,
143
- mutually_exclusive: mutually_exclusive,
144
- )
145
- end
162
+ if frag_type
163
+ new_parents = parents.dup
164
+ new_parents << frag_type
165
+ collect_fields_inner(sel.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
166
+ end
167
+ when GraphQL::Language::Nodes::FragmentSpread
168
+ (deferred_spreads ||= []) << sel
169
+ end
146
170
 
147
- # (G) Then collect conflicts between the first fragment and any nested
148
- # fragments spread in the second fragment.
149
- fragment_spreads1.each do |fragment_spread|
150
- find_conflicts_between_fragments(
151
- fragment_spread2,
152
- fragment_spread,
153
- mutually_exclusive: mutually_exclusive,
154
- )
171
+ sel_idx += 1
155
172
  end
156
- end
157
-
158
- def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:)
159
- fragment_name = fragment_spread.name
160
- return if @visited_fragments.key?(fragment_name)
161
- @visited_fragments[fragment_name] = true
162
173
 
163
- fragment = context.fragments[fragment_name]
164
- return if fragment.nil?
174
+ if deferred_spreads
175
+ visited_fragments ||= {}
176
+ sel_idx = 0
177
+ sel_len = deferred_spreads.size
165
178
 
166
- fragment_type = @types.type(fragment.type.name)
167
- return if fragment_type.nil?
179
+ while sel_idx < sel_len
180
+ sel = deferred_spreads[sel_idx]
181
+ sel_idx += 1
182
+ next if visited_fragments.key?(sel.name)
168
183
 
169
- fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type])
184
+ visited_fragments[sel.name] = true
185
+ frag = @fragments[sel.name]
186
+ next unless frag
170
187
 
171
- # (D) First find any conflicts between the provided collection of fields
172
- # and the collection of fields represented by the given fragment.
173
- find_conflicts_between(
174
- fields,
175
- fragment_fields,
176
- mutually_exclusive: mutually_exclusive,
177
- )
188
+ frag_type = @types.type(frag.type.name)
189
+ next unless frag_type
178
190
 
179
- # (E) Then collect any conflicts between the provided collection of fields
180
- # and any fragment names found in the given fragment.
181
- fragment_spreads.each do |fragment_spread|
182
- find_conflicts_between_fields_and_fragment(
183
- fragment_spread,
184
- fields,
185
- mutually_exclusive: mutually_exclusive,
186
- )
191
+ new_parents = parents.dup
192
+ new_parents << frag_type
193
+ collect_fields_inner(frag.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
194
+ end
187
195
  end
188
196
  end
189
197
 
190
198
  def find_conflicts_within(response_keys)
191
199
  response_keys.each do |key, fields|
192
- next if fields.size < 2
193
- # find conflicts within nodes
194
- i = 0
195
- while i < fields.size
196
- j = i + 1
197
- while j < fields.size
198
- find_conflict(key, fields[i], fields[j])
199
- j += 1
200
+ next unless fields.is_a?(Array)
201
+
202
+ # Optimization: group fields by signature (name + definition + arguments).
203
+ # Fields with the same signature can only conflict on sub-selections,
204
+ # so we only need to compare one pair within each group.
205
+ if fields.size > 4
206
+ f0 = fields[0]
207
+ all_same = true
208
+ i = 1
209
+ while i < fields.size
210
+ unless fields_same_signature?(f0, fields[i])
211
+ all_same = false
212
+ break
213
+ end
214
+ i += 1
215
+ end
216
+
217
+ if all_same
218
+ # All fields share a signature, so they can only conflict on
219
+ # sub-selections. Deduplicate by AST node identity — fields from
220
+ # the same node always have identical sub-selections.
221
+ unique_nodes = fields.uniq { |f| f.node.object_id }
222
+ i = 0
223
+ while i < unique_nodes.size
224
+ j = i + 1
225
+ while j < unique_nodes.size
226
+ if unique_nodes[i].node.selections.size > 0 || unique_nodes[j].node.selections.size > 0
227
+ find_conflict(key, unique_nodes[i], unique_nodes[j])
228
+ end
229
+ j += 1
230
+ end
231
+ i += 1
232
+ end
233
+ else
234
+ groups = fields.group_by { |f| field_signature(f) }
235
+ unique_groups = groups.values
236
+
237
+ # Compare representatives across different groups
238
+ gi = 0
239
+ while gi < unique_groups.size
240
+ gj = gi + 1
241
+ while gj < unique_groups.size
242
+ find_conflict(key, unique_groups[gi][0], unique_groups[gj][0])
243
+ gj += 1
244
+ end
245
+
246
+ # Within same group, deduplicate by AST node and compare all
247
+ # pairs for sub-selection conflicts
248
+ group = unique_groups[gi]
249
+ if group.size >= 2
250
+ unique_in_group = group.uniq { |f| f.node.object_id }
251
+ ui = 0
252
+ while ui < unique_in_group.size
253
+ uj = ui + 1
254
+ while uj < unique_in_group.size
255
+ if unique_in_group[ui].node.selections.size > 0 || unique_in_group[uj].node.selections.size > 0
256
+ find_conflict(key, unique_in_group[ui], unique_in_group[uj])
257
+ end
258
+ uj += 1
259
+ end
260
+ ui += 1
261
+ end
262
+ end
263
+
264
+ gi += 1
265
+ end
266
+ end
267
+ else
268
+ # Small number of fields — original O(n²) is fine
269
+ i = 0
270
+ while i < fields.size
271
+ j = i + 1
272
+ while j < fields.size
273
+ find_conflict(key, fields[i], fields[j])
274
+ j += 1
275
+ end
276
+ i += 1
200
277
  end
201
- i += 1
202
278
  end
203
279
  end
204
280
  end
205
281
 
282
+ def fields_same_signature?(f1, f2)
283
+ n1 = f1.node
284
+ n2 = f2.node
285
+
286
+ f1.definition.equal?(f2.definition) &&
287
+ n1.name == n2.name &&
288
+ same_arguments?(n1, n2)
289
+ end
290
+
291
+ def field_signature(field)
292
+ node = field.node
293
+ defn = field.definition
294
+ args = node.arguments
295
+
296
+ if args.empty?
297
+ [node.name, defn.object_id]
298
+ else
299
+ [node.name, defn.object_id, args.map { |a| [a.name, serialize_arg(a.value)] }]
300
+ end
301
+ end
302
+
206
303
  def find_conflict(response_key, field1, field2, mutually_exclusive: false)
207
- return if @conflict_count >= context.max_errors
304
+ return if @conflict_count >= @max_errors
208
305
  return if field1.definition.nil? || field2.definition.nil?
209
306
 
210
307
  node1 = field1.node
211
308
  node2 = field2.node
212
309
 
213
- are_mutually_exclusive = mutually_exclusive ||
214
- mutually_exclusive?(field1.parents, field2.parents)
310
+ are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents)
215
311
 
216
312
  if !are_mutually_exclusive
217
313
  if node1.name != node2.name
@@ -234,17 +330,20 @@ module GraphQL
234
330
  end
235
331
 
236
332
  if !conflicts[:field].key?(response_key) &&
237
- (t1 = field1.definition&.type) &&
238
- (t2 = field2.definition&.type) &&
333
+ !field1.definition.equal?(field2.definition) &&
334
+ (t1 = field1.return_type) &&
335
+ (t2 = field2.return_type) &&
239
336
  return_types_conflict?(t1, t2)
240
337
 
241
338
  return_error = nil
242
339
  message_override = nil
340
+
243
341
  case @schema.allow_legacy_invalid_return_type_conflicts
244
342
  when false
245
343
  return_error = true
246
344
  when true
247
345
  legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
346
+
248
347
  case legacy_handling
249
348
  when nil
250
349
  return_error = false
@@ -267,9 +366,11 @@ module GraphQL
267
366
 
268
367
  if return_error
269
368
  conflict = conflicts[:return_type][response_key]
369
+
270
370
  if message_override
271
371
  conflict.message = message_override
272
372
  end
373
+
273
374
  conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
274
375
  conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
275
376
  @conflict_count += 1
@@ -303,121 +404,78 @@ module GraphQL
303
404
  elsif type1.kind.leaf? && type2.kind.leaf?
304
405
  type1 != type2
305
406
  else
306
- # One or more of these are composite types,
307
- # their selections will be validated later on.
308
407
  false
309
408
  end
310
409
  end
311
410
 
411
+ # When two fields with the same response key both have sub-selections,
412
+ # we need to check those sub-selections against each other.
312
413
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
313
414
  return if field1.definition.nil? ||
314
415
  field2.definition.nil? ||
315
416
  (field1.node.selections.empty? && field2.node.selections.empty?)
316
417
 
317
- return_type1 = field1.definition.type.unwrap
318
- return_type2 = field2.definition.type.unwrap
319
- parents1 = [return_type1]
320
- parents2 = [return_type2]
418
+ node1 = field1.node
419
+ node2 = field2.node
321
420
 
322
- fields, fragment_spreads = fields_and_fragments_from_selection(
323
- field1.node,
324
- owner_type: return_type1,
325
- parents: parents1
326
- )
421
+ # Prevent infinite recursion from cyclic fragments
422
+ return if node1.equal?(node2)
327
423
 
328
- fields2, fragment_spreads2 = fields_and_fragments_from_selection(
329
- field2.node,
330
- owner_type: return_type2,
331
- parents: parents2
332
- )
333
-
334
- # (H) First, collect all conflicts between these two collections of field.
335
- find_conflicts_between(fields, fields2, mutually_exclusive: mutually_exclusive)
336
-
337
- # (I) Then collect conflicts between the first collection of fields and
338
- # those referenced by each fragment name associated with the second.
339
- fragment_spreads2.each do |fragment_spread|
340
- find_conflicts_between_fields_and_fragment(
341
- fragment_spread,
342
- fields,
343
- mutually_exclusive: mutually_exclusive,
344
- )
424
+ inner = @compared_sub_selections[node1]
425
+ if inner
426
+ return if inner.key?(node2)
427
+ inner[node2] = true
428
+ else
429
+ inner = {}.compare_by_identity
430
+ inner[node2] = true
431
+ @compared_sub_selections[node1] = inner
345
432
  end
346
433
 
347
- # (I) Then collect conflicts between the second collection of fields and
348
- # those referenced by each fragment name associated with the first.
349
- fragment_spreads.each do |fragment_spread|
350
- find_conflicts_between_fields_and_fragment(
351
- fragment_spread,
352
- fields2,
353
- mutually_exclusive: mutually_exclusive,
354
- )
355
- end
434
+ return_type1 = field1.unwrapped_return_type
435
+ return_type2 = field2.unwrapped_return_type
356
436
 
357
- # (J) Also collect conflicts between any fragment names by the first and
358
- # fragment names by the second. This compares each item in the first set of
359
- # names to each item in the second set of names.
360
- fragment_spreads.each do |frag1|
361
- fragment_spreads2.each do |frag2|
362
- find_conflicts_between_fragments(
363
- frag1,
364
- frag2,
365
- mutually_exclusive: mutually_exclusive
366
- )
367
- end
368
- end
369
- end
437
+ response_keys1 = cached_sub_fields(node1, return_type1)
438
+ response_keys2 = cached_sub_fields(node2, return_type2)
370
439
 
371
- def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
372
- response_keys.each do |key, fields|
373
- fields2 = response_keys2[key]
374
- if fields2
375
- fields.each do |field|
376
- fields2.each do |field2|
377
- find_conflict(
378
- key,
379
- field,
380
- field2,
381
- mutually_exclusive: mutually_exclusive,
382
- )
383
- end
384
- end
385
- end
386
- end
440
+ find_conflicts_between(response_keys1, response_keys2, mutually_exclusive: mutually_exclusive)
387
441
  end
388
442
 
389
- NO_SELECTIONS = [GraphQL::EmptyObjects::EMPTY_HASH, GraphQL::EmptyObjects::EMPTY_ARRAY].freeze
443
+ def cached_sub_fields(node, return_type)
444
+ inner = @sub_fields_cache[node]
390
445
 
391
- def fields_and_fragments_from_selection(node, owner_type:, parents:)
392
- if node.selections.empty?
393
- NO_SELECTIONS
446
+ if inner && inner.key?(return_type)
447
+ inner[return_type]
394
448
  else
395
- parents ||= []
396
- fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: [])
397
- response_keys = fields.group_by { |f| f.node.alias || f.node.name }
398
- [response_keys, fragment_spreads]
449
+ result = collect_fields(node.selections, owner_type: return_type, parents: [return_type])
450
+ inner ||= {}.compare_by_identity
451
+ inner[return_type] = result
452
+ @sub_fields_cache[node] = inner
453
+ result
399
454
  end
400
455
  end
401
456
 
402
- def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragment_spreads:)
403
- selections.each do |node|
404
- case node
405
- when GraphQL::Language::Nodes::Field
406
- definition = @types.field(owner_type, node.name)
407
- fields << Field.new(node, definition, owner_type, parents)
408
- when GraphQL::Language::Nodes::InlineFragment
409
- fragment_type = node.type ? @types.type(node.type.name) : owner_type
410
- find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
411
- when GraphQL::Language::Nodes::FragmentSpread
412
- fragment_spreads << FragmentSpread.new(node.name, parents)
457
+ def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
458
+ response_keys.each do |key, fields|
459
+ fields2 = response_keys2[key]
460
+ next unless fields2
461
+
462
+ fields_arr = fields.is_a?(Field) ? [fields] : fields
463
+ fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
464
+
465
+ fields_arr.each do |field|
466
+ fields2_arr.each do |field2|
467
+ find_conflict(
468
+ key,
469
+ field,
470
+ field2,
471
+ mutually_exclusive: mutually_exclusive,
472
+ )
473
+ end
413
474
  end
414
475
  end
415
-
416
- [fields, fragment_spreads]
417
476
  end
418
477
 
419
478
  def same_arguments?(field1, field2)
420
- # Check for incompatible / non-identical arguments on this node:
421
479
  arguments1 = field1.arguments
422
480
  arguments2 = field2.arguments
423
481
 
@@ -450,39 +508,47 @@ module GraphQL
450
508
  serialized_args
451
509
  end
452
510
 
453
- def compared_fragments_key(frag1, frag2, exclusive)
454
- # Cache key to not compare two fragments more than once.
455
- # The key includes both fragment names sorted (this way we
456
- # avoid computing "A vs B" and "B vs A"). It also includes
457
- # "exclusive" since the result may change depending on the parent_type
458
- "#{[frag1, frag2].sort.join('-')}-#{exclusive}"
459
- end
460
-
461
511
  # Given two list of parents, find out if they are mutually exclusive
462
- # In this context, `parents` represents the "self scope" of the field,
463
- # what types may be found at this point in the query.
464
512
  def mutually_exclusive?(parents1, parents2)
465
513
  if parents1.empty? || parents2.empty?
466
514
  false
467
515
  elsif parents1.length == parents2.length
468
- parents1.length.times.any? do |i|
516
+ i = 0
517
+ len = parents1.length
518
+
519
+ while i < len
469
520
  type1 = parents1[i - 1]
470
521
  type2 = parents2[i - 1]
471
- if type1 == type2
472
- # If the types we're comparing are the same type,
473
- # then they aren't mutually exclusive
474
- false
475
- else
476
- # Check if these two scopes have _any_ types in common.
477
- possible_right_types = context.types.possible_types(type1)
478
- possible_left_types = context.types.possible_types(type2)
479
- (possible_right_types & possible_left_types).empty?
522
+ unless type1.equal?(type2)
523
+ inner = @mutually_exclusive_cache[type1]
524
+ if inner
525
+ cached = inner[type2]
526
+ if cached.nil?
527
+ cached = types_mutually_exclusive?(type1, type2)
528
+ inner[type2] = cached
529
+ end
530
+ else
531
+ cached = types_mutually_exclusive?(type1, type2)
532
+ inner = {}.compare_by_identity
533
+ inner[type2] = cached
534
+ @mutually_exclusive_cache[type1] = inner
535
+ end
536
+ return true if cached
480
537
  end
538
+ i += 1
481
539
  end
540
+
541
+ false
482
542
  else
483
543
  true
484
544
  end
485
545
  end
546
+
547
+ def types_mutually_exclusive?(type1, type2)
548
+ possible_right_types = @types.possible_types(type1)
549
+ possible_left_types = @types.possible_types(type2)
550
+ (possible_right_types & possible_left_types).empty?
551
+ end
486
552
  end
487
553
  end
488
554
  end