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
@@ -6,6 +6,7 @@ require_relative "../reflection"
6
6
  require_relative "../source/node_walker"
7
7
  require_relative "../source/constant_path"
8
8
  require_relative "../inference/singleton_object_constant"
9
+ require_relative "../inference/object_mixins"
9
10
  require_relative "shadow_harness"
10
11
  require_relative "../type"
11
12
  require_relative "diagnostic"
@@ -29,6 +30,7 @@ require_relative "check_rules/self_closedness_scanner"
29
30
  require_relative "check_rules/source_arity"
30
31
  require_relative "check_rules/lexical_method_sites"
31
32
  require_relative "check_rules/special_global_setters"
33
+ require_relative "check_rules/ruby_deprecations"
32
34
 
33
35
  module Rigor
34
36
  module Analysis
@@ -66,6 +68,17 @@ module Rigor
66
68
  # resolution chain's.
67
69
  VISIBILITY_RANK = { public: 2, protected: 1, private: 0 }.freeze
68
70
 
71
+ # ADR-35 WD10 (issue #1716) — the object-lifecycle hooks the three override rules skip. `initialize` is
72
+ # reached through `Class#new` on the class the caller names, and the copy hooks are handed an instance
73
+ # of the receiver's own class by `dup` / `clone`, so neither is called with the PARENT's declared
74
+ # arguments through a parent-typed reference; `new` / `dup` / `clone` discard all four returns.
75
+ LIFECYCLE_HOOKS = %i[initialize initialize_copy initialize_dup initialize_clone].freeze
76
+
77
+ # The names CRuby makes private on every non-singleton `def` whatever section it is written in
78
+ # (`vm_method.c` `rb_method_entry_make`), so a source-discovered visibility for them is not the
79
+ # runtime one and `def.override-visibility-reduced` cannot prove a reduction.
80
+ IMPLICITLY_PRIVATE_METHODS = (LIFECYCLE_HOOKS + %i[respond_to_missing?]).freeze
81
+
69
82
  # Resolves a user-supplied rule token (`undefined-method`,
70
83
  # `call.undefined-method`, or the family wildcard `call`)
71
84
  # to the set of canonical rule identifiers it disables.
@@ -74,6 +87,7 @@ module Rigor
74
87
  def self.resolve_rule_token(token)
75
88
  return nil if token == "all"
76
89
  return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
90
+ return PRE_EVAL_RULES if token == "pre-eval"
77
91
  return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
78
92
 
79
93
  ALL_RULES.include?(token) ? [token] : []
@@ -96,8 +110,11 @@ module Rigor
96
110
  # direct caller with no plugin walk, e.g. a unit test), the standalone
97
111
  # {RuleWalk} walk runs here instead, so `diagnose` stays correct
98
112
  # without the converged path.
99
- def diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [],
100
- node_collectors: nil)
113
+ #
114
+ # @param stated_runtime_ruby — {Configuration#stated_runtime_ruby}: the explicit `target_ruby`, or nil. Only
115
+ # {RubyDeprecations} reads it, and it walks the file only when the stated Ruby deprecates something.
116
+ def diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [], # rubocop:disable Metrics/ParameterLists
117
+ node_collectors: nil, stated_runtime_ruby: nil)
101
118
  collectors = node_collectors || run_node_collectors(path, root, scope_index)
102
119
  diagnostics = collectors[:main_pass].results.dup
103
120
  diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
@@ -109,6 +126,9 @@ module Rigor
109
126
  # `suppression.*` joins the list, which stays reportable inside a dead arm — a malformed
110
127
  # `# rigor:disable` marker is an authoring error regardless of whether its code runs.
111
128
  diagnostics = DeadVersionGuardArms.filter(diagnostics, root)
129
+ # The ADR-47 WD5 amendment (#1692) — joined after that filter: the deprecation rule reads each version guard
130
+ # against the stated Ruby itself, and the analyzer's own Ruby says nothing about where the call runs.
131
+ diagnostics.concat(RubyDeprecations.diagnostics(path, root, scope_index, stated_runtime_ruby))
112
132
  # Suppression-marker validation (`suppression.*`) runs BEFORE the filter so its own diagnostics are
113
133
  # suppressible like any other rule — `# rigor:disable suppression.unknown-rule` on the offending
114
134
  # comment's line works, with no regress (the token itself is known, so it never re-fires).
@@ -166,7 +186,7 @@ module Rigor
166
186
  diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
167
187
  return diagnostic if diagnostic.nil? || lexical_sites.nil?
168
188
 
169
- lexical_sites.refinement_def?(node) ? nil : diagnostic
189
+ lexical_sites.refinements.refinement_def?(node) ? nil : diagnostic
170
190
  end
171
191
 
172
192
  # Constructs the fresh, unpopulated built-in collector set keyed by
@@ -178,7 +198,7 @@ module Rigor
178
198
  # diagnostics carry it (ADR-53 B3c hosts it on the same walk).
179
199
  def build_node_collectors(path, scope_index, root = nil)
180
200
  eval_ranges = receiver_eval_block_ranges(root)
181
- lexical_sites = LexicalMethodSites.new(root)
201
+ lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
182
202
  main_pass = ->(node) { main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites) }
