rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -243,7 +243,8 @@ module Rigor
243
243
  method_name: context.method_name,
244
244
  args: context.args,
245
245
  environment: environment,
246
- scope: context.scope
246
+ scope: context.scope,
247
+ call_node: context.call_node
247
248
  )
248
249
  end
249
250
 
@@ -277,6 +278,11 @@ module Rigor
277
278
 
278
279
  def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
279
280
  public_only: false, scope: nil, call_node: nil)
281
+ # Issue #1794 — a sequence's inherited Enumerable surface reads its Rigor-private element argument, with
282
+ # `self` still the sequence.
283
+ element = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment)
284
+ self_type_override ||= receiver if element
285
+ receiver = element || receiver
280
286
  descriptor = receiver_descriptor(receiver)
281
287
  return nil unless descriptor
282
288
 
@@ -1006,41 +1012,135 @@ module Rigor
1006
1012
  return override if override
1007
1013
 
1008
1014
  instance_type = Type::Combinator.nominal_of(class_name)
1009
- resolved_self_type =
1010
- case kind
1011
- when :singleton then Type::Combinator.singleton_of(class_name)
1012
- else instance_type
1013
- end
1014
1015
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
1015
1016
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
1016
1017
  # fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
1017
1018
  # type-argument-bearing projection through it ({SelfSubstitute}, #1092).
1018
- self_type = self_type_override || resolved_self_type
1019
+ self_type = self_type_override ||
1020
+ (kind == :singleton ? Type::Combinator.singleton_of(class_name) : instance_type)
1021
+
1022
+ keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
1023
+ call_site = [class_name, method_name, kind]
1024
+ if keywords_last && args.last.is_a?(Type::HashShape)
1025
+ distributed = distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args,
1026
+ block_type, scope, call_node, call_site, environment)
1027
+ return distributed unless distributed.equal?(NOT_DISTRIBUTED)
1028
+ end
1019
1029
 
1020
1030
  candidates = OverloadSelector.select_candidates(
1021
1031
  method_definition,
1022
1032
  arg_types: args,
1033
+ keywords_last: keywords_last, splats: splat_indices(call_node, args),
1023
1034
  # A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
1024
1035
  # read the static facet, as they did before the substitute carried the wrapping.
1025
1036
  self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1026
- instance_type: instance_type,
1027
- type_vars: type_vars,
1037
+ instance_type: instance_type, type_vars: type_vars,
1028
1038
  block_required: !block_type.nil?,
1029
1039
  environment: environment
1030
1040
  )
1031
1041
  return nil if candidates.empty?
1032
1042
 
1033
- call_site = [class_name, method_name, kind]
1034
1043
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
1035
1044
  join_candidate_returns(
1036
1045
  candidates,
1037
1046
  method_definition: method_definition,
1038
1047
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1039
1048
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1040
- alias_expander: environment.rbs_loader
1049
+ alias_expander: environment.rbs_loader, keywords_last: keywords_last
1041
1050
  )
1042
1051
  end
1043
1052
 
