graphql 2.0.31 → 2.6.1

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 (316) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/install/mutation_root_generator.rb +2 -2
  4. data/lib/generators/graphql/install/templates/base_mutation.erb +2 -0
  5. data/lib/generators/graphql/install/templates/mutation_type.erb +2 -0
  6. data/lib/generators/graphql/install_generator.rb +49 -0
  7. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  8. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  9. data/lib/generators/graphql/templates/base_connection.erb +2 -0
  10. data/lib/generators/graphql/templates/base_edge.erb +2 -0
  11. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  12. data/lib/generators/graphql/templates/base_field.erb +2 -0
  13. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  14. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  15. data/lib/generators/graphql/templates/base_object.erb +2 -0
  16. data/lib/generators/graphql/templates/base_resolver.erb +8 -0
  17. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  18. data/lib/generators/graphql/templates/base_union.erb +2 -0
  19. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  20. data/lib/generators/graphql/templates/graphql_controller.erb +2 -0
  21. data/lib/generators/graphql/templates/loader.erb +2 -0
  22. data/lib/generators/graphql/templates/mutation.erb +2 -0
  23. data/lib/generators/graphql/templates/node_type.erb +2 -0
  24. data/lib/generators/graphql/templates/query_type.erb +2 -0
  25. data/lib/generators/graphql/templates/schema.erb +5 -0
  26. data/lib/generators/graphql/type_generator.rb +1 -1
  27. data/lib/graphql/analysis/analyzer.rb +90 -0
  28. data/lib/graphql/analysis/field_usage.rb +82 -0
  29. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  30. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  31. data/lib/graphql/analysis/query_complexity.rb +263 -0
  32. data/lib/graphql/analysis/query_depth.rb +58 -0
  33. data/lib/graphql/analysis/visitor.rb +280 -0
  34. data/lib/graphql/analysis.rb +102 -1
  35. data/lib/graphql/autoload.rb +38 -0
  36. data/lib/graphql/backtrace/table.rb +118 -55
  37. data/lib/graphql/backtrace.rb +1 -19
  38. data/lib/graphql/coercion_error.rb +1 -9
  39. data/lib/graphql/current.rb +57 -0
  40. data/lib/graphql/dashboard/application_controller.rb +41 -0
  41. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  42. data/lib/graphql/dashboard/installable.rb +22 -0
  43. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  44. data/lib/graphql/dashboard/limiters.rb +93 -0
  45. data/lib/graphql/dashboard/operation_store.rb +199 -0
  46. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  47. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  48. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  49. data/lib/graphql/dashboard/statics/dashboard.css +30 -0
  50. data/lib/graphql/dashboard/statics/dashboard.js +143 -0
  51. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  52. data/lib/graphql/dashboard/statics/icon.png +0 -0
  53. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  54. data/lib/graphql/dashboard/subscriptions.rb +97 -0
  55. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  56. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  57. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  58. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  59. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +24 -0
  60. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  61. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  62. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  63. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  64. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  65. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  66. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  67. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  68. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  69. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  70. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
  71. data/lib/graphql/dashboard.rb +96 -0
  72. data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
  73. data/lib/graphql/dataloader/active_record_source.rb +47 -0
  74. data/lib/graphql/dataloader/async_dataloader.rb +112 -0
  75. data/lib/graphql/dataloader/null_dataloader.rb +55 -10
  76. data/lib/graphql/dataloader/request.rb +5 -0
  77. data/lib/graphql/dataloader/source.rb +35 -12
  78. data/lib/graphql/dataloader.rb +224 -149
  79. data/lib/graphql/date_encoding_error.rb +1 -1
  80. data/lib/graphql/dig.rb +2 -1
  81. data/lib/graphql/duration_encoding_error.rb +16 -0
  82. data/lib/graphql/execution/field_resolve_step.rb +631 -0
  83. data/lib/graphql/execution/finalize.rb +217 -0
  84. data/lib/graphql/execution/input_values.rb +261 -0
  85. data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
  86. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  87. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  88. data/lib/graphql/execution/interpreter/resolve.rb +23 -25
  89. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +228 -0
  90. data/lib/graphql/execution/interpreter/runtime.rb +365 -435
  91. data/lib/graphql/execution/interpreter.rb +87 -163
  92. data/lib/graphql/execution/lazy.rb +1 -1
  93. data/lib/graphql/execution/load_argument_step.rb +64 -0
  94. data/lib/graphql/execution/lookahead.rb +105 -31
  95. data/lib/graphql/execution/multiplex.rb +7 -6
  96. data/lib/graphql/execution/next.rb +90 -0
  97. data/lib/graphql/execution/prepare_object_step.rb +128 -0
  98. data/lib/graphql/execution/runner.rb +410 -0
  99. data/lib/graphql/execution/selections_step.rb +91 -0
  100. data/lib/graphql/execution.rb +8 -4
  101. data/lib/graphql/execution_error.rb +17 -10
  102. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  103. data/lib/graphql/introspection/directive_type.rb +7 -3
  104. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  105. data/lib/graphql/introspection/entry_points.rb +20 -6
  106. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  107. data/lib/graphql/introspection/field_type.rb +13 -5
  108. data/lib/graphql/introspection/input_value_type.rb +21 -13
  109. data/lib/graphql/introspection/schema_type.rb +8 -11
  110. data/lib/graphql/introspection/type_type.rb +64 -28
  111. data/lib/graphql/invalid_name_error.rb +1 -1
  112. data/lib/graphql/invalid_null_error.rb +26 -17
  113. data/lib/graphql/language/block_string.rb +34 -18
  114. data/lib/graphql/language/cache.rb +13 -0
  115. data/lib/graphql/language/comment.rb +18 -0
  116. data/lib/graphql/language/definition_slice.rb +1 -1
  117. data/lib/graphql/language/document_from_schema_definition.rb +90 -61
  118. data/lib/graphql/language/lexer.rb +323 -193
  119. data/lib/graphql/language/nodes.rb +139 -77
  120. data/lib/graphql/language/parser.rb +807 -1985
  121. data/lib/graphql/language/printer.rb +324 -151
  122. data/lib/graphql/language/sanitized_printer.rb +21 -23
  123. data/lib/graphql/language/static_visitor.rb +171 -0
  124. data/lib/graphql/language/visitor.rb +62 -119
  125. data/lib/graphql/language.rb +71 -1
  126. data/lib/graphql/load_application_object_failed_error.rb +5 -1
  127. data/lib/graphql/pagination/array_connection.rb +6 -6
  128. data/lib/graphql/pagination/connection.rb +30 -1
  129. data/lib/graphql/pagination/connections.rb +32 -0
  130. data/lib/graphql/pagination/mongoid_relation_connection.rb +1 -2
  131. data/lib/graphql/query/context/scoped_context.rb +101 -0
  132. data/lib/graphql/query/context.rb +88 -144
  133. data/lib/graphql/query/null_context.rb +15 -18
  134. data/lib/graphql/query/partial.rb +194 -0
  135. data/lib/graphql/query/validation_pipeline.rb +4 -4
  136. data/lib/graphql/query/variable_validation_error.rb +1 -1
  137. data/lib/graphql/query/variables.rb +3 -3
  138. data/lib/graphql/query.rb +135 -81
  139. data/lib/graphql/railtie.rb +16 -6
  140. data/lib/graphql/rake_task.rb +3 -12
  141. data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
  142. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  143. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  144. data/lib/graphql/rubocop.rb +2 -0
  145. data/lib/graphql/runtime_error.rb +6 -0
  146. data/lib/graphql/schema/addition.rb +26 -13
  147. data/lib/graphql/schema/always_visible.rb +7 -2
  148. data/lib/graphql/schema/argument.rb +78 -14
  149. data/lib/graphql/schema/base_64_encoder.rb +3 -5
  150. data/lib/graphql/schema/build_from_definition.rb +140 -66
  151. data/lib/graphql/schema/directive/flagged.rb +4 -2
  152. data/lib/graphql/schema/directive/one_of.rb +12 -0
  153. data/lib/graphql/schema/directive/specified_by.rb +14 -0
  154. data/lib/graphql/schema/directive.rb +78 -12
  155. data/lib/graphql/schema/enum.rb +110 -27
  156. data/lib/graphql/schema/enum_value.rb +11 -3
  157. data/lib/graphql/schema/field/connection_extension.rb +4 -51
  158. data/lib/graphql/schema/field/scope_extension.rb +19 -7
  159. data/lib/graphql/schema/field.rb +245 -119
  160. data/lib/graphql/schema/field_extension.rb +12 -9
  161. data/lib/graphql/schema/has_single_input_argument.rb +160 -0
  162. data/lib/graphql/schema/input_object.rb +123 -65
  163. data/lib/graphql/schema/interface.rb +60 -16
  164. data/lib/graphql/schema/introspection_system.rb +8 -17
  165. data/lib/graphql/schema/late_bound_type.rb +4 -0
  166. data/lib/graphql/schema/list.rb +8 -4
  167. data/lib/graphql/schema/loader.rb +3 -4
  168. data/lib/graphql/schema/member/base_dsl_methods.rb +18 -12
  169. data/lib/graphql/schema/member/has_arguments.rb +132 -100
  170. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  171. data/lib/graphql/schema/member/has_dataloader.rb +99 -0
  172. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  173. data/lib/graphql/schema/member/has_directives.rb +5 -5
  174. data/lib/graphql/schema/member/has_fields.rb +121 -17
  175. data/lib/graphql/schema/member/has_interfaces.rb +27 -13
  176. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  177. data/lib/graphql/schema/member/has_validators.rb +1 -1
  178. data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
  179. data/lib/graphql/schema/member/scoped.rb +19 -0
  180. data/lib/graphql/schema/member/type_system_helpers.rb +18 -5
  181. data/lib/graphql/schema/member/validates_input.rb +3 -3
  182. data/lib/graphql/schema/member.rb +6 -0
  183. data/lib/graphql/schema/mutation.rb +7 -0
  184. data/lib/graphql/schema/non_null.rb +1 -1
  185. data/lib/graphql/schema/object.rb +34 -8
  186. data/lib/graphql/schema/printer.rb +9 -7
  187. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  188. data/lib/graphql/schema/relay_classic_mutation.rb +6 -129
  189. data/lib/graphql/schema/resolver.rb +128 -32
  190. data/lib/graphql/schema/scalar.rb +4 -9
  191. data/lib/graphql/schema/subscription.rb +63 -12
  192. data/lib/graphql/schema/timeout.rb +19 -2
  193. data/lib/graphql/schema/type_expression.rb +2 -2
  194. data/lib/graphql/schema/union.rb +2 -2
  195. data/lib/graphql/schema/unique_within_type.rb +1 -1
  196. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  197. data/lib/graphql/schema/validator/required_validator.rb +92 -11
  198. data/lib/graphql/schema/validator.rb +3 -1
  199. data/lib/graphql/schema/visibility/migration.rb +188 -0
  200. data/lib/graphql/schema/visibility/profile.rb +464 -0
  201. data/lib/graphql/schema/visibility/visit.rb +190 -0
  202. data/lib/graphql/schema/visibility.rb +311 -0
  203. data/lib/graphql/schema/warden.rb +275 -103
  204. data/lib/graphql/schema/wrapper.rb +7 -1
  205. data/lib/graphql/schema.rb +954 -212
  206. data/lib/graphql/static_validation/all_rules.rb +3 -3
  207. data/lib/graphql/static_validation/base_visitor.rb +96 -71
  208. data/lib/graphql/static_validation/literal_validator.rb +6 -7
  209. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +2 -2
  210. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  211. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +6 -2
  212. data/lib/graphql/static_validation/rules/directives_are_defined.rb +6 -3
  213. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  214. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +14 -3
  215. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +59 -15
  216. data/lib/graphql/static_validation/rules/fields_will_merge.rb +391 -262
  217. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  218. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +6 -6
  219. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +15 -2
  220. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  221. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  222. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  223. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  224. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  225. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +28 -8
  226. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +5 -5
  227. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  228. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +7 -3
  229. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  230. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  231. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  232. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +14 -2
  233. data/lib/graphql/static_validation/validation_context.rb +22 -6
  234. data/lib/graphql/static_validation/validator.rb +9 -1
  235. data/lib/graphql/static_validation.rb +0 -1
  236. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +8 -5
  237. data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
  238. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +45 -19
  239. data/lib/graphql/subscriptions/event.rb +22 -4
  240. data/lib/graphql/subscriptions/serialize.rb +3 -1
  241. data/lib/graphql/subscriptions.rb +56 -17
  242. data/lib/graphql/testing/helpers.rb +161 -0
  243. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  244. data/lib/graphql/testing.rb +3 -0
  245. data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
  246. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  247. data/lib/graphql/tracing/appoptics_trace.rb +11 -3
  248. data/lib/graphql/tracing/appoptics_tracing.rb +9 -2
  249. data/lib/graphql/tracing/appsignal_trace.rb +32 -55
  250. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  251. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  252. data/lib/graphql/tracing/data_dog_trace.rb +46 -158
  253. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  254. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  255. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  256. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  257. data/lib/graphql/tracing/detailed_trace.rb +156 -0
  258. data/lib/graphql/tracing/legacy_hooks_trace.rb +75 -0
  259. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  260. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  261. data/lib/graphql/tracing/new_relic_trace.rb +47 -54
  262. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  263. data/lib/graphql/tracing/notifications_trace.rb +184 -34
  264. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  265. data/lib/graphql/tracing/null_trace.rb +9 -0
  266. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  267. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  268. data/lib/graphql/tracing/perfetto_trace.rb +864 -0
  269. data/lib/graphql/tracing/platform_trace.rb +5 -0
  270. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  271. data/lib/graphql/tracing/{prometheus_tracing → prometheus_trace}/graphql_collector.rb +5 -1
  272. data/lib/graphql/tracing/prometheus_trace.rb +72 -68
  273. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  274. data/lib/graphql/tracing/scout_trace.rb +32 -55
  275. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  276. data/lib/graphql/tracing/sentry_trace.rb +82 -0
  277. data/lib/graphql/tracing/statsd_trace.rb +33 -41
  278. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  279. data/lib/graphql/tracing/trace.rb +118 -1
  280. data/lib/graphql/tracing.rb +31 -28
  281. data/lib/graphql/type_kinds.rb +2 -1
  282. data/lib/graphql/types/iso_8601_duration.rb +77 -0
  283. data/lib/graphql/types/relay/connection_behaviors.rb +45 -3
  284. data/lib/graphql/types/relay/edge_behaviors.rb +19 -1
  285. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  286. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  287. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  288. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  289. data/lib/graphql/types.rb +18 -10
  290. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  291. data/lib/graphql/unauthorized_error.rb +9 -1
  292. data/lib/graphql/version.rb +1 -1
  293. data/lib/graphql.rb +69 -54
  294. data/readme.md +12 -2
  295. metadata +236 -40
  296. data/lib/graphql/analysis/ast/analyzer.rb +0 -84
  297. data/lib/graphql/analysis/ast/field_usage.rb +0 -57
  298. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  299. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  300. data/lib/graphql/analysis/ast/query_complexity.rb +0 -230
  301. data/lib/graphql/analysis/ast/query_depth.rb +0 -55
  302. data/lib/graphql/analysis/ast/visitor.rb +0 -276
  303. data/lib/graphql/analysis/ast.rb +0 -81
  304. data/lib/graphql/backtrace/inspect_result.rb +0 -50
  305. data/lib/graphql/backtrace/trace.rb +0 -96
  306. data/lib/graphql/backtrace/tracer.rb +0 -80
  307. data/lib/graphql/deprecation.rb +0 -9
  308. data/lib/graphql/filter.rb +0 -59
  309. data/lib/graphql/language/parser.y +0 -560
  310. data/lib/graphql/language/token.rb +0 -34
  311. data/lib/graphql/schema/base_64_bp.rb +0 -26
  312. data/lib/graphql/schema/invalid_type_error.rb +0 -7
  313. data/lib/graphql/schema/null_mask.rb +0 -11
  314. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
  315. data/lib/graphql/static_validation/type_stack.rb +0 -216
  316. data/lib/graphql/subscriptions/instrumentation.rb +0 -28
