rigortype 0.4.1 → 0.4.2

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 (182) 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 +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -5,8 +5,11 @@ require_relative "../acceptance"
5
5
  require_relative "../rbs_type_translator"
6
6
  require_relative "alias_strict_nominals"
7
7
  require_relative "facet_distribution"
8
+ require_relative "imprecise_argument"
9
+ require_relative "keyword_arguments"
8
10
  require_relative "proven_overload"
9
11
  require_relative "receiver_affinity"
12
+ require_relative "splat_arity"
10
13
 
11
14
  module Rigor
12
15
  module Inference
@@ -14,9 +17,14 @@ module Rigor
14
17
  # Picks the RBS overload that should answer a call given the caller's actual argument types. Slice 4
15
18
  # phase 2c shape (with the v0.1.2 interface-strictness preference layered on top):
16
19
  #
17
- # 1. Filter overloads by positional arity (required, optional and rest_positionals are honored;
18
- # required_keywords disqualify the overload because we do not yet thread keyword args through
19
- # `call_arg_types`).
20
+ # 1. Filter overloads by positional arity (required, optional and rest_positionals are honored).
21
+ # Keyword arguments (#1727): when the call's last argument is a keyword hash (`keywords_last:`, which
22
+ # only the caller can tell from the AST) and an overload declares keywords, that argument is matched
23
+ # against the overload's keywords rather than as a positional `Hash` — every required keyword
24
+ # present, every key declared, every value accepted. An overload that declares no keywords still
25
+ # reads it as a trailing positional `Hash`, as Ruby passes it, but never in place of a keyword overload that
26
+ # takes it (#1800, `KeywordArguments.prefer_takers`). A call without keywords skips an overload that requires
27
+ # one.
20
28
  # 1a. **Pass 0 — proven, in declared order (#1344).** With only plain-value arguments, the first
21
29
  # overload they do not rule out wins outright when each of its params names its argument's own
22
30
  # class and accepts it with a `yes`; every later pass reads the receiver-affinity order instead.
@@ -44,7 +52,8 @@ module Rigor
44
52
  module OverloadSelector
45
53
  module_function
46
54
 
47
- # `ALIAS_STRICT_NOMINALS`, the alias-strict pass's table, lives in `alias_strict_nominals.rb`.
55
+ # `ALIAS_STRICT_NOMINALS`, the alias-strict pass's table, lives in `alias_strict_nominals.rb`, with the pass's
56
+ # per-parameter test (`alias_param_accepts?`).
48
57
 
49
58
  # @param arg_types — caller-provided types in positional order. Empty when
50
59
  # there are no arguments.
@@ -70,32 +79,48 @@ module Rigor
70
79
  # position, not by types — `[true] * n` with an untyped `n` pinned `Array#*(string) -> String`
71
80
  # and answered a wrong precise type the runtime can contradict. The caller unions the candidates'
72
81
  # returns, which contains the truth whichever overload the runtime takes. The member-wise reading of a
73
- # two-member `Dynamic` facet (#1350, `FacetDistribution`) returns one candidate per member the same way.
82
+ # two-member `Dynamic` facet (#1350, `FacetDistribution`) returns one candidate per member the same way, and
83
+ # where that reading falls back to the arguments as given, every overload they gradually match (#1782).
74
84
  # Every other path (strict, alias-resolved, typed-gradual, arity fallback) yields exactly one candidate,
75
85
  # and `select` (`member_wise: false`) keeps its historical single answer.
76
86
  #
77
87
  # @return matching overloads; empty when the definition declares none.
78
88
  # rubocop:disable-next Metrics/ParameterLists -- the selection keywords plus the member-wise switch.