1053
+ # Issue #1746 — a keyword hash holding a union value (`headers: flag` with `flag : bool`) selects per member
1054
+ # at runtime, so the return reads one overload per {KeywordArguments.distributions} list and joins their
1055
+ # returns, as the block probe does for its parameters (#1737). Selecting once for the whole value took one
1056
+ # member's overload (`Symbol` for `(headers: true) -> Symbol | (?headers: false) -> Integer`). Each list
1057
+ # counts only a genuine match: a member no overload takes, or more lists than the limit, answers
1058
+ # `Dynamic[top]` rather than lend the first-overload fallback to the join. A precise union value joins the
1059
+ # returns as a union; a `Dynamic` value keeps the answer `Dynamic`, as the #521 join does, since the split
1060
+ # dropped the value's `nil` and the untyped input behind it. A keyword hash with no union value answers
1061
+ # {NOT_DISTRIBUTED}, and the call selects as before; so does one whose values do not split (#1779) and that no
1062
+ # overload genuinely takes.
1063
+ NOT_DISTRIBUTED = Object.new.freeze
1064
+ private_constant :NOT_DISTRIBUTED
1065
+
1066
+ # rubocop:disable Metrics/ParameterLists
1067
+ def distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args, block_type,
1068
+ scope, call_node, call_site, environment)
1069
+ # rubocop:enable Metrics/ParameterLists
1070
+ # One overload is the only one any member can select, and a wide union value (`receiver_type: Type::t`)
1071
+ # spelled out member by member would only pass the distribution limit.
1072
+ method_types = method_definition.method_types
1073
+ return NOT_DISTRIBUTED if method_types.size < 2
1074
+ # A keyword hash no overload takes as keywords is a positional `Hash` throughout (#1737).
1075
+ return NOT_DISTRIBUTED unless KeywordArguments.any_declares?(method_types)
1076
+
1077
+ keywords = args.last
1078
+ return NOT_DISTRIBUTED if keywords.pairs.none? { |_name, value| KeywordArguments.union_members(value) }
1079
+
1080
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1081
+ per_list = distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1082
+ !block_type.nil?, environment, splat_indices(call_node, args))
1083
+ return NOT_DISTRIBUTED if per_list.equal?(NOT_DISTRIBUTED)
1084
+ return Type::Combinator.untyped if per_list.nil?
1085
+
1086
+ overloads = per_list.flat_map(&:last).uniq
1087
+ if overloads.size == 1
1088
+ record_dispatch_provenance(method_definition, overloads.first, scope, call_node, call_site)
1089
+ end
1090
+ returns = per_list.map do |arg_types, matches|
1091
+ join_candidate_returns(
1092
+ matches,
1093
+ method_definition: method_definition,
1094
+ self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1095
+ args: arg_types, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1096
+ alias_expander: environment.rbs_loader, keywords_last: true
1097
+ )
1098
+ end
1099
+ join_distributed_returns(returns, keywords)
1100
+ end
1101
+
1102
+ # Each distribution list with its genuine matches, or nil past the distribution limit or when a list has
1103
+ # none. When no value split (no union value's key discriminates between the overloads, #1779), the one list
1104
+ # is the call as given, and one with no genuine match answers {NOT_DISTRIBUTED}: the call then selects as it
1105
+ # did before #1746, first-overload fallback included (`1.step(10, by: (c ? 1 : 2.0))` is
1106
+ # `Enumerator::ArithmeticSequence` through the incomplete stdlib RBS).
1107
+ # rubocop:disable Metrics/ParameterLists
1108
+ def distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1109
+ block_required, environment, splats)
1110
+ # rubocop:enable Metrics/ParameterLists
1111
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1112
+ environment, splats)
1113
+ return nil if distributions.nil?
1114
+
1115
+ distributions.map do |arg_types|
1116
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars,
1117
+ block_required, environment, splats)
1118
+ return arg_types.equal?(args) ? NOT_DISTRIBUTED : nil if matches.empty?
1119
+
1120
+ [arg_types, matches]
1121
+ end
1122
+ end
1123
+
1124
+ def join_distributed_returns(returns, keywords)
1125
+ return nil if returns.any?(&:nil?)
1126
+
1127
+ joined = Type::Combinator.union(*returns.uniq)
1128
+ dynamic_value = keywords.pairs.any? { |_name, value| value.is_a?(Type::Dynamic) }
1129
+ dynamic_value && !joined.is_a?(Type::Dynamic) ? Type::Combinator.dynamic(joined) : joined
1130
+ end
1131
+
1132
+ # Issue #1801 — the indices of `args` the call passes as splat arguments (`f(*xs, k: 1)`), whose positional
1133
+ # count is not known statically, or nil without one. As for {ArgumentNodes.keyword_hash_last?}, only a call
1134
+ # whose node's arguments are exactly `args` answers.
1135
+ def splat_indices(call_node, args)
1136
+ return nil if args.empty? || !call_node.respond_to?(:arguments)
1137
+
1138
+ arguments = call_node.arguments&.arguments
1139
+ return nil if arguments.nil? || arguments.size != args.size || arguments.none?(Prism::SplatNode)
1140
+
1141
+ arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
1142
+ end
1143
+
1044
1144
  # The two provenance side-tables the return-typing tier is the last place able to populate, recorded