@@ -8,220 +8,310 @@ 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
 
36
- def field_conflicts
37
- @field_conflicts ||= Hash.new do |errors, field|
38
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
39
- end
40
- end
41
-
42
- def arg_conflicts
43
- @arg_conflicts ||= Hash.new do |errors, field|
44
- errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
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
45
86
  end
46
- end
47
87
 
48
- def setting_errors
49
- @field_conflicts = nil
50
- @arg_conflicts = nil
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
51
101
 
52
- yield
53
- # don't initialize these if they weren't initialized in the block:
54
- @field_conflicts && @field_conflicts.each_value { |error| add_error(error) }
55
- @arg_conflicts && @arg_conflicts.each_value { |error| add_error(error) }
102
+ false
103
+ else
104
+ true # Assume potential conflicts for larger sets
105
+ end
56
106
  end
57
107
 
58
- def conflicts_within_selection_set(node, parent_type)
59
- return if parent_type.nil?
60
-
61
- fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: nil)
62
-
63
- # (A) Find find all conflicts "within" the fields of this selection set.
64
- find_conflicts_within(fields)
65
-
66
- fragment_spreads.each_with_index do |fragment_spread, i|
67
- are_mutually_exclusive = mutually_exclusive?(
68
- fragment_spread.parents,
69
- [parent_type]
70
- )
71
-
72
- # (B) Then find conflicts between these fields and those represented by
73
- # each spread fragment name found.
74
- find_conflicts_between_fields_and_fragment(
75
- fragment_spread,
76
- fields,
77
- mutually_exclusive: are_mutually_exclusive,
78
- )
79
-
80
- # (C) Then compare this fragment with all other fragments found in this
81
- # selection set to collect conflicts between fragments spread together.
82
- # This compares each item in the list of fragment names to every other
83
- # item in that same list (except for itself).
84
- fragment_spreads[i + 1..-1].each do |fragment_spread2|
85
- are_mutually_exclusive = mutually_exclusive?(
86
- fragment_spread.parents,
87
- fragment_spread2.parents
88
- )
89
-
90
- find_conflicts_between_fragments(
91
- fragment_spread,
92
- fragment_spread2,
93
- mutually_exclusive: are_mutually_exclusive,
94
- )
108
+ def conflicts
109
+ @conflicts ||= Hash.new do |h, error_type|
110
+ h[error_type] = Hash.new do |h2, field_name|
111
+ h2[field_name] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: error_type, field_name: field_name)
95
112
  end