183
203
  {
184
204
  main_pass: MainPassCollector.new(main_pass),
@@ -252,6 +272,14 @@ module Rigor
252
272
  collector.class.new(scope_index).collect(root)
253
273
  end
254
274
 
275
+ # Issue #1673 — the in-effect refinements `Inference::ScopeIndexer` stamped for `root`, read off the root's scope
276
+ # so the rules answer from the query the typer reads; nil where the index holds no scope for it.
277
+ def stamped_refinements(scope_index, root)
278
+ return nil if root.nil? || scope_index.nil?
279
+
280
+ scope_index[root]&.discovery&.in_effect_refinements
281
+ end
282
+
255
283
  # The former inline main pass, kept as the shadow oracle: walks the
256
284
  # tree with `Source::NodeWalker.each` and accumulates the same
257
285
  # per-node diagnostics in the same order {MainPassCollector} now
@@ -259,7 +287,7 @@ module Rigor
259
287
  def main_pass_oracle(path, root, scope_index)
260
288
  diagnostics = []
261
289
  eval_ranges = receiver_eval_block_ranges(root)
262
- lexical_sites = LexicalMethodSites.new(root)
290
+ lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
263
291
  Source::NodeWalker.each(root) do |node|
264
292
  diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites))
265
293
  end
@@ -284,15 +312,15 @@ module Rigor
284
312
  def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
285
313
  [
286
314
  undefined_method_diagnostic(path, node, scope_index, lexical_sites),
287
- unresolved_toplevel_diagnostic(path, node, scope_index, eval_ranges),
288
- wrong_arity_diagnostic(path, node, scope_index),
289
- argument_type_diagnostic(path, node, scope_index),
290
- nil_receiver_diagnostic(path, node, scope_index),
315
+ unresolved_toplevel_diagnostic(path, node, scope_index, eval_ranges, lexical_sites),
316
+ wrong_arity_diagnostic(path, node, scope_index, lexical_sites),
317
+ argument_type_diagnostic(path, node, scope_index, lexical_sites),
318
+ nil_receiver_diagnostic(path, node, scope_index, lexical_sites),
291
319
  dump_type_diagnostic(path, node, scope_index),
292
320
  assert_type_diagnostic(path, node, scope_index),
293
- always_raises_diagnostic(path, node, scope_index),
321
+ always_raises_diagnostic(path, node, scope_index, lexical_sites),
294
322
  raise_non_exception_diagnostic(path, node, scope_index),
295
- visibility_mismatch_diagnostic(path, node, scope_index)
323
+ visibility_mismatch_diagnostic(path, node, scope_index, lexical_sites)
296
324
  ].compact
297
325
  end
298
326
 
@@ -316,6 +344,10 @@ module Rigor
316
344
  # Class-level ivars (`@x = 1` outside any def, in the
317
345
  # class body) are also skipped — they're a separate
318
346
  # surface (`Module#@var`) the engine doesn't yet model.
347
+ # - A divergence the class's RBS declaration admits is withheld: when the ivar is declared (`@x: T`,
348
+ # or the slot an `attr_*` member implies) and that type accepts both the first write and the
349
+ # later one, the union is the author's intent. The declaration only silences here; checking
350
+ # writes against it is #1406.
319
351
  def ivar_write_mismatch_diagnostics(path, ivar_writes)
320
352
  ivar_writes.flat_map do |class_name, writes_by_ivar|
321
353
  writes_by_ivar.flat_map do |ivar_name, writes|
@@ -427,11 +459,18 @@ module Rigor
427
459
  writes[(canonical_index + 1)..].filter_map do |write|
428
460
  other_class = ivar_class_for(write[:type])
429
461
  next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class
462
+ next nil if declaration_admits?(write[:declared], canonical[:type], write[:type])
430
463
 
431
464
  build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class)
432
465
  end
433
466
  end
434
467
 
468
+ def declaration_admits?(declared, *write_types)
469
+ return false if declared.nil?
470
+
471
+ write_types.none? { |type| Inference::Acceptance.accepts(declared, type, mode: :gradual).no? }
472
+ end
473
+
435
474
  # v0.0.2 #6 — diagnostic suppression. Three kinds of
436
475
  # suppression compose:
437
476
  #
@@ -516,7 +555,7 @@ module Rigor
516
555
  line_suppressions = Hash.new { |h, k| h[k] = Set.new }
517
556
  file_suppressions = Set.new
518
557
  comments.each do |comment|
519
- source = comment.location.slice
558
+ source = comment_text(comment)
520
559
  if (match = FILE_SUPPRESSION_PATTERN.match(source))
521
560
  absorb_suppression_tokens(match[:rules], file_suppressions)
522
561
  elsif (match = LINE_SUPPRESSION_PATTERN.match(source))
@@ -526,6 +565,15 @@ module Rigor
526
565
  [line_suppressions, file_suppressions]
527
566
  end
528
567
 
568
+ # The comment's text for the suppression regexps. A comment is not validated by Ruby, so it may hold a
569
+ # byte that is invalid in the file's encoding, and a regexp raises on such a string; the marker grammar
570
+ # is ASCII, so scrubbing the bad bytes loses nothing. Positions come from `comment.location`, which is
571
+ # byte-offset based and untouched.
572
+ def comment_text(comment)
573
+ text = comment.location.slice
574
+ text.valid_encoding? ? text : text.scrub("?")
575
+ end
576
+
529
577
  def absorb_suppression_tokens(raw, target)
530
578
  raw.to_s.split(/[\s,]+/).reject(&:empty?).each do |token|
531
579
  target.merge(expand_token(token))
@@ -543,7 +591,7 @@ module Rigor
543
591
  # that token is known, so acknowledging it never re-fires the rule.