1045
1145
  # together because they share one gate (`scope` && `call_node`) and one call site.
1046
1146
  #
@@ -1074,10 +1174,16 @@ module Rigor
1074
1174
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
1075
1175
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
1076
1176
  # rubocop:disable-next Metrics/ParameterLists
1177
+ # rubocop:disable Metrics/ParameterLists
1077
1178
  def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
1078
- block_type:, scope:, call_node:, call_site:, alias_expander: nil)
1179
+ block_type:, scope:, call_node:, call_site:, alias_expander: nil,
1180
+ keywords_last: false)
1181
+ # rubocop:enable Metrics/ParameterLists
1079
1182
  returns = candidates.map do |method_type|
1080
- full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1183
+ # An overload that takes the keyword hash as its keywords binds no positional parameter to it.
1184
+ positional = KeywordArguments.positional(method_type, args, keywords_last)
1185
+ full_type_vars = compose_type_vars(method_type, type_vars, args, positional, block_type, scope,
1186
+ call_node, call_site)
1081
1187
  returned = RbsTypeTranslator.translate(
1082
1188
  method_type.type.return_type,
1083
1189
  self_type: self_type,
@@ -1087,7 +1193,7 @@ module Rigor
1087
1193
  )
1088
1194
  next returned unless combining_overload?(method_definition, method_type, call_site)
1089
1195
 
1090
- class_level_sum(returned, method_type, args)
1196
+ class_level_sum(returned, method_type, positional)
1091
1197
  end
1092
1198
  return returns.first if returns.size == 1
1093
1199
  return nil if returns.any?(&:nil?)
@@ -1098,11 +1204,17 @@ module Rigor
1098
1204
  Type::Combinator.dynamic(Type::Combinator.union(*distinct))
1099
1205
  end
1100
1206
 
1101
- def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1207
+ # `positional` is `args` less a keyword hash the overload takes as its keywords (#1727). The block-return
1208
+ # rule reads every argument, the keyword hash included: a keyword parameter naming the block's variable
1209
+ # (`[T] (?default: T) { () -> T } -> T`) is an argument reaching it. The positional binding reads only the
1210
+ # arguments that land in positional parameters.
1211
+ # rubocop:disable Metrics/ParameterLists
1212
+ def compose_type_vars(method_type, type_vars, args, positional, block_type, scope, call_node, call_site)
1213
+ # rubocop:enable Metrics/ParameterLists
1102
1214
  vars = compose_block_type_vars(method_type, type_vars, block_type, args,
1103
1215
  scope: scope, call_node: call_node, call_site: call_site)
1104
- compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
1105
- call_site: call_site)
1216
+ compose_arg_type_vars(method_type, vars, positional, scope: scope, call_node: call_node,
1217
+ call_site: call_site)
1106
1218
  end
1107
1219
 
1108
1220
  # Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
@@ -1268,7 +1380,8 @@ module Rigor
1268
1380
  # Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
1269
1381
  # inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
1270
1382
  # parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
1271
- # arrive as one more entry in `args`, so any argument counts as reaching every parameter.
1383
+ # arrive as one more entry in `args` (untrimmed, #1727), so any argument counts as reaching every
1384
+ # parameter.
1272
1385
  def argument_reaches_variable?(method_type, name, args)
1273
1386
  return false if args.empty?
1274
1387
 
@@ -1602,11 +1715,15 @@ module Rigor
1602
1715
 
1603
1716
  # ----- block parameter probe (Phase C sub-phase 1) -----
1604
1717
 
1605
- def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
1718
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
1606
1719
  args ||= []
1720
+ keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
1721
+ splat = splat_indices(call_node, args)
1607
1722
  case receiver
1608
- when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
1609
- else probe_block_param_types_one(receiver, method_name, args, environment, scope)
1723
+ when Type::Union
1724
+ probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1725
+ else
1726
+ probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1610
1727
  end