89
+ # @param keywords_last — the call's last argument is a keyword hash (`f(a, k: 1)`, not `f(a, { k: 1 })`).
90
+ # @param splats — the indices of `arg_types` that are splat arguments (`f(*xs)`), or nil (#1801).
79
91
  def select_candidates(definition, arg_types:, self_type:, instance_type:, type_vars: {},
80
- block_required: false, environment: nil, member_wise: true)
92
+ block_required: false, environment: nil, member_wise: true, keywords_last: false,
93
+ splats: nil)
81
94
  unless FacetDistribution.faceted?(arg_types)
82
95
  return select_declared(definition, arg_types, self_type, instance_type, type_vars, block_required,
83
- environment, false)
96
+ environment, false, keywords_last, splats)
84
97
  end
85
98
 
86
99
  FacetDistribution.select(arg_types, definition, member_wise:, environment:) do |args, member|
87
- select_declared(definition, args, self_type, instance_type, type_vars, block_required, environment, member)
100
+ # Issue #1782 — the fallback (`member` false) reads the arguments as given, where the bare `Dynamic`
101
+ # gradually matches every arm and the strict pass's pick is the first by position, so the member-wise answer
102
+ # is every gradual match (`:joined`), as the block probe reads it (#1750); the caller joins their returns.
103
+ select_declared(definition, args, self_type, instance_type, type_vars, block_required, environment,
104
+ member || (member_wise && :joined), keywords_last, splats)
88
105
  end
89
106
  end
90
107
 
91
108
  # The selection proper, positional so the hot path forwards no keyword hash. A member-wise call (`member`, see
92
- # `FacetDistribution.select`) answers only a genuine match, never the first-overload fallback.
109
+ # `FacetDistribution.select`) answers only a genuine match, never the first-overload fallback. `member` set to
110
+ # `:gradual` answers every overload the arguments gradually match instead (#1750): where
111
+ # `FacetDistribution.select` falls back to the arguments as given, a bare `Dynamic` positional gradually
112
+ # matches every arm and the strict pass's single pick is decided by position, so the block-parameter probe
113
+ # binds only what all of them agree on. `:joined` answers the same matches, but where none matches it keeps the
114
+ # first-overload fallback, as the return path's reading of the arguments as given always has (#1782).
93
115
  # rubocop:disable-next Metrics/ParameterLists -- the selection inputs plus the member-wise flag.
94
116
  def select_declared(method_definition, arg_types, self_type, instance_type, type_vars, block_required,
95
- environment, member)
117
+ environment, member, keywords_last, splats = nil)
96
118
  declared = method_definition.method_types
97
119
  return [] if declared.empty?
98
120
 
121
+ # A keyword hash only some overload takes as keywords is a plain positional `Hash` otherwise (#1737).
122
+ keywords_last &&= KeywordArguments.any_declares?(declared)
123
+
99
124
  # `rigor:v1:param: <name> <refinement>` annotations on this method override the RBS-declared
100
125
  # parameter type at the matching name. The map is consumed inside `accepts_param?` so overload
101
126
  # selection sees the tighter type when filtering candidates by argument compatibility.
@@ -110,12 +135,17 @@ module Rigor
110
135
 
111
136
  # One keyword bundle for the pass pipeline (see `run_selection_passes`); a block retry rebuilds it
112
137
  # with the block flag cleared. Built once and passed positionally -- the previous lambda plus a
113
- # `**shared` splat per pass allocated three objects per selection (#775).
138
+ # `**shared` splat per pass allocated three objects per selection (#775). Keep it at eight keys or fewer:
139
+ # CRuby embeds a Hash that small, and a ninth key allocates a table on every selection (the RBS loader is
140
+ # read from `environment` rather than carried as a key of its own, #1727).
114
141
  shared = { arg_types: arg_types, self_type: self_type, instance_type: instance_type,
115
142
  type_vars: type_vars, block_required: block_required, param_overrides: param_overrides,
116
- alias_expander: environment&.rbs_loader, environment: environment }
143
+ environment: environment, keywords_last: keywords_last }
144
+ # Only a call with a splat pays for a ninth key (#1801).
145
+ shared[:splats] = splats if splats
117
146
 