544
592
  def suppression_marker_diagnostics(path, comments)
545
593
  comments.each_with_object([]) do |comment, diagnostics|
546
- source = comment.location.slice
594
+ source = comment_text(comment)
547
595
  if (match = FILE_SUPPRESSION_PATTERN.match(source))
548
596
  validate_suppression_tokens(match[:rules], "rigor:disable-file", path, comment, diagnostics)
549
597
  elsif (match = LINE_SUPPRESSION_PATTERN.match(source))
@@ -607,7 +655,7 @@ module Rigor
607
655
  # `dynamic.*`, `rbs.*`, `pre-eval.*`, and any `plugin.`-prefixed id — plugins load dynamically, so
608
656
  # their rule vocabulary cannot be enumerated here and under-warning is the FP-safe direction).
609
657
  def known_suppression_token?(token)
610
- return true if token == "all"
658
+ return true if %w[all pre-eval].include?(token)
611
659
  return true if ALL_RULES.include?(token) || LEGACY_RULE_ALIASES.key?(token) ||
612
660
  RULE_FAMILIES.include?(token) || NON_CHECK_DIAGNOSTIC_IDS.include?(token)
613
661
 
@@ -678,13 +726,13 @@ module Rigor
678
726
  class << self
679
727
  private
680
728
 
681
- def undefined_method_diagnostic(path, call_node, scope_index, lexical_sites = nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
729
+ def undefined_method_diagnostic(path, call_node, scope_index, lexical_sites = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
682
730
  return nil if call_node.receiver.nil?
683
731
 
684
732
  scope = scope_index[call_node]
685
733
  return nil if scope.nil?
686
734
 
687
- # The two exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
735
+ # The exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
688
736
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
689
737
  # gate belongs where that one consult is rather than duplicated into each shape's path.
690
738
  #
@@ -722,12 +770,18 @@ module Rigor
722
770
  # the recorded name only: `Merger.nope` still fires.
723
771
  return nil if Inference::SingletonObjectConstant.recorded?(call_node, receiver_type, call_node.name, scope)
724
772
 
773
+ # Issue #1699 — judged as its one class; the message still names the union. See
774
+ # {#single_class_literal_union}.
775
+ rendered_receiver = receiver_type
776
+ receiver_type = single_class_literal_union(receiver_type)
725
777
  class_name = concrete_class_name(receiver_type)
726
778
  # A union receiver has no single concrete class. The scalar path
727
779
  # below cannot reason about it, but the call is still definitely
728
780
  # undefined when EVERY arm lacks the method — see
729
781
  # `union_undefined_method_diagnostic`.
730
- return union_undefined_method_diagnostic(path, call_node, receiver_type, scope) if class_name.nil?
782
+ if class_name.nil?
783
+ return union_undefined_method_diagnostic(path, call_node, receiver_type, scope, lexical_sites)
784
+ end
731
785
 
732
786
  # ADR-26 — a plugin may declare a class "open": one
733
787
  # known to respond beyond its RBS-declared method
@@ -782,50 +836,100 @@ module Rigor
782
836
  # `call.undefined-method` into 171 through exactly this reading, 49 of them here.
783
837
  return nil if project_sidecar_owns_method?(scope, class_name, call_node.name, kind)
784
838
 
785
- build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
839
+ build_undefined_method_diagnostic(path, call_node, rendered_receiver, definition_site, class_name)
840
+ end
841
+
842
+ # Issue #1717 — an explicit `self.m` / `self.m = v` inside a block whose `self` the engine does not know
843
+ # (`Scope#block_self_unknown?`, set where issue #316's opaque mark is). The receiver still types as the
844
+ # ENCLOSING `self`, but the yielding method may `instance_exec` the block on another object
845
+ # (`ActionController::Renderers.add` runs its block on the controller), so a method missing on the
846
+ # enclosing `self` is no evidence. The implicit `m` in the same block is already silent; the explicit
847
+ # spelling must not be stricter. A block
848
+ # whose `self` IS narrowed (`block_as_methods:`, `define_method`, a `Class.new` body) has cleared the
849
+ # mark and keeps being checked, unless the narrowing was read off an unknown enclosing `self`.
850
+ def unknown_block_self_receiver?(call_node, scope)
851
+ call_node.receiver.is_a?(Prism::SelfNode) && scope.block_self_unknown?
786
852
  end
787
853
 
788
854
  # Issue #1120 — the method exists at THIS call site but not everywhere: a refinement whose `using` is in
789
855
  # effect here, or a `def o.m` on the receiver local in this scope. Asked from {#last_resort_surface_answers?},
790
856
  # once every project-wide table has come back "absent", so the file walk behind {LexicalMethodSites} runs
791
857
  # only for a call about to fire.
792
- # A refinement answers instance-side receivers only: a refined singleton (`refine X.singleton_class`) names
793
- # no constant target, so nothing records it.
794
858
  def lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
795
859
  return false if lexical_sites.nil?
796
860
  return true if lexical_sites.singleton_local_def?(call_node)
797
- return false unless kind == :instance
798
861
 
799
- # ADR-46 — the answer below is a function of every refinement of this name in the project, so the
800
- # consumer depends on the name whichever way it answers; a refine body edited in another file must
801
- # re-check it (`IncrementalSession#refinement_affected`).
802
- DependencyRecorder.read_name(:refinement, call_node.name) if DependencyRecorder.active?
803
- modules = refining_modules(scope, class_name, call_node.name)
804
- !modules.nil? && lexical_sites.refinement_active?(call_node, modules)
862
+ refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites)
805
863
  end
806
864
 
807
- # Issue #1120 — every module that refines `method_name` into `class_name` or one of its ancestors (a
808
- # refinement of `Object` reaches a `String` receiver), or nil when none does. The table is empty on a
809
- # project that refines nothing, which answers without a lookup.
810
- def refining_modules(scope, class_name, method_name)
811
- refinements = scope.discovered_refinements
812
- return nil if refinements.empty?
865
+ # Is a refinement of `call_node`'s method name into `class_name` (or an ancestor) in effect at the call?
866
+ # Issue #1120 asks it for a method the class lacks. The two argument rules ask
867
+ # {#refined_redefinition_in_effect?} instead (issue #1740). Typing the call from the refine body is #1664.
868
+ # Issue #1752 — a class-object receiver (`kind` `:singleton`) is reached by a refinement of any class or module
869
+ # on its lookup: `Class`, `Module`, `Object`, `Kernel`, `BasicObject`, or a module it extends. A refined
870
+ # singleton class (`refine X.singleton_class`) names no constant target, so its rows are class-unknown, which
871
+ # reach every receiver. Issue #1673 — the answer is derived from the file's in-effect refinements
872
+ # ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded through its
873
+ # module's includes (issue #1671).
874
+ #
875
+ # ADR-121 WD7 — a module in effect whose refinements Rigor cannot read
876
+ # ({Inference::InEffectRefinements.opaque_module?}: a gem's, one from outside the analysed paths, one with an
877
+ # unreadable `refine`) may refine any class, a class object's included, so the call declines whatever its
878
+ # receiver. Issue #1796.
879
+ def refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites)
880
+ return false if lexical_sites.nil?
881
+ return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
882
+
883
+ modules = Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name, kind)
884
+ !modules.nil? &&
885
+ lexical_sites.refinements.refinement_active?(call_node.location.start_offset, modules,
886
+ scope.declared_refinements) do |name|
887
+ Inference::InEffectRefinements.activated_modules(scope, name)
888
+ end
889
+ end
890
+
891
+ # Issues #1663, #1740 — the decline `call.wrong-arity` and `call.argument-type-mismatch` ask once a report
892
+ # would fire: a refinement that REDEFINES a method the class already has replaces the signature those rules
893
+ # check against, but Ruby finds a method the receiver's class (or a nearer ancestor) defines before it
894
+ # consults an ancestor's refinement (`refine Object do def center(a, b, c)` leaves `"x".center(1, 2, 3)` an
895
+ # `ArgumentError`). It stands on its own rather than behind {#refined_method_in_effect?}, whose ancestor
896
+ # reach declines `class Inc; def bar(a); include N; end` under `refine N do def bar(a, b, c)` (issue #1797).
897
+ #
898
+ # A call with nothing in effect is checked. Otherwise it reports only from positive knowledge: the list
899
+ # carries no unknown marker and no opaque module, and {Inference::RefinedDispatch::OwnMethod} proves the
900
+ # receiver's own definer answers first. Issue #1752 — a class-object receiver (`kind` `:singleton`) walks its
901
+ # singleton levels: `Integer.sqrt(1, 2)` under `refine Object do def sqrt(a, b)` runs `Integer.sqrt` and raises.
902
+ def refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
903
+ return false if lexical_sites.nil?
813
904
 