96
113
  end
97
114
  end
98
115
 
99
- def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:)
100
- fragment_name1 = fragment_spread1.name
101
- fragment_name2 = fragment_spread2.name
102
- return if fragment_name1 == fragment_name2
103
-
104
- cache_key = compared_fragments_key(
105
- fragment_name1,
106
- fragment_name2,
107
- mutually_exclusive,
108
- )
109
- if @compared_fragments.key?(cache_key)
110
- return
111
- else
112
- @compared_fragments[cache_key] = true
113
- end
116
+ # Core algorithm: collect ALL fields (expanding fragments inline) into a flat
117
+ # map keyed by response key, then compare within each group.
118
+ def conflicts_within_selection_set(node, parent_type)
119
+ return if parent_type.nil?
120
+ return if node.selections.empty?
114
121
 
115
- fragment1 = context.fragments[fragment_name1]
116
- fragment2 = context.fragments[fragment_name2]
122
+ # Collect all fields from this selection set, expanding fragments transitively
123
+ response_keys = collect_fields(node.selections, owner_type: parent_type, parents: [])
117
124
 
118
- return if fragment1.nil? || fragment2.nil?
125
+ # Find conflicts within each response key group
126
+ find_conflicts_within(response_keys)
127
+ end
119
128
 
120
- fragment_type1 = context.warden.get_type(fragment1.type.name)
121
- fragment_type2 = context.warden.get_type(fragment2.type.name)
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
122
136
 