118
- matches = run_selection_passes(declared, overloads, shared)
147
+ gradual = member.is_a?(Symbol) # `:gradual` or `:joined`
148
+ matches = run_selection_passes(declared, overloads, shared, gradual)
119
149
  return matches unless matches.empty?
120
150
 
121
151
  # A block at the call site that no block-declaring overload matched: Ruby ignores a block handed
@@ -124,10 +154,10 @@ module Rigor
124
154
  # `define_command(:x) do … end` against `def define_command: (Symbol) -> Symbol`) degraded to
125
155
  # `Dynamic[Top]` — and on a self-send suppressed the whole method's return type.
126
156
  if block_required
127
- matches = run_selection_passes(declared, overloads, shared.merge(block_required: false))
157
+ matches = run_selection_passes(declared, overloads, shared.merge(block_required: false), gradual)
128
158
  return matches unless matches.empty?
129
159
  end
130
- return [] if member
160
+ return [] if member && member != :joined
131
161
 
132
162
  # No (usable) block at the call site: prefer an overload that does not REQUIRE a block over
133
163
  # `overloads.first`. Methods like `Array#filter` / `Enumerable#map` declare the block-bearing
@@ -153,7 +183,7 @@ module Rigor
153
183
  private
154
184
 
155
185
  # Overload search after pass 0 (`find_proven_overload`):
156
- # - Pass 1 (strict): skipped when any arg is imprecise (`imprecise_arg?`), because gradual
186
+ # - Pass 1 (strict): skipped when any arg is imprecise (`ImpreciseArgument.imprecise?`), because gradual
157
187
  # acceptance against an untyped arg accepts every param indiscriminately and would let pass 1
158
188
  # lock in an arbitrary strict overload (e.g. `Regexp#=~(nil) -> nil` over the
159
189
  # `(::interned?) -> Integer?` overload).
@@ -172,25 +202,39 @@ module Rigor
172
202
  # order, the strict pass let the `bigdecimal` reopen's `(BigDecimal) -> BigDecimal` arm, which it
173
203
  # matches on a `maybe`, take `Integer#+` of a `bot`, a `Dynamic[Integer | Float | …]` or an
174
204
  # unloadable class (612 call sites across the survey corpus).
175
- def run_selection_passes(declared, overloads, shared)
176
- # Unreordered, pass 0 can only pick what the strict pass picks, so it is skipped (#1344's allocations).
177
- proven = find_proven_overload(declared, shared) unless overloads.equal?(declared)
178
- strict = proven || find_matching_overload(overloads, shared, strict: true)
179
- return strict unless strict.empty?
180
-
181
- alias_hit = find_matching_overload_via_aliases(
182
- overloads, arg_types: shared[:arg_types], block_required: shared[:block_required]
183
- )
184
- return [alias_hit] if alias_hit
205
+ def run_selection_passes(declared, overloads, shared, gradual = false) # rubocop:disable Style/OptionalBooleanParameter
206
+ matches = run_passes(declared, overloads, shared, gradual)
207
+ return matches unless shared[:keywords_last]
208
+
209
+ # Issue #1800 — never a positional reader of the keyword hash in place of a keyword overload that takes it.
210
+ KeywordArguments.prefer_takers(declared, matches) do |takers, strict|
211
+ find_matching_overload(takers, shared, strict:)
212
+ end
213
+ end
214
+
215
+ def run_passes(declared, overloads, shared, gradual)
216
+ # The `:gradual` list (#1750) skips straight to pass 2's every-match answer, stand-ins included.
217
+ unless gradual
218
+ # Unreordered, pass 0 can only pick what the strict pass picks, so it is skipped (#1344's allocations).
219
+ proven = find_proven_overload(declared, shared) unless overloads.equal?(declared)
220
+ strict = proven || find_matching_overload(overloads, shared, strict: true)
221
+ return strict unless strict.empty?
222
+
223
+ alias_hit = find_matching_overload_via_aliases(overloads, shared)
224
+ return [alias_hit] if alias_hit
225
+ end
185
226
 