814
- modules = nil
815
- refinements.each do |refined, methods|
816
- names = methods[method_name]
817
- next if names.nil? || !refined_receiver_class?(scope, class_name, refined)
905
+ list = refinement_list_at(call_node, scope, lexical_sites)
906
+ return false if list.empty?
907
+ return true if list.include?(Inference::InEffectRefinements::UNKNOWN)
908
+ return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
818
909
 
819
- (modules ||= []).concat(names)
910
+ !Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list, kind: kind)
911
+ end
912
+
913
+ # The in-effect list at the call as the typer reads it: the lexical activations, each `using` expanded,
914
+ # then the plugin-declared modules (#1667).
915
+ def refinement_list_at(call_node, scope, lexical_sites)
916
+ query = lexical_sites.refinements
917
+ declared = scope.declared_refinements
918
+ return Inference::InEffectRefinements::EMPTY if query.empty? && declared.empty?
919
+
920
+ query.at(call_node.location.start_offset, declared) do |name|
921
+ Inference::InEffectRefinements.activated_modules(scope, name)
820
922
  end
821
- modules
822
923
  end
823
924
 
824
- def refined_receiver_class?(scope, class_name, refined)
825
- return true if class_name == refined
925
+ # ADR-121 WD7 — does the call's in-effect list (its lexical activations, each `using` expanded; a
926
+ # plugin-declared module is the plugin's declaration and never opaque) hold an opaque module?
927
+ def opaque_refinement_in_effect?(call_node, scope, lexical_sites)
928
+ query = lexical_sites.refinements
929
+ return false if query.empty?
826
930
 
827
- environment = scope.environment
828
- !environment.nil? && environment.class_ordering(class_name, refined) == :subclass
931
+ list = Inference::InEffectRefinements.lexical_list(scope, query, call_node.location.start_offset)
932
+ Inference::InEffectRefinements.opaque_in?(scope, list)
829
933
  end
830
934
 
831
935
  # The probes that run only once every cheaper answer has come back "absent", kept together
@@ -858,10 +962,25 @@ module Rigor
858
962
  return true if module_mixin_receiver?(receiver_type, scope)