1611
1728
  end
1612
1729
 
@@ -1614,9 +1731,9 @@ module Rigor
1614
1731
  # member resolves the same arity and types (otherwise the call sites would have to thread
1615
1732
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
1616
1733
  # array so the binder defaults all params to Dynamic[Top].
1617
- def probe_block_param_types_union(receiver, method_name, args, environment, scope)
1734
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1618
1735
  results = receiver.members.map do |member|
1619
- probe_block_param_types_one(member, method_name, args, environment, scope)
1736
+ probe_block_param_types_one(member, method_name, args, environment, scope, keywords_last, splat)
1620
1737
  end
1621
1738
  return [] if results.empty?
1622
1739
  return [] unless results.all? { |r| r == results.first }
@@ -1624,7 +1741,9 @@ module Rigor
1624
1741
  results.first
1625
1742
  end
1626
1743
 
1627
- def probe_block_param_types_one(receiver, method_name, args, environment, scope)
1744
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1745
+ receiver = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment, block: true) ||
1746
+ receiver
1628
1747
  descriptor = receiver_descriptor(receiver)
1629
1748
  return [] unless descriptor
1630
1749
 
@@ -1642,7 +1761,8 @@ module Rigor
1642
1761
  environment: environment,
1643
1762
  receiver: receiver,
1644
1763
  receiver_args: receiver_args,
1645
- method_name: method_name
1764
+ method_name: method_name,
1765
+ keywords_last: keywords_last, positional_splat: splat
1646
1766
  )
1647
1767
  rescue StandardError
1648
1768
  []
@@ -1651,7 +1771,7 @@ module Rigor
1651
1771
  # rubocop:disable Metrics/ParameterLists
1652
1772
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
1653
1773
  environment: nil, receiver: nil, receiver_args: [],
1654
- method_name: nil)
1774
+ method_name: nil, keywords_last: false, positional_splat: nil)
1655
1775
  # rubocop:enable Metrics/ParameterLists
1656
1776
  instance_type = Type::Combinator.nominal_of(class_name)
1657
1777
  self_type =
@@ -1672,12 +1792,25 @@ module Rigor
1672
1792
  substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
1673
1793
  self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
1674
1794
 
1795
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1796
+ if keywords_last
1797
+ return agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type,
1798
+ type_vars, environment, positional_splat)
1799
+ end
1800
+
1801
+ # A splat (#1801) leaves the positional count open, so every overload some count reaches must agree.
1802
+ if positional_splat || FacetDistribution.faceted?(args)
1803
+ matches = facet_matches(method_definition, args, selector_self, instance_type, type_vars, true,
1804
+ environment, false, positional_splat)
1805
+ return agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1806
+ end
1807
+
1675
1808
  method_type = OverloadSelector.select(
1676
1809
  method_definition,
1677
1810
  arg_types: args,
1678
1811
  # Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
1679
1812
  # the substitution verdict here is built from the receiver's type arguments alone.
1680
- self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1813
+ self_type: selector_self,
1681
1814
  instance_type: instance_type,
1682
1815
  type_vars: type_vars,
1683
1816
  block_required: true,
@@ -1685,8 +1818,20 @@ module Rigor
1685
1818
  )
1686
1819
  return [] unless method_type
1687
1820
 
1821
+ block_params_of(method_type, self_type, instance_type, type_vars, environment) || []
1822
+ end
1823
+
1824
+ # The block parameters every overload in `matches` declares alike, or no information (`[]`).
1825
+ def agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1826
+ answers = matches.uniq.map do |method_type|
1827
+ block_params_of(method_type, self_type, instance_type, type_vars, environment)
1828
+ end
1829
+ answers.uniq.size == 1 && answers.first ? answers.first : []
1830
+ end
1831
+
1832
+ def block_params_of(method_type, self_type, instance_type, type_vars, environment)
1688
1833
  block = method_type.respond_to?(:block) ? method_type.block : nil
1689
- return [] unless block
1834
+ return nil unless block
1690
1835
 