123
- return if fragment_type1.nil? || fragment_type2.nil?
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
124
141
 
125
- fragment_fields1, fragment_spreads1 = fields_and_fragments_from_selection(
126
- fragment1,
127
- owner_type: fragment_type1,
128
- parents: [*fragment_spread1.parents, fragment_type1]
129
- )
130
- fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection(
131
- fragment2,
132
- owner_type: fragment_type1,
133
- parents: [*fragment_spread2.parents, fragment_type2]
134
- )
142
+ while sel_idx < sel_len
143
+ sel = selections[sel_idx]
135
144
 
136
- # (F) First, find all conflicts between these two collections of fields
137
- # (not including any nested fragments).
138
- find_conflicts_between(
139
- fragment_fields1,
140
- fragment_fields2,
141
- mutually_exclusive: mutually_exclusive,
142
- )
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
143
161
 
144
- # (G) Then collect conflicts between the first fragment and any nested
145
- # fragments spread in the second fragment.
146
- fragment_spreads2.each do |fragment_spread|
147
- find_conflicts_between_fragments(
148
- fragment_spread1,
149
- fragment_spread,
150
- mutually_exclusive: mutually_exclusive,
151
- )
152
- 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
153
170
 
154
- # (G) Then collect conflicts between the first fragment and any nested
155
- # fragments spread in the second fragment.
156
- fragment_spreads1.each do |fragment_spread|
157
- find_conflicts_between_fragments(
158
- fragment_spread2,
159
- fragment_spread,
160
- mutually_exclusive: mutually_exclusive,
161
- )
171
+ sel_idx += 1
162
172
  end