186
227
  # Pass 2, array-valued. With every argument carrying real type information the first gradual
187
228
  # match keeps its historical single-winner contract. With an imprecise argument in play the
188
229
  # matches are indistinguishable by types — position alone would pick — so ALL of them come back
189
230
  # and the dispatch layer unions their returns (#521).
190
231
  matches = find_matching_overload(overloads, shared, strict: false)
191
- return matches.first(1) unless shared[:arg_types].any? { |t| imprecise_arg?(t) }
232
+ every_match = gradual || ImpreciseArgument.any_in?(shared[:arg_types], shared[:keywords_last])
233
+ return matches.first(1) unless every_match
192
234
 
193
- matches
235
+ # Issue #1675 — and every overload the arguments' untyped parts reach on their own, after the
236
+ # whole-argument matches so the singular `select` keeps its answer.
237
+ ImpreciseArgument.with_stand_ins(matches, shared) { find_matching_overload(overloads, it, strict: false) }
194
238
  end
195
239
 
196
240
  # Pass 0. With every argument a plain value (a `Constant`, or a `Nominal` with no type arguments), the
@@ -207,7 +251,7 @@ module Rigor
207
251
 
208
252
  index = declared.index { |mt| engages_block_shape?(mt, shared[:block_required]) && matches?(mt, shared) }
209
253
  first = index && declared[index]
210
- proven = first && strictly_typed_params?(first, args.size) && matches?(first, shared, strict: :proven)
254
+ proven = first && strictly_typed_params?(first, shared) && matches?(first, shared, strict: :proven)
211
255
  [first] if proven && !ProvenOverload.contested?(declared, index, args, shared[:environment])
212
256
  end
213
257
 
@@ -216,10 +260,10 @@ module Rigor
216
260
  private_constant :NO_MATCH
217
261
 
218
262
  # `shared` is the keyword bundle `select_candidates` assembled (arg_types, self_type, instance_type,
219
- # type_vars, block_required, param_overrides, alias_expander).
263
+ # type_vars, block_required, param_overrides, environment, keywords_last).
220
264
  def find_matching_overload(overloads, shared, strict:)
221
265
  arg_types = shared[:arg_types]
222
- return NO_MATCH if strict && arg_types.any? { |t| imprecise_arg?(t) }
266
+ return NO_MATCH if strict && ImpreciseArgument.any_in?(arg_types, shared[:keywords_last])
223
267
 
224
268
  block_required = shared[:block_required]
225
269
  # Strict keeps its historical first-match short-circuit (a dispatch hot path); the gradual
@@ -227,8 +271,8 @@ module Rigor
227
271
  if strict
228
272
  found = overloads.find do |method_type|
229
273
  engages_block_shape?(method_type, block_required) &&
230
- strictly_typed_params?(method_type, arg_types.size) &&
231
- matches?(method_type, shared, strict: true)
274
+ strictly_typed_params?(method_type, shared) &&
275
+ matches?(method_type, shared, strict: strict)
232
276
  end
233
277
  return found ? [found] : NO_MATCH
234
278
  end
@@ -247,38 +291,26 @@ module Rigor
247
291
  !OverloadSelector.overload_requires_block?(method_type)
248
292
  end
249
293
 