1691
1836
  translate_block_positional_params(
1692
1837
  block,
@@ -1697,6 +1842,80 @@ module Rigor
1697
1842
  )
1698
1843
  end
1699
1844
 
1845
+ # {KeywordArguments.distributions} for the call's keyword hash. An overload that declares no keywords may read
1846
+ # the hash as a positional `Hash`, and then every union value splits: with
1847
+ # `(a: Integer) -> Integer | (Hash[Symbol, String]) -> String`, `a: Integer | String`'s `String` member takes
1848
+ # the second overload and its `Integer` member the first. A parameter that cannot take a `Hash` (`(String)`)
1849
+ # leaves the split to the keyword declarations.
1850
+ # `splats`: a splat argument hides how many positional arguments precede the hash.
1851
+ def keyword_distributions(method_definition, args, selector_self, instance_type, type_vars, environment,
1852
+ splats)
1853
+ hash = Type::Combinator.nominal_of("Hash")
1854
+ count = splats ? nil : args.size
1855
+ KeywordArguments.distributions(args, true, method_definition.method_types, count) do |param|
1856
+ param_type = RbsTypeTranslator.translate(
1857
+ param.type, self_type: selector_self, instance_type: instance_type, type_vars: type_vars,
1858
+ alias_expander: environment.rbs_loader
1859
+ )
1860
+ !param_type.accepts(hash, mode: :gradual).no?
1861
+ end
1862
+ end
1863
+
1864
+ # rubocop:disable Metrics/ParameterLists
1865
+ def agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type, type_vars,
1866
+ environment, splats)
1867
+ # rubocop:enable Metrics/ParameterLists
1868
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1869
+ environment, splats)
1870
+ return [] if distributions.nil?
1871
+
1872
+ candidates = []
1873
+ distributions.each do |arg_types|
1874
+ # Genuine matches only (`member` true): a member no overload takes must not lend the first-overload
1875
+ # fallback to the agreement, which bound `headers: (true | nil)`'s block to the `true` arm.
1876
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, true,
1877
+ environment, splats)
1878
+ return [] if matches.empty?
1879
+
1880
+ candidates.concat(matches)
1881
+ end
1882
+ agreed_block_params(candidates, self_type, instance_type, type_vars, environment)
1883
+ end
1884
+
1885
+ # One distribution's genuine matches. A faceted positional argument (a `Dynamic` with sealed facet members) is
1886
+ # read member-wise as `OverloadSelector.select_candidates` reads it, but every list answers only a genuine
1887
+ # match: the first-overload fallback must not count toward the block probe's agreement or the return path's
1888
+ # join.
1889
+ # rubocop:disable Metrics/ParameterLists
1890
+ def keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1891
+ environment, splats)
1892
+ # rubocop:enable Metrics/ParameterLists
1893
+ unless FacetDistribution.faceted?(arg_types)
1894
+ return OverloadSelector.select_declared(method_definition, arg_types, selector_self, instance_type,
1895
+ type_vars, block_required, environment, true, true, splats)
1896
+ end
1897
+
1898
+ facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1899
+ environment, true, splats)
1900
+ end
1901
+
1902
+ # Issue #1750 — the overloads a faceted argument list may reach, read member-wise with genuine matches only.
1903
+ # Where `FacetDistribution.select` falls back to the arguments as given (overloads not provable, more lists
1904
+ # than its cap, or a member no overload takes), the bare `Dynamic` gradually matches every arm and the
1905
+ # strict pass's pick is the first by position, so the answer is every gradual match
1906
+ # (`OverloadSelector.select_declared`'s `:gradual`) and the caller binds only what they all agree on.
1907
+ # rubocop:disable Metrics/ParameterLists
1908
+ def facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1909
+ environment, keywords_last, splats = nil)
1910
+ # rubocop:enable Metrics/ParameterLists
1911
+ FacetDistribution.select(arg_types, method_definition, member_wise: true, environment:) do |list, member|
1912
+ # The fallback list (`member` false) answers every gradual match rather than the strict pass's pick.
1913
+ OverloadSelector.select_declared(method_definition, list, selector_self, instance_type, type_vars,
1914
+ block_required, environment, member || :gradual, keywords_last,
1915
+ splats)
1916
+ end
1917
+ end
1918
+
1700
1919
  # `RBS::Types::Block#type` is normally an `RBS::Types::Function` carrying the block's parameter