163
- end
164
173
 
165
- def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:)
166
- fragment_name = fragment_spread.name
167
- return if @visited_fragments.key?(fragment_name)
168
- @visited_fragments[fragment_name] = true
174
+ if deferred_spreads
175
+ visited_fragments ||= {}
176
+ sel_idx = 0
177
+ sel_len = deferred_spreads.size
169
178
 
170
- fragment = context.fragments[fragment_name]
171
- return if fragment.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)
172
183
 
173
- fragment_type = context.warden.get_type(fragment.type.name)
174
- return if fragment_type.nil?
184
+ visited_fragments[sel.name] = true
185
+ frag = @fragments[sel.name]
186
+ next unless frag
175
187
 
176
- fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type])
177
-
178
- # (D) First find any conflicts between the provided collection of fields
179
- # and the collection of fields represented by the given fragment.
180
- find_conflicts_between(
181
- fields,
182
- fragment_fields,
183
- mutually_exclusive: mutually_exclusive,
184
- )
188
+ frag_type = @types.type(frag.type.name)
189
+ next unless frag_type
185
190
 
186
- # (E) Then collect any conflicts between the provided collection of fields
187
- # and any fragment names found in the given fragment.
188
- fragment_spreads.each do |fragment_spread|
189
- find_conflicts_between_fields_and_fragment(
190
- fragment_spread,
191
- fields,
192
- mutually_exclusive: mutually_exclusive,
193
- )
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
194
195
  end