250
- # Treats the literal `untyped` carrier (`Dynamic[Top]`) as too imprecise to drive a strict-pass
251
- # match. Other `Dynamic`-wrapped types with a concrete static facet carry enough information to
252
- # pick a sensible overload.
253
- def untyped_arg?(type) = type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
254
-
255
- # Issue #1021 — the strict and alias passes must not discriminate on an argument whose type
256
- # cannot rule an overload out: the bare untyped carrier, or a union with an untyped member. That
257
- # member may reach any overload at runtime, so a strict match is decided by the union's other
258
- # members alone — `Dynamic[top] | nil` pinned `Regexp#match?(nil) -> false` and typed a live
259
- # predicate as the literal `false`. The gradual pass still accepts against the whole union, so
260
- # `Dynamic[top] | nil` keeps both `match?` overloads and the #521 join answers `Dynamic[bool]`.
261
- # A `Dynamic` with a concrete static facet stays out: its facet discriminates.
262
- def imprecise_arg?(type)
263
- untyped_arg?(type) || (type.is_a?(Type::Union) && type.members.any? { |member| untyped_arg?(member) })
264
- end
265
-
266
294
  # Pass 1.5: for arity-compatible overloads whose every positional param is either a strict
267
295
  # nominal OR a well-known core alias (`int` / `string` / `interned` / etc.), check the arg
268
296
  # against the alias's STRICT arm. An Integer literal arg matches `int` here but not `string`, so
269
297
  # `Array#*(int)` wins over the `Array#*(string) -> String` overload — even though both translate
270
298
  # to `Dynamic[Top]` at the param level. Only fires when EVERY positional param has a known
271
299
  # alias-or-strict shape; otherwise gradual matching takes over.
272
- def find_matching_overload_via_aliases(overloads, arg_types:, block_required:)
300
+ def find_matching_overload_via_aliases(overloads, shared)
273
301
  # Issue #521 — an untyped argument "maybe"-accepts EVERY alias's strict arm, so it cannot
274
302
  # discriminate between overloads here any more than in the strict pass; without this guard a
275
303
  # Dynamic arg pinned `Array#*(string) -> String` purely by declaration order.
276
- return nil if arg_types.any? { |t| imprecise_arg?(t) }
304
+ return nil if ImpreciseArgument.any_in?(shared[:arg_types], shared[:keywords_last])
277
305
 
278
306
  overloads.find do |method_type|
279
- next false unless engages_block_shape?(method_type, block_required)
307
+ next false unless engages_block_shape?(method_type, shared[:block_required])
280
308
 
281
309
  fun = method_type.type
310
+ ok = KeywordArguments.accepted_by?(fun, shared, false) { |*kv| accepts_param?(*kv, shared, false, true) }
311
+ next false unless ok
312
+
313
+ arg_types = KeywordArguments.selection_positional(method_type, shared)
282
314
  next false unless arity_compatible?(fun, arg_types.size)
283
315
 
284
316
  params = positional_params_for(fun, arg_types.size)
@@ -297,51 +329,22 @@ module Rigor
297
329
  params.all? { |param| yield(param, arg_types[index += 1]) }
298
330
  end
299
331
 
300
- # Checks the param's RBS type against an arg using alias-strict-arm matching. Optional / Union
301
- # wrappers are flattened; alias resolution is one level deep (the canonical core aliases all have
302
- # non-alias strict arms).
303
- def alias_param_accepts?(rbs_type, arg)
304
- nominal_names = strict_nominal_names_for(rbs_type)
305
- return false if nominal_names.nil? || nominal_names.empty?
306
-
307
- nominal_names.any? do |class_name|
308
- result = Type::Combinator.nominal_of(class_name).accepts(arg, mode: :gradual)
309
- result.yes? || result.maybe?
310
- end
311
- end
312
-
313
- # Returns the candidate class names a param's RBS type accepts under alias-resolved strict
314
- # matching, or nil when the shape cannot be reduced to a closed set of nominals (e.g. an
315
- # Interface or an unrecognised alias).
316
- def strict_nominal_names_for(rbs_type)
317
- case rbs_type
318
- when RBS::Types::ClassInstance
319
- [rbs_type.name.to_s.delete_prefix("::")]
320
- when RBS::Types::Alias
321
- ALIAS_STRICT_NOMINALS[rbs_type.name.to_s]
322
- when RBS::Types::Optional
323
- strict_nominal_names_for(rbs_type.type)
324
- when RBS::Types::Union
325
- parts = rbs_type.types.map { |t| strict_nominal_names_for(t) }
326
- return nil if parts.any?(&:nil?)
327
-
328
- parts.flatten
329
- end
330
- end
331
-
332
332
  # Returns true when every positional param the call site engages translates to a non-`Dynamic[Top]`