859
963
  return true if mixin_self_class_receiver?(call_node, scope)
860
964
  return true if unknown_mixin_includer?(class_name, scope)
965
+ return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
861
966
 
862
967
  ancestry_declares_method?(scope, class_name, call_node.name, kind)
863
968
  end
864
969
 
970
+ # Issue #1697 — a module the project mixes into `Object` (a top-level `include M`, or `include M` in
971
+ # `class Object`) gives every object its instance methods, and the RBS lookup above only saw core's
972
+ # `Object`. A class object is an `Object` whatever class it is, so the singleton side always reaches
973
+ # it; the instance side does when the receiver's class descends from `Object`. A mixed-in module
974
+ # whose surface is unknown, or a `module_function` reached only privately, does not count here
975
+ # ({Inference::ObjectMixins.may_define?}).
976
+ def object_mixin_may_define?(receiver_type, class_name, method_name, scope)
977
+ unless receiver_type.is_a?(Type::Singleton) || Inference::ObjectMixins.reaches_object?(scope, class_name)
978
+ return false
979
+ end
980
+
981
+ Inference::ObjectMixins.may_define?(scope, method_name, receiver: :explicit)
982
+ end
983
+
865
984
  # Issue #739 — the singleton twin of the module-mixin receiver, and the only shape that reaches the
866
985
  # singleton side legitimately. `self.class` inside a mixin's instance method is the INCLUDER's class
867
986
  # at runtime; the engine types it `Singleton[<the module>]`, one hop along from the same wrong
@@ -999,6 +1118,15 @@ module Rigor
999
1118
  # 3. The standard `Kernel` / `Object` private-method
1000
1119
  # surface (`puts`, `p`, `require`, `loop`, `raise`,
1001
1120
  # …) drawn from the loaded RBS environment.
1121
+ # 4. The private singleton methods of the top-level `main`
1122
+ # object (`using`, `include`, `public`, `private`,
1123
+ # `define_method`), read from RBS core's
1124
+ # `RBS::Unnamed::TopLevelSelfClass`, plus
1125
+ # `ruby2_keywords` (issue #1383).
1126
+ # 5. The instance methods of a module the project mixes
1127
+ # into `Object` — a top-level `include M`, or an
1128
+ # `include M` in `class Object` — or any call at all
1129
+ # when such a module's surface is unknown (issue #1697).
1002
1130
  #
1003
1131
  # The rule deliberately does NOT generalise to
1004
1132
  # implicit-self calls inside `def` / `class` / `module`
@@ -1010,7 +1138,7 @@ module Rigor
1010
1138
  # Authored severity is `:warning`; the severity profile
1011
1139
  # remaps it (`strict` → `:error`, `balanced` →
1012
1140
  # `:warning`, `lenient` → `:off` / suppressed).
1013
- def unresolved_toplevel_diagnostic(path, call_node, scope_index, eval_ranges = nil)
1141
+ def unresolved_toplevel_diagnostic(path, call_node, scope_index, eval_ranges = nil, lexical_sites = nil)
1014
1142
  return nil unless call_node.receiver.nil?
1015
1143
 
1016
1144
  scope = scope_index[call_node]
@@ -1021,15 +1149,38 @@ module Rigor
1021
1149
  return nil if scope.top_level_def_for(name)
1022
1150
  return nil if source_declared_method?(scope, "Object", name, :instance)
1023
1151
  return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
1152
+ return nil if main_singleton_method?(name, scope)
1153
+ return nil if Inference::ObjectMixins.may_define?(scope, name, receiver: :implicit)
1024
1154
  # `Target.class_eval { def added = 1; def use_added = added }` files the defs on Target
1025
1155
  # but leaves the eval body with a nil `self_type`, so `toplevel?` still holds. An eval
1026
1156
  # body is morally a class body, so ADR-34 stays silent there — including on a genuinely
1027
1157
  # undefined name inside the block. Ranges are computed once per file.
1028
1158
  return nil if call_inside_receiver_eval_ranges?(eval_ranges, call_node)
1159
+ # ADR-121 WD7 — a refinement of `Object` (or one Rigor cannot read) in effect here may define the name.
1160
+ return nil if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
1029
1161
 
1030
1162
  build_unresolved_toplevel_diagnostic(path, call_node)
1031
1163
  end
1032
1164
 
1165
+ # Issue #1383 — RBS core declares `main`'s private singleton methods on
1166
+ # `RBS::Unnamed::TopLevelSelfClass`, so the set follows RBS rather than a hand-kept
1167
+ # list. CRuby 4.0's `main` also has `ruby2_keywords`, which RBS does not declare.
1168
+ # rbs 3.x has no `TopLevelSelfClass`, so there only `ruby2_keywords` is covered.
1169
+ # A call inside a top-level block (`describe do include M end`) is judged here too;
1170
+ # silencing it is the cheaper error.
1171
+ MAIN_SINGLETON_CLASS_NAME = "RBS::Unnamed::TopLevelSelfClass"
1172
+ MAIN_SINGLETON_METHODS_MISSING_FROM_RBS = %i[ruby2_keywords].freeze
1173
+ private_constant :MAIN_SINGLETON_CLASS_NAME, :MAIN_SINGLETON_METHODS_MISSING_FROM_RBS
1174
+
1175
+ def main_singleton_method?(name, scope)
1176
+ return true if MAIN_SINGLETON_METHODS_MISSING_FROM_RBS.include?(name)
1177
+ # `TopLevelSelfClass < Object`: when the project's own RBS broke `Object`, building it would only
1178
+ # fail again and report a second, unfixable class name beside `Object` (#696's report).
1179
+ return false if Rigor::Reflection.instance_definition("Object", scope: scope).nil?
1180
+
1181
+ !Rigor::Reflection.instance_method_definition(MAIN_SINGLETON_CLASS_NAME, name, scope: scope).nil?
1182
+ end
1183
+
1033
1184
  # ScopeIndexer keeps RECEIVER_EVAL_CALLS private; this list is the same family