195
196
  end
196
197
 
197
198
  def find_conflicts_within(response_keys)
198
199
  response_keys.each do |key, fields|
199
- next if fields.size < 2
200
- # find conflicts within nodes
201
- i = 0
202
- while i < fields.size
203
- j = i + 1
204
- while j < fields.size
205
- find_conflict(key, fields[i], fields[j])
206
- 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
207
277
  end
208
- i += 1
209
278
  end
210
279
  end
211
280
  end
212
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
+
213
303
  def find_conflict(response_key, field1, field2, mutually_exclusive: false)
214
- return if @conflict_count >= context.max_errors
304
+ return if @conflict_count >= @max_errors
305
+ return if field1.definition.nil? || field2.definition.nil?
215
306
 
216
307
  node1 = field1.node
217
308
  node2 = field2.node
218
309
 
219
- are_mutually_exclusive = mutually_exclusive ||
220
- mutually_exclusive?(field1.parents, field2.parents)
310
+ are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents)
221
311
 
222
312
  if !are_mutually_exclusive
223
313
  if node1.name != node2.name
224
- conflict = field_conflicts[response_key]
314
+ conflict = conflicts[:field][response_key]
225
315
 
226
316
  conflict.add_conflict(node1, node1.name)
227
317
  conflict.add_conflict(node2, node2.name)
@@ -230,7 +320,7 @@ module GraphQL
230
320
  end
231
321
 
232
322
  if !same_arguments?(node1, node2)
233
- conflict = arg_conflicts[response_key]
323
+ conflict = conflicts[:argument][response_key]
234
324
 
235
325
  conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
236
326
  conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
@@ -239,6 +329,54 @@ module GraphQL
239
329
  end
240
330
  end
241
331
 