333
333
  # carrier. Alias / Interface / Intersection RBS types all degrade to `Dynamic[Top]` per the
334
334
  # translator's current shape — those gradually accept any arg, so an overload that includes one
335
335
  # would beat strictly-typed alternatives in pass 2 of the selector.
336
- def strictly_typed_params?(method_type, actual_count)
336
+ def strictly_typed_params?(method_type, shared)
337
+ actual_count = KeywordArguments.selection_positional(method_type, shared).size
337
338
  fun = method_type.type
338
339
  # A `(?)` method type declares no params at all: it is the gradual case by construction, so it
339
340
  # must never win the strict pass over a genuinely typed sibling overload.
340
341
  return false unless fun.respond_to?(:required_positionals)
341
342
  return false unless arity_compatible?(fun, actual_count)
342
343
 
343
- params = positional_params_for(fun, actual_count)
344
- params.all? { |param| !alias_or_interface_param?(param.type) }
344
+ # #1737 — the keywords the call passes count too, so an `untyped` keyword cannot win the strict pass over
345
+ # a typed one.
346
+ positional_params_for(fun, actual_count).all? { |param| !alias_or_interface_param?(param.type) } &&
347
+ KeywordArguments.passed_params(fun, shared).all? { |param| !alias_or_interface_param?(param.type) }
345
348
  end
346
349
 
347
350
  # Recursive: an Optional / Union wrapper is strict iff every member is strict. Type args of a
@@ -368,26 +371,33 @@ module Rigor
368
371
  end
369
372
 
370
373
  # `shared` is the keyword bundle `select_candidates` assembled (see `find_matching_overload`).
371
- # `strict:` is `false` (gradual), `true` (strict pass) or `:proven` (pass 0; see `find_proven_overload`).
374
+ # `strict:` is `false` (gradual), `true` (strict pass), `:proven` (pass 0; see `find_proven_overload`) or
375
+ # `:yes` (the strict pass with a `yes` at every pair, #1800's proof; see `KeywordArguments.prefer_takers`).
372
376
  def matches?(method_type, shared, strict: false)
373
- return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
374
-
375
- arg_types = shared[:arg_types]
376
377
  fun = method_type.type
377
- return false unless arity_compatible?(fun, arg_types.size)
378
+ ok = KeywordArguments.accepted_by?(fun, shared, strict) { |*kv| accepts_param?(*kv, shared, strict, true) }
379
+ return false unless ok
378
380
 
379
- params = positional_params_for(fun, arg_types.size)
380
- each_param_accepts?(params, arg_types) { |param, arg| accepts_param?(param, arg, shared, strict) }
381
+ arg_types = KeywordArguments.selection_positional(method_type, shared)
382
+ splats = shared[:splats]
383
+ return positionals_accept?(fun, arg_types, shared, strict) unless splats
384
+
385
+ # #1801 — a splat stands for any number of arguments: some count of its elements must fit.
386
+ lists = SplatArity.expansions(fun, arg_types, splats)
387
+ return lists.any? { |list| positionals_accept?(fun, list, shared, strict) } if lists
388
+
389
+ # Past the cap the position each other argument lands at is not spelled out, but it lands at one.
390
+ params = KeywordArguments.positional_params(fun)
391
+ arg_types.each_with_index.all? do |arg, index|
392
+ splats.include?(index) || params.any? { |param| accepts_param?(param, arg, shared, strict) }
393
+ end
381
394
  end