1701
1920
  # list; some signatures use `RBS::Types::UntypedFunction` (a `(?)` block) which exposes no
1702
1921
  # parameter types -- we treat it as "no information" and return an empty array so the binder
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module MethodDispatcher
6
+ # How `OverloadSelector` reads a call whose positional arguments include a splat (`f(*xs, a: v)`), #1801.
7
+ #
8
+ # A splat stands for any number of arguments, so the call's positional count is not known statically. The
9
+ # selector sees each splat as one untyped entry of the argument list, and counted that way it ruled out every
10
+ # overload whose arity the splat may still meet: `ph(*xs, a: "s")` skipped `(Hash[Symbol, String]) -> String`,
11
+ # which takes `{ a: "s" }` when `xs` is empty (`def ph(h) = h; ph(*[], a: "s")` is `{ a: "s" }`). Instead, an
12
+ # overload matches when some count of the splats' elements fits its arity and every argument, the splats'
13
+ # untyped elements included, is accepted at the parameter that count binds it to.
14
+ module SplatArity
15
+ # The most argument lists {.expansions} spells out; past it, the selector only asks that every other argument
16
+ # fit some positional parameter of the overload.
17
+ LIMIT = 32
18
+ private_constant :LIMIT
19
+
20
+ module_function
21
+
22
+ # The argument lists `arg_types` stands for against `fun`, each splat entry (at `splats`, indices into
23
+ # `arg_types`) repeated as often as `fun`'s positional parameters can take, from none upward; nil past
24
+ # {LIMIT}. An untyped `(?)` parameter list takes any count, so the list as given stands for all of them.
25
+ def expansions(fun, arg_types, splats)
26
+ return [arg_types] unless fun.respond_to?(:required_positionals)
27
+
28
+ indices = splats.select { |index| index < arg_types.size }
29
+ return [arg_types] if indices.empty?
30
+
31
+ bound = element_bound(fun, arg_types.size - indices.size)
32
+ counts = counts(indices.size, bound)
33
+ return nil if counts.nil?
34
+
35
+ counts.map { |count| expand(arg_types, indices, count) }
36
+ end
37
+
38
+ # The most splat elements a call with `fixed` other positional arguments can pass to `fun`: up to its maximum
39
+ # arity, or, with a `*rest`, one past filling every other parameter, since further elements land in the rest
40
+ # parameter alike.
41
+ def element_bound(fun, fixed)
42
+ declared = fun.required_positionals.size + fun.optional_positionals.size + fun.trailing_positionals.size
43
+ return declared + 1 if fun.rest_positionals
44
+
45
+ [declared - fixed, 0].max
46
+ end
47
+
48
+ # Every way to give `splats` splats at most `bound` elements in all, or nil past {LIMIT}.
49
+ def counts(splats, bound)
50
+ combinations = [[]]
51
+ splats.times do
52
+ combinations = combinations.flat_map do |partial|
53
+ (0..(bound - partial.sum)).map { |count| partial + [count] }
54
+ end
55
+ return nil if combinations.size > LIMIT
56
+ end
57
+ combinations
58
+ end
59
+
60
+ def expand(arg_types, indices, counts)
61
+ expanded = []
62
+ arg_types.each_with_index do |arg, index|
63
+ position = indices.index(index)
64
+ position ? expanded.concat([arg] * counts[position]) : expanded << arg
65
+ end
66
+ expanded
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -29,7 +29,7 @@ module Rigor
29
29
  # `StructInstance`, the member map built from the call's arguments (positional or keyword per the
30
30
  # class's `keyword_init`). A form / arity mismatch degrades to `Dynamic[top]` rather than a wrong
31
31
  # member map. `.members` on the class folds.