332
+ if !conflicts[:field].key?(response_key) &&
333
+ !field1.definition.equal?(field2.definition) &&
334
+ (t1 = field1.return_type) &&
335
+ (t2 = field2.return_type) &&
336
+ return_types_conflict?(t1, t2)
337
+
338
+ return_error = nil
339
+ message_override = nil
340
+
341
+ case @schema.allow_legacy_invalid_return_type_conflicts
342
+ when false
343
+ return_error = true
344
+ when true
345
+ legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
346
+
347
+ case legacy_handling
348
+ when nil
349
+ return_error = false
350
+ when :return_validation_error
351
+ return_error = true
352
+ when String
353
+ return_error = true
354
+ message_override = legacy_handling
355
+ else
356
+ raise GraphQL::Error, "#{@schema}.legacy_invalid_scalar_conflicts returned unexpected value: #{legacy_handling.inspect}. Expected `nil`, String, or `:return_validation_error`."
357
+ end
358
+ else
359
+ return_error = false
360
+ @context.query.logger.warn <<~WARN
361
+ GraphQL-Ruby encountered mismatched types in this query: `#{t1.to_type_signature}` (at #{node1.line}:#{node1.col}) vs. `#{t2.to_type_signature}` (at #{node2.line}:#{node2.col}).
362
+ This will return an error in future GraphQL-Ruby versions, as per the GraphQL specification
363
+ Learn about migrating here: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_return_type_conflicts-class_method
364
+ WARN
365
+ end
366
+
367
+ if return_error
368
+ conflict = conflicts[:return_type][response_key]
369
+
370
+ if message_override
371
+ conflict.message = message_override
372
+ end
373
+
374
+ conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
375
+ conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
376
+ @conflict_count += 1
377
+ end
378
+ end
379
+
242
380
  find_conflicts_between_sub_selection_sets(
243
381
  field1,
244
382
  field2,
@@ -246,115 +384,98 @@ module GraphQL
246
384
  )
247
385
  end
248
386
 
387
+ def return_types_conflict?(type1, type2)
388
+ if type1.list?
389
+ if type2.list?
390
+ return_types_conflict?(type1.of_type, type2.of_type)
391
+ else
392
+ true
393
+ end
394
+ elsif type2.list?
395
+ true
396
+ elsif type1.non_null?
397
+ if type2.non_null?
398
+ return_types_conflict?(type1.of_type, type2.of_type)
399
+ else
400
+ true
401
+ end
402
+ elsif type2.non_null?
403
+ true
404
+ elsif type1.kind.leaf? && type2.kind.leaf?
405
+ type1 != type2
406
+ else
407
+ false
408
+ end
409
+ end
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.
249
413
  def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
250
414
  return if field1.definition.nil? ||
251
415
  field2.definition.nil? ||
252
416
  (field1.node.selections.empty? && field2.node.selections.empty?)
253
417
 
254
- return_type1 = field1.definition.type.unwrap
255
- return_type2 = field2.definition.type.unwrap
256
- parents1 = [return_type1]
257
- parents2 = [return_type2]
258
-
259
- fields, fragment_spreads = fields_and_fragments_from_selection(
260
- field1.node,
261
- owner_type: return_type1,
262
- parents: parents1
263
- )
418
+ node1 = field1.node
419
+ node2 = field2.node
264
420
 
265
- fields2, fragment_spreads2 = fields_and_fragments_from_selection(
266
- field2.node,
267
- owner_type: return_type2,
268
- parents: parents2
269
- )
421
+ # Prevent infinite recursion from cyclic fragments
422
+ return if node1.equal?(node2)
270
423
 
271
- # (H) First, collect all conflicts between these two collections of field.
272
- find_conflicts_between(fields, fields2, mutually_exclusive: mutually_exclusive)
273
-
274
- # (I) Then collect conflicts between the first collection of fields and
275
- # those referenced by each fragment name associated with the second.
276
- fragment_spreads2.each do |fragment_spread|
277
- find_conflicts_between_fields_and_fragment(
278
- fragment_spread,
279
- fields,
280
- mutually_exclusive: mutually_exclusive,
281
- )
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
282
432
  end
283
433
 
284
- # (I) Then collect conflicts between the second collection of fields and
285
- # those referenced by each fragment name associated with the first.
286
- fragment_spreads.each do |fragment_spread|
287
- find_conflicts_between_fields_and_fragment(
288
- fragment_spread,
289
- fields2,
290
- mutually_exclusive: mutually_exclusive,
291
- )
292
- end
434
+ return_type1 = field1.unwrapped_return_type
435
+ return_type2 = field2.unwrapped_return_type
293
436
 
294
- # (J) Also collect conflicts between any fragment names by the first and
295
- # fragment names by the second. This compares each item in the first set of
296
- # names to each item in the second set of names.
297
- fragment_spreads.each do |frag1|
298
- fragment_spreads2.each do |frag2|
299
- find_conflicts_between_fragments(
300
- frag1,
301
- frag2,
302
- mutually_exclusive: mutually_exclusive
303
- )
304
- end
305
- end
306
- end
437
+ response_keys1 = cached_sub_fields(node1, return_type1)
438
+ response_keys2 = cached_sub_fields(node2, return_type2)
307
439
 
308
- def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
309
- response_keys.each do |key, fields|
310
- fields2 = response_keys2[key]
311
- if fields2
312
- fields.each do |field|
313
- fields2.each do |field2|
314
- find_conflict(
315
- key,
316
- field,
317
- field2,
318
- mutually_exclusive: mutually_exclusive,
319
- )
320
- end
321
- end
322
- end
323
- end
440
+ find_conflicts_between(response_keys1, response_keys2, mutually_exclusive: mutually_exclusive)
324
441
  end
325
442
 
326
- 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]
327
445
 