1034
1185
  # (`class_eval` / `module_eval` / `class_exec` / `module_exec` / `instance_eval` /
1035
1186
  # `instance_exec`). Used only to decline ADR-34 inside those blocks.
@@ -1305,8 +1456,11 @@ module Rigor
1305
1456
  # - ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound, so firing
1306
1457
  # undefined-method against it is a false positive by construction.
1307
1458
  # - Issue #653 — a plugin answered this call site ({#plugin_typed_call?}).
1459
+ # - Issue #1717 — an explicit `self` receiver in a block whose `self` is unmodelled
1460
+ # ({#unknown_block_self_receiver?}).
1308
1461
  def call_site_exempt?(call_node, scope)
1309
- inferred_param_receiver?(call_node, scope) || plugin_typed_call?(call_node, scope)
1462
+ inferred_param_receiver?(call_node, scope) || plugin_typed_call?(call_node, scope) ||
1463
+ unknown_block_self_receiver?(call_node, scope)
1310
1464
  end
1311
1465
 
1312
1466
  # Issue #653 — true when a plugin's `dynamic_return` supplied the return type for THIS call node
@@ -1568,7 +1722,7 @@ module Rigor
1568
1722
  # when the call's receiver / RBS coverage / call shape
1569
1723
  # disqualifies the rule.
1570
1724
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1571
- def wrong_arity_diagnostic(path, call_node, scope_index)
1725
+ def wrong_arity_diagnostic(path, call_node, scope_index, lexical_sites = nil)
1572
1726
  return nil if call_node.receiver.nil?
1573
1727
  return nil unless plain_positional_call?(call_node)
1574
1728
 
@@ -1609,11 +1763,18 @@ module Rigor
1609
1763
  source_arity&.settle_by_definitions
1610
1764
  return nil
1611
1765
  end
1766
+ return nil if arity_report_withheld?(source_arity, class_name, call_node, scope, kind, lexical_sites)
1767
+
1768
+ build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1769
+ end
1770
+
1771
+ # The declines `call.wrong-arity` asks only once the arity is out of the envelope.
1772
+ def arity_report_withheld?(source_arity, class_name, call_node, scope, kind, lexical_sites)
1612
1773
  # Issue #992 — a source envelope's remaining declines can only withhold, so they run only now.
1613
1774
  source_arity&.settle_by_walk
1614
- return nil if source_arity && !source_arity.authoritative?(class_name)
1775
+ return true if source_arity && !source_arity.authoritative?(class_name)
1615
1776
 
1616
- build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1777
+ refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
1617
1778
  end
1618
1779
 
1619
1780
  # The `[min, max]` the call is checked against — a declared signature's, or (issue #992) the project
@@ -1813,7 +1974,7 @@ module Rigor
1813
1974
  # and union receivers where every member already
1814
1975
  # disqualifies the call (avoid duplicating the
1815
1976
  # undefined-method diagnostic).
1816
- def nil_receiver_diagnostic(path, call_node, scope_index)
1977
+ def nil_receiver_diagnostic(path, call_node, scope_index, lexical_sites = nil)
1817
1978
  return nil if call_node.receiver.nil?
1818
1979
  # Safe-navigation calls (`recv&.method`) already
1819
1980
  # short-circuit on nil at runtime, so a nil-bearing
@@ -1862,10 +2023,22 @@ module Rigor
1862
2023
  return nil unless Rigor::Reflection.rbs_class_known?("NilClass", scope: scope)
1863
2024
 
1864
2025
  return nil unless nil_bearing_union_witnesses?(receiver_type, call_node.name, scope)
2026
+ return nil if nil_receiver_withheld?(call_node, receiver_type, scope, scope_index, lexical_sites)
1865
2027
 
1866
2028
  build_nil_receiver_diagnostic(path, call_node)
1867
2029
  end
1868
2030
 
2031
+ # The declines `call.possible-nil-receiver` asks only once a report would fire.
2032
+ def nil_receiver_withheld?(call_node, receiver_type, scope, scope_index, lexical_sites)
2033
+ # Issue #1703 — a `nil` that only a `key?` guard on the same receiver and key rules out. It re-walks the file
2034
+ # once with guards on, and only to withhold this report.
2035
+ return true if Inference::KeyPresenceGuard.withholds_nil?(call_node, receiver_type, lexical_sites&.root,
2036
+ scope_index)
2037
+
2038
+ # ADR-121 WD7 — a refinement of `NilClass` (or one Rigor cannot read) in effect may define the name.
2039
+ refined_method_in_effect?("NilClass", call_node, scope, :instance, lexical_sites)
2040
+ end
2041
+
1869
2042
  # The receiver-type half of the rule, factored out of the node-shape
1870
2043
  # guards above: the union must carry nil, must carry a non-nil arm the
1871
2044
  # presence question can be asked of, must support the method on every
@@ -1914,7 +2087,13 @@ module Rigor
1914
2087
  # arm restores the polarity without touching the shared helper:
1915
2088
  # `String | nil` keeps firing, and so does `Dynamic | String | nil` —
1916
2089
  # the nameless arm stays permissive inside the all-arms check, which is
1917
- # only about the arms' method surface.
2090
+ # only about the arms' method surface. That union no longer arises from
2091
+ # a merge re-joining the narrowed edges of one untyped local, which
2092
+ # binds it back to `Dynamic[top]` (issue #1840,
2093
+ # `Inference::UntypedRejoin`); it still does when the program writes
2094
+ # the `nil` (`x = nil if c`) and no `nil?` edge strips it before the
2095
+ # re-joining merge, or when the arms are not a narrowing of one
2096
+ # untyped value.
1918
2097
  def union_has_nameable_non_nil_arm?(union, scope)
1919
2098
  union.members.any? do |m|
1920
2099
  next false if nil_member?(m)
@@ -1957,7 +2136,9 @@ module Rigor
1957
2136
 
1958
2137
  # Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
1959
2138
  # the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
1960
- ancestry_declares_method?(scope, class_name, method_name, :instance)
2139
+ return true if ancestry_declares_method?(scope, class_name, method_name, :instance)
2140
+
2141
+ object_mixin_may_define?(member, class_name, method_name, scope)
1961
2142
  end
1962
2143
 
1963
2144
  def nil_class_has_method?(method_name, scope)
@@ -1984,7 +2165,7 @@ module Rigor
1984
2165
  # `possible-nil-receiver` rule, safe-navigation, and ADR-58
1985
2166
  # declaration-sourced nil. Slice 1 handles pure non-nil unions
1986
2167
  # (e.g. `String | Symbol`).
1987
- def union_undefined_method_diagnostic(path, call_node, receiver_type, scope)
2168
+ def union_undefined_method_diagnostic(path, call_node, receiver_type, scope, lexical_sites = nil)
1988
2169
  return nil unless receiver_type.is_a?(Type::Union)
1989
2170
  return nil if call_node.safe_navigation?
1990
2171
 
@@ -2006,10 +2187,45 @@ module Rigor
2006
2187
  # `.pack`). Require at least two distinct arm classes.
2007
2188
  return nil if members.map { |member| concrete_class_name(member) }.uniq.size < 2
2008
2189
  return nil if members.any? { |member| method_present_anywhere?(member, call_node.name, scope) }
2190
+ return nil if refined_on_some_arm?(members, call_node, scope, lexical_sites)
2009
2191
 
2010
2192
  build_undefined_method_diagnostic(path, call_node, receiver_type)
2011
2193
  end
2012
2194
 
2195
+ # ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
2196
+ def refined_on_some_arm?(members, call_node, scope, lexical_sites)
2197
+ members.any? do |member|
2198
+ kind = member.is_a?(Type::Singleton) ? :singleton : :instance
2199
+ refined_method_in_effect?(concrete_class_name(member), call_node, scope, kind, lexical_sites)
2200
+ end
2201
+ end
2202
+
2203
+ # Issue #1699 — a union of one class's literals, alone or beside that class's plain nominal
2204
+ # (`0 | Integer`, which `gets.to_i` types; `"a" | "b"`; `1 | 2`), holds nothing but instances of
2205
+ # that class, so it answers a method-existence question exactly as the class does. The union rule
2206
+ # above declines it through its distinct-class guard, which exists for SHAPE joins
2207
+ # (`Hash[K1, V1] | Hash[K2, V2]`, a corpus misinference on mail), and the scalar rule declined it
2208
+ # for having no single concrete class, so neither judged it. Returns that class's nominal, or `type`
2209
+ # itself. Literal-and-plain-nominal arms only: a generic, shape or refinement arm keeps the union
2210
+ # declined, and so does a union spanning two classes.
2211
+ def single_class_literal_union(type)
2212
+ return type unless type.is_a?(Type::Union)
2213
+
2214
+ members = type.members
2215
+ return type unless members.any?(Type::Constant)
2216
+ return type unless members.all? do |member|
2217
+ member.is_a?(Type::Constant) || (member.is_a?(Type::Nominal) && member.type_args.empty?)
2218
+ end
2219
+
2220
+ class_names = members.map { |member| concrete_class_name(member) }.uniq
2221
+ return type unless class_names.size == 1
2222
+
2223
+ class_name = class_names.first
2224
+ return type if class_name.nil? || class_name == "NilClass"
2225
+
2226
+ Type::Combinator.nominal_of(class_name)
2227
+ end
2228
+
2013
2229
  # An arm that makes a sound "undefined on every arm" verdict impossible: a non-class surface
2014
2230
  # (Dynamic / Top / Bot), a singleton (slice 1 reasons about instance arms only), the generic
2015
2231
  # metaclass `Class` / `Module` (a value typed as one is *some* class/module object whose singleton
@@ -2150,9 +2366,13 @@ module Rigor
2150
2366
  INTEGER_RAISING_OPERATORS = %i[/ % div modulo divmod].freeze
2151
2367
  private_constant :INTEGER_RAISING_OPERATORS
2152
2368
 
2153
- def always_raises_diagnostic(path, call_node, scope_index)
2369
+ def always_raises_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2154
2370
  return nil unless integer_zero_division?(call_node, scope_index)
2155
2371
 