32
- # 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` / `with` on a
32
+ # 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` on a
33
33
  # *fresh* `StructInstance` -> the precise projected type; member *setters* (`s.x = v`) return the
34
34
  # assigned value type. Unhandled / stored-receiver calls return nil so the pipeline projects the
35
35
  # instance to its nominal through RbsDispatch.
@@ -38,8 +38,9 @@ module Rigor
38
38
  module StructFolding
39
39
  module_function
40
40
 
41
- # The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
41
+ # The `[]` / `to_h` / `deconstruct` / `members` projections
42
42
  # and the reader-redefinition guard are shared with {DataFolding}.
43
+ # `Struct` has no `#with` (issue #1335): that projection stays on {DataFolding} only.
43
44
  extend MemberShapeProjection
44
45
 
45
46
  # Issue #595 / #525 — the shared materialisation test; see {StructMaterialization}.
@@ -239,10 +240,6 @@ module Rigor
239
240
  when :deconstruct then instance_deconstruct(instance)
240
241
  when :deconstruct_keys then instance_deconstruct_keys(instance, args)
241
242
  when :members then instance_members(instance)
242
- when :with
243
- instance_with(instance, args) do |members, class_name|
244
- Type::Combinator.struct_instance_of(members: members, class_name: class_name)
245
- end
246
243
  end
247
244
  end
248
245
 
@@ -332,7 +329,7 @@ module Rigor
332
329
  node = context.call_node
333
330
  return false if node.nil?
334
331
 
335
- materialization_call?(node.receiver, context.receiver, context.scope)
332
+ materialization_call?(node.receiver, context.scope)
336
333
  end
337
334
 
338
335
  # ADR-48 slice 4 — precise mutated-member re-typing. After a `local.member = v` setter on a
@@ -34,18 +34,20 @@ module Rigor
34
34
  # Issue #595 / #525 — the ONE materialisation test. Both places that must answer "was this receiver
35
35
  # expression's struct newly built?" go through it: this module's direct member-read gate above, and
36
36
  # `ExpressionTyper`'s caller-side `:self`-grant arm. They had forked answers once (the grant arm was
37
- # tightened for #591 while this one still trusted any chained call), which is exactly how the
38
- # composed `x.dup_self.with(indent: 9).shout` shape survived — a correctly whitelisted `.with` fold
39
- # firing off a receiver the other gate should never have called fresh.
37
+ # tightened for #591 while this one still trusted any chained call), so both consumers must share this
38
+ # one answer. A `.with` call is never a materialisation: the composed `x.dup_self.with(indent: 9)`
39
+ # shape once slipped through as one, and `Struct#with` no longer exists to fold (#1335).
40
40
  #
41
41
  # @param node — the receiver EXPRESSION.
42
- # @param receiver — the carrier the expression produced.
43
- def materialization_call?(node, receiver, scope)
42
+ # @param scope — the scope the factory and ancestor lookups resolve against.
43
+ def materialization_call?(node, scope)
44
44
  return false unless node.is_a?(Prism::CallNode)
45
45
 
46
46
  case node.name
47
47
  when :new, :[] then struct_class_expression?(node.receiver, scope)
48
- when :with then !hand_written_with?(receiver, scope)
48
+ # `Struct` has no `#with` (#1335), so a `.with` call is never a materialisation here; `Data#with` is
49
+ # a frozen copy and needs no freshness gate.
50
+ when :with then false
49
51
  else fresh_factory_call?(node, scope)
50
52
  end
51
53
  end
@@ -127,26 +129,6 @@ module Rigor
127
129
  found
128
130
  end
129
131
 