382
395
 
383
- # Slice 4 phase 2c does not pass keyword arguments through the call site (caller passes only
384
- # positional `arg_types`). An overload that requires keywords is therefore not a viable
385
- # candidate; we skip it instead of forcing a fallback.
386
- def rejects_keyword_required?(method_type)
387
- fun = method_type.type
388
- return false unless fun.respond_to?(:required_keywords)
396
+ def positionals_accept?(fun, arg_types, shared, strict)
397
+ return false unless arity_compatible?(fun, arg_types.size)
389
398
 
390
- !fun.required_keywords.empty?
399
+ params = positional_params_for(fun, arg_types.size)
400
+ each_param_accepts?(params, arg_types) { |kw, arg| accepts_param?(kw, arg, shared, strict) }
391
401
  end
392
402
 
393
403
  # `RBS::Types::UntypedFunction` (`(?)`) declares no arity to enforce, so every call site is
@@ -434,23 +444,25 @@ module Rigor
434
444
  end
435
445
 
436
446
  # `shared` is the keyword bundle `select_candidates` assembled (see `find_matching_overload`).
437
- def accepts_param?(param, arg, shared, strict)
447
+ def accepts_param?(param, arg, shared, strict, keyword = false) # rubocop:disable Style/OptionalBooleanParameter
438
448
  param_type = shared[:param_overrides][param.name] || RbsTypeTranslator.translate(
439
449
  param.type,
440
450
  self_type: shared[:self_type],
441
451
  instance_type: shared[:instance_type],
442
452
  type_vars: shared[:type_vars],
443
- alias_expander: shared[:alias_expander]
453
+ alias_expander: shared[:environment]&.rbs_loader
444
454
  )
445
455
  # An `untyped` arg gradually accepts against every param, so a value-pinning param would be
446
456
  # "matched" with zero evidence and its value-precise return (`(nil) -> []`) would beat broader
447
457
  # overloads purely by list position. Decline the pair; only the strict pass (where the arg
448
458
  # proves the value) or the final first-overload fallback may select such an overload. (Pass 1
449
459
  # already skips untyped args entirely, so this only engages pass 2.)
450
- return false if untyped_arg?(arg) && value_pinning?(param_type)
460
+ # A keyword value is exempt (#1737): an untyped one marks the call imprecise (`ImpreciseArgument.any_in?`),
461
+ # so every arm it reaches joins the #521 union instead of one winning by position.
462
+ return false if !keyword && ImpreciseArgument.untyped?(arg) && value_pinning?(param_type)
451
463
 
452
464
  result = param_type.accepts(arg, mode: :gradual)
453
- return result.yes? && ProvenOverload.names_arg_class?(param_type, arg) if strict == :proven
465
+ return proof?(result, param_type, arg, strict) if strict.is_a?(Symbol)
454
466
 
455
467
  # A record's `maybe` for a `Hash` with a gradual arm or an open shape is no evidence for the overload:
456
468
  # with `({ a: Integer }) -> Integer | (Hash[Symbol, untyped]) -> String`, `{ **o, b: 2 }` has a key the
@@ -461,6 +473,11 @@ module Rigor
461
473
  result.yes? || result.maybe?
462
474
  end
463
475
 
476
+ # Pass 0 (`:proven`) takes a `yes` naming the argument's own class; #1800's keyword proof (`:yes`) any `yes`.
477
+ def proof?(result, param_type, arg, strict)
478
+ result.yes? && (strict == :yes || ProvenOverload.names_arg_class?(param_type, arg))
479
+ end
480
+
464
481
  # A type that admits only specific VALUES rather than a class of values: a `Constant` carrier
465
482
  # (RBS `nil` and literal types both translate to one) or a union made up entirely of them
466
483
  # (`true | false`, `1 | 2`, `nil?`-style optionals of literals).