2372
+ # ADR-121 WD7 — a refinement of `Integer`'s operator (or one Rigor cannot read) in effect may not raise.
2373
+ scope = scope_index[call_node]
2374
+ return nil if refined_method_in_effect?("Integer", call_node, scope, :instance, lexical_sites)
2375
+
2156
2376
  build_always_raises_diagnostic(path, call_node)
2157
2377
  end
2158
2378
 
@@ -2493,7 +2713,7 @@ module Rigor
2493
2713
  names = [method_name, *GLOBAL_WRITE_HATCHES]
2494
2714
  census = global_write_census(scope)
2495
2715
  program_may_answer?(ancestors, names, census, scope) ||
2496
- (method_name == :write && refined_writer_in_effect?(census, node, lexical_sites))
2716
+ (method_name == :write && refined_writer_in_effect?(census, node, scope, lexical_sites))
2497
2717
  end
2498
2718
 
2499
2719
  def overridden_hatch?(definition)
@@ -2549,9 +2769,10 @@ module Rigor
2549
2769
  # refine(T)`), so a refined `write` on any class declines every literal. `respond_to?(:write)` honours an
2550
2770
  # active refinement on Ruby 4.0.5; a refined `respond_to_missing?` or `respond_to?`, and every implicit
2551
2771
  # conversion, do not, so a refinement that adds only those leaves the write reported.
2552
- def refined_writer_in_effect?(census, node, lexical_sites)
2772
+ def refined_writer_in_effect?(census, node, scope, lexical_sites)
2553
2773
  Inference::GlobalWriteCensus.refines_write?(census) &&
2554
- (lexical_sites.nil? || lexical_sites.using_in_effect?(node))
2774
+ (lexical_sites.nil? || !scope.declared_refinements.empty? ||
2775
+ lexical_sites.refinements.any_at?(node.location.start_offset))
2555
2776
  end
2556
2777
 
2557
2778
  def build_global_write_type_diagnostic(path, node, contract, class_name)
@@ -2634,7 +2855,7 @@ module Rigor
2634
2855
  # - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
2635
2856
  # is what the call reaches; the class's own private `def` answers only when nothing prepended
2636
2857
  # defines the name ({#reached_definer_visibility}).
2637
- def visibility_mismatch_diagnostic(path, call_node, scope_index)
2858
+ def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2638
2859
  return nil unless explicit_non_self_receiver?(call_node.receiver)
2639
2860
 
2640
2861
  scope = scope_index[call_node]
@@ -2653,6 +2874,8 @@ module Rigor
2653
2874
  reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
2654
2875
  return nil if reached == :undecided
2655
2876
  return nil unless reached.nil? || reached == :private
2877
+ # ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may redefine the method, public.
2878
+ return nil if refined_method_in_effect?(receiver_type.class_name, call_node, scope, :instance, lexical_sites)
2656
2879
 
2657
2880
  build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
2658
2881
  end
@@ -2905,7 +3128,7 @@ module Rigor
2905
3128
  COERCE_DISPATCH_METHODS = %i[+ - * / % ** & | ^ << >> < > <= >=].to_set.freeze
2906
3129
  private_constant :COERCE_DISPATCH_METHODS
2907
3130
 
2908
- def argument_type_diagnostic(path, call_node, scope_index)
3131
+ def argument_type_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2909
3132
  return nil if call_node.receiver.nil?
2910
3133
  return nil if UNIVERSAL_EQUALITY_METHODS.include?(call_node.name)
2911
3134
  return nil unless plain_positional_call?(call_node)
@@ -2932,11 +3155,19 @@ module Rigor
2932
3155
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
2933
3156
  mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, scope_index)
2934
3157
  return nil if mismatch.nil?
2935
- return nil if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
3158
+ return nil if argument_report_withheld?(receiver_type, class_name, call_node, mismatch, scope, lexical_sites)
2936
3159
 
2937
3160
  build_argument_type_diagnostic(path, call_node, class_name, mismatch)
2938
3161
  end
2939
3162
 
3163
+ # The declines `call.argument-type-mismatch` asks only once an argument mismatches.
3164
+ def argument_report_withheld?(receiver_type, class_name, call_node, mismatch, scope, lexical_sites)
3165
+ return true if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
3166
+
3167
+ kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
3168
+ refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
3169
+ end
3170
+
2940
3171
  # ADR-67 WD6b — an argument-type-mismatch verdict resting on an open-call-site lower bound, on
2941
3172
  # either side of the call. The ARGUMENT side: the mismatching argument is an inferred-parameter
2942
3173
  # local, so firing against it is an FP by construction. The RECEIVER side: when the receiver roots
@@ -3738,6 +3969,7 @@ module Rigor
3738
3969
  # silent.
3739
3970
  def override_visibility_diagnostic(path, def_node, scope_index)
3740
3971
  return nil unless def_node.receiver.nil? # instance methods only
3972
+ return nil if IMPLICITLY_PRIVATE_METHODS.include?(def_node.name)
3741
3973
 
3742
3974
  scope = scope_index[def_node]
3743
3975
  return nil if scope.nil?
@@ -3869,6 +4101,7 @@ module Rigor
3869
4101
  # fire) when any gate is unmet.
3870
4102
  def resolve_authored_override(def_node, scope_index)
3871
4103
  return nil unless def_node.receiver.nil? # instance methods only (singleton: follow-on)
4104
+ return nil if LIFECYCLE_HOOKS.include?(def_node.name) # ADR-35 WD10: outside substitutability
3872
4105
 
3873
4106
  scope = scope_index[def_node]
3874
4107
  return nil if scope.nil?