328
- def fields_and_fragments_from_selection(node, owner_type:, parents:)
329
- if node.selections.empty?
330
- NO_SELECTIONS
446
+ if inner && inner.key?(return_type)
447
+ inner[return_type]
331
448
  else
332
- parents ||= []
333
- fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: [])
334
- response_keys = fields.group_by { |f| f.node.alias || f.node.name }
335
- [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
336
454
  end
337
455
  end
338
456
 
339
- def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragment_spreads:)
340
- selections.each do |node|
341
- case node
342
- when GraphQL::Language::Nodes::Field
343
- definition = context.query.get_field(owner_type, node.name)
344
- fields << Field.new(node, definition, owner_type, parents)
345
- when GraphQL::Language::Nodes::InlineFragment
346
- fragment_type = node.type ? context.warden.get_type(node.type.name) : owner_type
347
- find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
348
- when GraphQL::Language::Nodes::FragmentSpread
349
- 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
350
474
  end
351
475
  end
352
-
353
- [fields, fragment_spreads]
354
476
  end
355
477
 
356
478
  def same_arguments?(field1, field2)
357
- # Check for incompatible / non-identical arguments on this node:
358
479
  arguments1 = field1.arguments
359
480
  arguments2 = field2.arguments
360
481
 
@@ -387,39 +508,47 @@ module GraphQL
387
508
  serialized_args
388
509
  end
389
510
 
390
- def compared_fragments_key(frag1, frag2, exclusive)
391
- # Cache key to not compare two fragments more than once.
392
- # The key includes both fragment names sorted (this way we
393
- # avoid computing "A vs B" and "B vs A"). It also includes
394
- # "exclusive" since the result may change depending on the parent_type
395
- "#{[frag1, frag2].sort.join('-')}-#{exclusive}"
396
- end
397
-
398
511
  # Given two list of parents, find out if they are mutually exclusive
399
- # In this context, `parents` represends the "self scope" of the field,
400
- # what types may be found at this point in the query.
401
512
  def mutually_exclusive?(parents1, parents2)
402
513
  if parents1.empty? || parents2.empty?
403
514
  false
404
515
  elsif parents1.length == parents2.length
405
- parents1.length.times.any? do |i|
516
+ i = 0
517
+ len = parents1.length
518
+
519
+ while i < len
406
520
  type1 = parents1[i - 1]
407
521
  type2 = parents2[i - 1]
408
- if type1 == type2
409
- # If the types we're comparing are the same type,
410
- # then they aren't mutually exclusive
411
- false
412
- else
413
- # Check if these two scopes have _any_ types in common.
414
- possible_right_types = context.query.possible_types(type1)
415
- possible_left_types = context.query.possible_types(type2)
416
- (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
417
537
  end
538
+ i += 1
418
539
  end
540
+
541
+ false
419
542
  else
420
543
  true
421
544
  end
422
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
423
552
  end
424
553
  end
425
554
  end