130
- # `.with` copies the receiver into a new instance — unless the struct wrote its own, which is free to
131
- # return `self` and would reopen the very hole above.
132
- #
133
- # An ANONYMOUS carrier cannot have one: a `StructInstance` with no class name came from a blockless
134
- # `Struct.new(…)` (the block form defers in {fold_struct_new} before any instance exists), and a
135
- # blockless factory defines nothing but the generated accessors. So it is the NAMED carriers that
136
- # need the lookup — and a named one with no scope to ask refuses, since unproven is not fresh.
137
- def hand_written_with?(receiver, scope)
138
- return true unless receiver.is_a?(Type::StructInstance)
139
-
140
- class_name = receiver.class_name
141
- return false if class_name.nil?
142
- return true if scope.nil?
143
-
144
- # The ANCESTOR walk, not the own-class table: a `with` contributed by an included module is just as
145
- # free to return `self`. Both consumers reach the identical answer because there is one lookup here
146
- # rather than a parameter each of them supplies (#598 review).
147
- !scope.user_def_through_ancestors(class_name, :with).first.nil?
148
- end
149
-
150
132
  # An expression naming the struct class itself. All three definition forms ADR-48 supports qualify,
151
133
  # because each of them really does construct: a constant whose member layout the project side-table
152
134
  # recorded (`Point.new(…)`), a local holding a `StructClass` carrier (`c = Struct.new(:x, :y)` then
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Issue #1357 — `Kernel#Rational(x, 1)` converts its argument. CRuby's `nurat_convert` returns a
9
+ # `Rational` for a `Float` / `Integer` / `Rational` / `String`-free numerator and for a `BigDecimal` (which
10
+ # has `to_r`; NaN and Infinity raise). A `Complex` is unwrapped only when its imaginary part is an exact
11
+ # zero, otherwise `a / 1` comes back, a `Complex`, so a `Complex` numerator answers `Rational | Complex`.
12
+ # The core RBS spells the `1`-denominator form `[T < Numeric] (T value, 1) -> T`, which binds `T` to the
13
+ # argument's own class and read `Rational(f, 1)` as a `Float`. Any other numerator (a user `Numeric`
14
+ # without `to_r`) keeps the RBS answer.
15
+ module UnitDenominatorRational
16
+ module_function
17
+
18
+ CONVERTING = Set["Float", "Integer", "Rational", "BigDecimal"].freeze
19
+ private_constant :CONVERTING
20
+
21
+ # Answers `Rational` (or `Rational | Complex`), or nil when the call is not the unit-denominator form.
22
+ def answer(method_name, args)
23
+ return nil unless method_name == :Rational && args.size.between?(2, 3)
24
+ return nil if args.size == 3 && !exception_keyword?(args[2])
25
+
26
+ denominator = args[1]
27
+ return nil unless denominator.is_a?(Type::Constant) && denominator.value.eql?(1)
28
+
29
+ classes = numerator_classes(args.first)
30
+ return nil if classes.nil?
31
+
32
+ members = [Type::Combinator.nominal_of("Rational")]
33
+ members << Type::Combinator.nominal_of("Complex") if classes.include?(:complex)
34
+ Type::Combinator.union(*members)
35
+ end
36
+
37
+ # The trailing `exception:` keyword hash: a `HashShape` whose only key is `:exception`. Its value
38
+ # does not change the answer (a Float that cannot convert raises either way).
39
+ def exception_keyword?(type)
40
+ type.is_a?(Type::HashShape) && type.pairs.keys == [:exception]
41
+ end
42
+
43
+ # `[:converting]`, `[:complex]` or both for a recognised numerator; nil when any member is unknown.
44
+ def numerator_classes(type)
45
+ case type
46
+ when Type::Union then union_classes(type)
47
+ when Type::IntegerRange then [:converting]
48
+ when Type::Refined then type.base.is_a?(Type::Nominal) ? numerator_classes(type.base) : nil
49
+ when Type::Constant then class_kind(type.value.class.name)
50
+ when Type::Nominal then type.type_args.empty? ? class_kind(type.class_name) : nil
51
+ end
52
+ end
53
+
54
+ def union_classes(union)
55
+ kinds = union.members.map { |member| numerator_classes(member) }
56
+ kinds.any?(&:nil?) ? nil : kinds.flatten.uniq
57
+ end
58
+
59
+ def class_kind(name)
60
+ return [:complex] if name == "Complex"
61
+
62
+ CONVERTING.include?(name) ? [:converting] : nil
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end