rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -12,11 +12,13 @@ require_relative "../type"
12
12
  require_relative "diagnostic"
13
13
  require_relative "dependency_recorder"
14
14
  require_relative "check_rules/rule_ids"
15
+ require_relative "check_rules/call_facts"
15
16
  require_relative "check_rules/inferred_param_guard"
16
17
  require_relative "check_rules/declaration_sourced_guard"
17
18
  require_relative "check_rules/published_constant_guard"
18
19
  require_relative "check_rules/dead_version_guard_arms"
19
20
  require_relative "check_rules/rule_walk"
21
+ require_relative "check_rules/always_raises_shield"
20
22
  require_relative "check_rules/always_truthy_condition_collector"
21
23
  require_relative "check_rules/unreachable_clause_collector"
22
24
  require_relative "check_rules/shadowed_rescue_collector"
@@ -33,7 +35,8 @@ require_relative "check_rules/rebinding_block_callees"
33
35
  require_relative "check_rules/rbs_receiver_surface"
34
36
  require_relative "check_rules/special_global_setters"
35
37
  require_relative "check_rules/ruby_deprecations"
36
- require_relative "../run_scoped_slots"
38
+ require_relative "check_rules/plugin_api_surface"
39
+ require_relative "../typing_state"
37
40
 
38
41
  module Rigor
39
42
  module Analysis
@@ -110,7 +113,8 @@ module Rigor
110
113
  diagnostics = collectors[:main_pass].results.dup
111
114
  diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
112
115
  COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
113
- diagnostics.concat(send(builder, path, collectors[role].results))
116
+ collector = OPTIONAL_COLLECTOR_ROLES.include?(role) ? collectors[role] : collectors.fetch(role)
117
+ diagnostics.concat(send(builder, path, collector.results)) if collector
114
118
  end
115
119
  # ADR-47 WD5 — the dead arm of a decidable version guard (#627) reports nothing: it cannot run on
116
120
  # the Ruby being checked with, so a diagnostic there is a false positive. Applied before
@@ -141,18 +145,25 @@ module Rigor
141
145
  ivar_writes: :ivar_write_mismatch_diagnostics,
142
146
  dead_assignments: :dead_assignment_diagnostics,
143
147
  duplicate_hash_keys: :duplicate_hash_key_diagnostics,
144
- return_in_ensure: :return_in_ensure_diagnostics
148
+ return_in_ensure: :return_in_ensure_diagnostics,
149
+ plugin_api_surface: :plugin_api_surface_diagnostics
145
150
  }.freeze
146
151
  private_constant :COLLECTOR_DIAGNOSTIC_BUILDERS
147
152
 
153
+ # The roles {.build_node_collectors} builds only on request: `plugin_api_surface`, when the rule is active
154
+ # ({PluginApiSurface.active?}). {.diagnose} skips an absent optional role; every other role is required, and a
155
+ # missing one raises.
156
+ OPTIONAL_COLLECTOR_ROLES = %i[plugin_api_surface].freeze
157
+
148
158
  # The verbatim per-node dispatch of the former inline main pass
149
159
  # (`diagnose`'s `Source::NodeWalker.each` `case`), now invoked by
150
160
  # {MainPassCollector} on the shared {RuleWalk}. Returns the
151
161
  # diagnostics for one node, in the same emission order as before.
152
- def main_pass_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
162
+ def main_pass_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil,
163
+ raise_shield = nil)
153
164
  case node
154
165
  when Prism::CallNode
155
- call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites)
166
+ call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
156
167
  when Prism::DefNode
157
168
  [
158
169
  refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites),
@@ -174,7 +185,8 @@ module Rigor
174
185
  # check in the body stands. The refine-body test runs only once a mismatch would report, so a file pays the
175
186
  # {LexicalMethodSites} walk only then.
176
187
  def refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites)
177
- diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
188
+ replaced = lexical_sites&.replaced_singleton_defs(scope_index)
189
+ diagnostic = return_type_mismatch_diagnostic(path, node, scope_index, replaced)
178
190
  return diagnostic if diagnostic.nil? || lexical_sites.nil?
179
191
 
180
192
  lexical_sites.refinements.refinement_def?(node) ? nil : diagnostic
@@ -187,11 +199,17 @@ module Rigor
187
199
  # traversal, and hand the populated set back to {.diagnose} as
188
200
  # `node_collectors:`. The main pass needs `path` because its per-node
189
201
  # diagnostics carry it (ADR-53 B3c hosts it on the same walk).
190
- def build_node_collectors(path, scope_index, root = nil)
202
+ #
203
+ # `plugin_api_surface:` adds the `plugin.api-surface` collector, which the runner asks for only when the rule
204
+ # resolves to a reporting severity ({PluginApiSurface.active?}).
205
+ def build_node_collectors(path, scope_index, root = nil, plugin_api_surface: false)
191
206
  eval_ranges = receiver_eval_block_ranges(root)
192
207
  lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
193
- main_pass = ->(node) { main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites) }
194
- {
208
+ raise_shield = root && AlwaysRaisesShield.new(root)
209
+ main_pass = lambda do |node|
210
+ main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
211
+ end
212
+ collectors = {
195
213
  main_pass: MainPassCollector.new(main_pass),
196
214
  void_value_use: VoidValueUseCollector.new(scope_index),
197
215
  always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
@@ -202,6 +220,8 @@ module Rigor
202
220
  duplicate_hash_keys: DuplicateHashKeyCollector.new(scope_index),
203
221
  return_in_ensure: ReturnInEnsureCollector.new(scope_index)
204
222
  }
223
+ collectors[:plugin_api_surface] = PluginApiSurface::Collector.new(scope_index) if plugin_api_surface
224
+ collectors
205
225
  end
206
226
 
207
227
  # A {RuleWalk::CollectorDriver} over a built-in collector set, for a
@@ -279,8 +299,11 @@ module Rigor
279
299
  diagnostics = []
280
300
  eval_ranges = receiver_eval_block_ranges(root)
281
301
  lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
302
+ raise_shield = AlwaysRaisesShield.new(root)
282
303
  Source::NodeWalker.each(root) do |node|
283
- diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites))
304
+ diagnostics.concat(
305
+ main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
306
+ )
284
307
  end
285
308
  diagnostics
286
309
  end
@@ -300,18 +323,22 @@ module Rigor
300
323
  shadow_verify_node_collectors(path, root, scope_index, collectors)
301
324
  end
302
325
 
303
- def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
326
+ # Issue #2786 — the rules that type the call's receiver read it from one {CallFacts}, so the receiver chain is
327
+ # typed once per call node rather than once per rule.
328
+ def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil,
329
+ raise_shield = nil)
330
+ facts = CallFacts.new(node, scope_index)
304
331
  [
305
- undefined_method_diagnostic(path, node, scope_index, lexical_sites),
332
+ undefined_method_diagnostic(path, node, facts, lexical_sites),
306
333
  unresolved_toplevel_diagnostic(path, node, scope_index, eval_ranges, lexical_sites),
307
- wrong_arity_diagnostic(path, node, scope_index, lexical_sites),
308
- argument_type_diagnostic(path, node, scope_index, lexical_sites),
309
- nil_receiver_diagnostic(path, node, scope_index, lexical_sites),
334
+ wrong_arity_diagnostic(path, node, facts, lexical_sites),
335
+ argument_type_diagnostic(path, node, facts, lexical_sites),
336
+ nil_receiver_diagnostic(path, node, facts, lexical_sites),
310
337
  dump_type_diagnostic(path, node, scope_index),
311
338
  assert_type_diagnostic(path, node, scope_index),
312
- always_raises_diagnostic(path, node, scope_index, lexical_sites),
339
+ always_raises_diagnostic(path, node, facts, lexical_sites, raise_shield),
313
340
  raise_non_exception_diagnostic(path, node, scope_index),
314
- visibility_mismatch_diagnostic(path, node, scope_index, lexical_sites)
341
+ visibility_mismatch_diagnostic(path, node, facts, lexical_sites)
315
342
  ].compact
316
343
  end
317
344
 
@@ -407,6 +434,11 @@ module Rigor
407
434
  end
408
435
  end
409
436
 
437
+ # ADR-128 WD6 / ADR-130 slice 2 — `plugin.api-surface`, from its collector's results.
438
+ def plugin_api_surface_diagnostics(path, results)
439
+ PluginApiSurface.diagnostics(path, results)
440
+ end
441
+
410
442
  # ADR-100 WD2 — `static.value-use.void`. Its value-context slot inspection rides the shared per-node
411
443
  # {RuleWalk} like the other collectors, so the file is not re-traversed for it. Each result is a
412
444
  # value-context use of a call whose author-declared `-> void` return the engine recovered to `top`.
@@ -740,10 +772,10 @@ module Rigor
740
772
  class << self
741
773
  private
742
774
 
743
- def undefined_method_diagnostic(path, call_node, scope_index, lexical_sites = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
775
+ def undefined_method_diagnostic(path, call_node, facts, lexical_sites = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
744
776
  return nil if call_node.receiver.nil?
745
777
 
746
- scope = scope_index[call_node]
778
+ scope = facts.scope
747
779
  return nil if scope.nil?
748
780
 
749
781
  # The exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
@@ -776,7 +808,7 @@ module Rigor
776
808
  # below — preserving that keeps `&.` from newly firing on the
777
809
  # non-nil constituent (which, for a cross-file project def, would
778
810
  # be a working-code false positive).
779
- receiver_type = safe_navigation_receiver(call_node, scope)
811
+ receiver_type = safe_navigation_receiver(call_node, facts.receiver_type)
780
812
 
781
813
  # #320 — the private-singleton-object idiom (`class << Merger = Object.new`). The body's methods
782
814
  # are recorded on the constant's own name, but the receiver reads back as `Object`, so the
@@ -973,16 +1005,268 @@ module Rigor
973
1005
  call_node: call_node)
974
1006
  return true if module_mixin_receiver?(receiver_type, scope)
975
1007
  return true if mixin_self_class_receiver?(call_node, scope)
976
- return true if unknown_mixin_includer?(class_name, scope)
1008
+ return true if unknown_mixin_includer?(class_name, scope, kind)
977
1009
  return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
978
1010
  return true if ancestry_declares_method?(scope, class_name, call_node.name, kind)
1011
+ return true if open_names_through_ancestors?(scope, class_name, kind)
1012
+ return true if foreign_singleton_through_ancestors?(scope, class_name, call_node.name, kind)
979
1013
 
980
1014
  rbs_ancestor_may_answer?(class_name, call_node.name, scope, kind)
981
1015
  end
982
1016
 
1017
+ # Issue #2793 — the class, or a project class or module on its chain, carries
1018
+ # `Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK`: a macro there defines methods under names its syntax
1019
+ # does not spell (`def_delegators :@x, *NAMES`), so no name is provably missing. A chain cut at its budget
1020
+ # never reaches here: {#ancestry_declares_method?} reads the same chain first and declines on the cut.
1021
+ # A non-literal `define_method` records its own side's twin of the mark instead, read beside it here.
1022
+ OPEN_NAMES_MARKS = {
1023
+ instance: [Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK,
1024
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK].freeze,
1025
+ singleton: [Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK,
1026
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK].freeze
1027
+ }.freeze
1028
+ private_constant :OPEN_NAMES_MARKS
1029
+
1030
+ def open_names_through_ancestors?(scope, class_name, kind)
1031
+ side = kind == :singleton ? :singleton : :instance
1032
+ marks = OPEN_NAMES_MARKS.fetch(side)
1033
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, :methods)
1034
+ # A project class with no `def` of its own is held as an external entry (#2094), so an external's
1035
+ # candidate names are asked too.
1036
+ found = chain.search(scope) do |entry|
1037
+ names = entry.external? ? entry.candidates : [entry.name]
1038
+ names.any? do |name|
1039
+ bucket = scope.parameter_envelopes_of(name)
1040
+ marks.any? { |mark| bucket.key?(mark) } ||
1041
+ (side == :instance && (computed_definer_called?(scope, name, bucket) ||
1042
+ hook_block_macro_defines?(scope, name)))
1043
+ end
1044
+ end
1045
+ !found.nil?
1046
+ end
1047
+
1048
+ # Issue #3018 — a class object's singleton method that `def Foo.x`, `Foo.define_singleton_method(:x)` or a
1049
+ # `Foo.singleton_class.class_eval` def defines outside `Foo`'s body records no `:singleton` row, only an
1050
+ # envelope entry. The class-object path reads the definer's own entry
1051
+ # (`Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND`) on the chain's class-object entries, the class and
1052
+ # its superclasses, so a subclass inherits it. An extended module's entry is instance-side: `extend M` adds
1053
+ # `M`'s instance methods, not a `def M.x`. It does not read the opaque `[:singleton, x]` entry beside it, which
1054
+ # a name-rewriting class-body macro (`my_attr :x`) files too without defining `x` on the class object.
1055
+ def foreign_singleton_through_ancestors?(scope, class_name, method_name, kind)
1056
+ return false unless kind == :singleton
1057
+
1058
+ key = [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name.to_sym]
1059
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
1060
+ found = chain.search(scope, side: :singleton) do |entry|
1061
+ names = entry.external? ? entry.candidates : [entry.name]
1062
+ names.any? { |name| scope.parameter_envelopes_of(name).key?(key) }
1063
+ end
1064
+ !found.nil?
1065
+ end
1066
+
1067
+ # Issue #2901 — the body of `name` calls, on `self`, a method its singleton chain resolves to a computed
1068
+ # definer (`Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND`): a class macro of its own, a
1069
+ # superclass's or an extended module's, which runs `define_method` under a name no table holds. The first
1070
+ # entry on that chain carrying the macro's definer entry answers; a closer override that defines nothing is
1071
+ # not looked for, so it leaves the class open.
1072
+ #
1073
+ # Issue #2975 — the definer entry carries its side: the class's own singleton entry answers with a singleton
1074
+ # method (`def self.my_attr`), an extended module's entry with an instance method, so an instance `def my_attr`
1075
+ # of the class itself opens nothing. A macro patched onto `Module` (or `Class`, `Object`, `Kernel`,
1076
+ # `BasicObject`) as an instance method is on every class object's singleton chain past its own classes.
1077
+ def computed_definer_called?(scope, name, bucket)
1078
+ macros = bucket.each_key.filter_map do |key|
1079
+ key.last if key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND
1080
+ end
1081
+ return false if macros.empty?
1082
+
1083
+ chain = Scope::ResolutionChain.for(scope, name.to_s, :singleton, :methods)
1084
+ found = macros.any? do |macro|
1085
+ !chain.search(scope) { |entry| macro_entry_defines?(scope, entry, macro) }.nil? ||
1086
+ universal_macro_defines?(scope, macro)
1087
+ end
1088
+ found || hook_extended_macro_defines?(scope, name, macros)
1089
+ end
1090
+
1091
+ # Issue #2975 — a module that a class on `name`'s superclass chain includes or prepends directly, and whose
1092
+ # `included` / `prepended` hook extends its includer with a module (`def self.included(base) =
1093
+ # base.extend(ClassMethods)`), puts that module's instance methods on `name`'s singleton chain, which records no
1094
+ # edge for it. Ruby runs the hook for the direct includer only, so a module included through another module does
1095
+ # not count; the chain's levels flatten a class's transitive mixins, so each level class's own mixin list is
1096
+ # read, one hop, never followed further. Each mixin and each extended constant resolves to the first candidate
1097
+ # the project declares, the innermost cref first, whether or not RBS declares the modules; a concern's
1098
+ # `ClassMethods` resolves as `const_get` does instead (#2986).
1099
+ def hook_extended_macro_defines?(scope, name, macros)
1100
+ definers = macro_definers(macros)
1101
+ direct_hook_mixins(scope, name).any? { |mixin, _edges| hook_extension_defines?(scope, mixin, definers) }
1102
+ end
1103
+
1104
+ # Issue #2986 — an ActiveSupport::Concern's `included do … end` block runs in the class body of a class that
1105
+ # includes the concern directly, and its `prepended do … end` block in one that prepends it, so a macro call
1106
+ # in the block is read as that class's own, against the module the concern extends it with. A call in the
1107
+ # concern's own module body is no includer's: Ruby raises there as the module loads.
1108
+ #
1109
+ # Issue #2992 — Ruby runs the block in every direct includer, a module included, so the read is made for each
1110
+ # entry of the receiver's chain (`owner`) against that entry's own mixins, one hop. A concern included into a
1111
+ # concern after its `extend ActiveSupport::Concern` runs no block there: `Concern#append_features` records it as
1112
+ # a dependency, and the outer concern's final includer includes it, on that includer's own edge
1113
+ # ({#with_concern_dependencies}). Only a mixin whose one `include` / `prepend` statement is such a dependency
1114
+ # is left out of `owner`'s own read: one written ahead of the `extend`, or again in a reopening that may load
1115
+ # first, is an ordinary include there.
1116
+ def hook_block_macro_defines?(scope, owner)
1117
+ own_mixin_edges(scope, owner, deferred: true).any? do |mixin, edges|
1118
+ macros = scope.parameter_envelopes_of(mixin).each_key.filter_map do |key|
1119
+ key[2] if key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_HOOK_BODY_CALL_KIND &&
1120
+ edges.include?(Scope::ResolutionChain.effective_hook(scope.discovery, mixin, key[1]))
1121
+ end
1122
+ !macros.empty? && hook_extension_defines?(scope, mixin, macro_definers(macros))
1123
+ end
1124
+ end
1125
+
1126
+ def macro_definers(macros)
1127
+ macros.map { |macro| [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, :instance, macro] }
1128
+ end
1129
+
1130
+ # Each module a class on `name`'s superclass chain includes or prepends directly, resolved, with the hook
1131
+ # blocks its edges run: `:included` for an `include`, `:prepended` for a `prepend`; a concern's dependencies
1132
+ # follow it on the same edges.
1133
+ def direct_hook_mixins(scope, name)
1134
+ chain = Scope::ResolutionChain.for(scope, name.to_s, :instance, :methods)
1135
+ chain.record(scope) if Analysis::DependencyRecorder.active?
1136
+ chain.level_classes.compact.flat_map { |klass| own_mixin_edges(scope, klass) }
1137
+ end
1138
+
1139
+ # The modules `owner` itself includes or prepends, as {#direct_hook_mixins} gives them. With `deferred:`, a
1140
+ # provable dependency of a concern `owner` is left out ({#hook_block_macro_defines?}).
1141
+ def own_mixin_edges(scope, owner, deferred: false)
1142
+ mixins = scope.includes_of(owner)
1143
+ prepends = scope.prepends_of(owner)
1144
+ deferred_mixins = deferred ? deferred_dependencies(scope, owner, mixins) : NO_DEFERRED
1145
+ mixins.uniq.flat_map do |raw|
1146
+ mixin = declared_constant(scope, owner, raw)
1147
+ next [] unless mixin
1148
+
1149
+ edges = []
1150
+ edges << :prepended if prepends.include?(raw)
1151
+ edges << :included if mixins.count(raw) > prepends.count(raw)
1152
+ next [] if deferred_mixins.include?(raw)
1153
+
1154
+ with_concern_dependencies(scope, mixin).map { |member| [member, edges] }
1155
+ end
1156
+ end
1157
+
1158
+ NO_DEFERRED = [].freeze
1159
+ private_constant :NO_DEFERRED
1160
+
1161
+ # The spellings among `owner`'s mixins that name a concern dependency of `owner`, when `owner` is declared in
1162
+ # one file: a reopening may write the same `include` without the `extend`, and load first.
1163
+ def deferred_dependencies(scope, owner, mixins)
1164
+ sites = scope.discovery.discovered_class_sources[owner.to_s]
1165
+ return NO_DEFERRED if sites.nil? || sites.size > 1
1166
+
1167
+ resolver = Scope::ResolutionChain.resolver_for(scope, :methods)
1168
+ dependencies = Scope::ResolutionChain.concern_dependencies(scope.discovery, resolver, owner)
1169
+ return NO_DEFERRED if dependencies.empty?
1170
+
1171
+ Scope::ResolutionChain.dependency_spellings(scope.discovery, owner).select do |raw|
1172
+ mixins.include?(raw) && dependencies.include?(resolver.resolve_declared(owner, raw))
1173
+ end
1174
+ end
1175
+
1176
+ # Issue #2992 — `mixin` and, when it is a concern, the concerns it includes or prepends, at any depth through
1177
+ # concerns ({Scope::ResolutionChain.concern_dependencies}): `Concern#append_features` (or `prepend_features`)
1178
+ # includes (or prepends) each of them into the final includer ahead of `mixin` itself, on the includer's own
1179
+ # edge to `mixin`, whatever edge each was given. A reader files each one's sites as it reads its envelopes.
1180
+ def with_concern_dependencies(scope, mixin)
1181
+ resolver = Scope::ResolutionChain.resolver_for(scope, :methods)
1182
+ [mixin, *Scope::ResolutionChain.concern_dependencies(scope.discovery, resolver, mixin)]
1183
+ end
1184
+
1185
+ def hook_extension_defines?(scope, mixin, definers)
1186
+ hook_extends(scope, mixin).any? do |written, lookup|
1187
+ target = if lookup == :const_get then const_get_constant(scope, mixin, written)
1188
+ else declared_constant(scope, mixin, written)
1189
+ end
1190
+ record_hook_target_edges(scope, mixin, written, lookup) if Analysis::DependencyRecorder.active?
1191
+ target && definers.any? { |definer| scope.parameter_envelopes_of(target).key?(definer) }
1192
+ end
1193
+ end
1194
+
1195
+ # Issue #2992 — the read depends on the extended module's files whether or not it resolved: an empty
1196
+ # `module ClassMethods; end` is no project class yet, and the edit that gives it the definer makes it one. So
1197
+ # every name the lookup tried files its declaring sites, and the constant's last segment its negative class
1198
+ # edge, which a NEW file declaring it fires.
1199
+ def record_hook_target_edges(scope, mixin, written, lookup)
1200
+ candidates = if lookup == :const_get
1201
+ chain = Scope::ResolutionChain.for(scope, mixin, :instance, :methods)
1202
+ chain.entries.flat_map { |entry| entry.external? ? entry.candidates : [entry.name] }
1203
+ .map { |name| "#{name}::#{written}" } << written
1204
+ else
1205
+ own = written.start_with?("::") ? [] : ["#{mixin}::#{written}"]
1206
+ own + scope.ancestor_name_candidates(mixin, written)
1207
+ end
1208
+ candidates.each { |candidate| Scope::ResolutionChain.record_class(scope, candidate.delete_prefix("::")) }
1209
+ Analysis::DependencyRecorder.read_last_segment(:class, written)
1210
+ end
1211
+
1212
+ # The constant `raw`, written in `owner`'s body, as the first candidate the project declares: `Owner::Raw`
1213
+ # first, since `owner` is the innermost cref there, then the enclosing namespaces. Nil when none is declared.
1214
+ def declared_constant(scope, owner, raw)
1215
+ candidates = scope.ancestor_name_candidates(owner, raw)
1216
+ candidates = ["#{owner}::#{raw}", *candidates] unless raw.start_with?("::")
1217
+ candidates.find { |candidate| scope.known_user_class?(candidate) }
1218
+ end
1219
+
1220
+ # Issue #2986 — `owner.const_get(raw)`, as `Concern#append_features` looks `ClassMethods` up: `owner` and its
1221
+ # ancestors, then `Object`, never the namespaces enclosing `owner`. Issue #2997 — a `class_methods` block's
1222
+ # `Name::ClassMethods` counts only for a concern `Name` ({Scope::ResolutionChain.const_get_candidate?}).
1223
+ def const_get_constant(scope, owner, raw)
1224
+ chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
1225
+ found = chain.search(scope) do |entry|
1226
+ names = entry.external? ? entry.candidates : [entry.name]
1227
+ names.map { |name| [name, "#{name}::#{raw}"] }.find do |name, candidate|
1228
+ scope.known_user_class?(candidate) &&
1229
+ Scope::ResolutionChain.const_get_candidate?(scope.discovery, name, candidate)
1230
+ end&.last
1231
+ end
1232
+ found || (raw if scope.known_user_class?(raw))
1233
+ end
1234
+
1235
+ # The `[written, lookup]` pairs of a module's hook-extend entries; `lookup` is `:const_get` for a concern's.
1236
+ # Issue #3005 — a gated concern-block entry counts only for a concern `owner`.
1237
+ def hook_extends(scope, owner)
1238
+ scope.parameter_envelopes_of(owner).each_key.filter_map do |key|
1239
+ next unless key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND
1240
+ next unless Scope::ResolutionChain.effective_hook(scope.discovery, owner, key[2])
1241
+
1242
+ key[1, 2]
1243
+ end
1244
+ end
1245
+
1246
+ # A macro's owner holds the macro's `def`, so it is never an external entry.
1247
+ def macro_entry_defines?(scope, entry, macro)
1248
+ !entry.external? &&
1249
+ scope.parameter_envelopes_of(entry.name).key?([Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND,
1250
+ entry.side, macro])
1251
+ end
1252
+
1253
+ # The classes past a class object's own singleton chain, whose instance methods it answers. A module's chain
1254
+ # does not reach `Class`, but a module body calling a `Class` macro raises as it loads, so it is not told apart.
1255
+ UNIVERSAL_MACRO_OWNERS = %w[Class Module Object Kernel BasicObject].freeze
1256
+ private_constant :UNIVERSAL_MACRO_OWNERS
1257
+
1258
+ def universal_macro_defines?(scope, macro)
1259
+ definer = [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, :instance, macro]
1260
+ UNIVERSAL_MACRO_OWNERS.any? { |owner| scope.parameter_envelopes_of(owner).key?(definer) }
1261
+ end
1262
+
983
1263
  # Issue #1722 — a `method_missing`, or a project mixin, on one of the class's RBS ancestors
984
1264
  # ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
1265
+ # Issue #2792 — a `method_missing` RBS declares on the class or an ancestor, other than the universal one.
985
1266
  def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
1267
+ side = kind == :singleton ? :singleton : :instance
1268
+ return true if RbsReceiverSurface.rbs_missing_hook?(scope, class_name, side)
1269
+
986
1270
  if kind == :singleton
987
1271
  RbsReceiverSurface.singleton_hook?(scope, class_name) ||
988
1272
  RbsReceiverSurface.singleton_mixin_may_answer?(scope, class_name, method_name)
@@ -1376,13 +1660,19 @@ module Rigor
1376
1660
  # answers, that never reaches a verdict — depend on `Widget`'s declaring file.
1377
1661
  # `dependency_recorder_spec` caught it here exactly as it caught #723's walk.
1378
1662
  #
1379
- # The receiver's OWN includes only. A project ancestor's unknown include makes the surface just as
1380
- # unknown; walking the chain is a deliberate deferral rather than an oversight.
1381
- def unknown_mixin_includer?(class_name, scope)
1663
+ # A constant the environment does not know is read off the receiver's OWN includes only. Issue #2991 — a
1664
+ # mixin no name spells (`include(*MODS)`, `include helper`) opens the receiver when it sits anywhere on its
1665
+ # instance-side chain, its own level or an ancestor's: the chain marks it (`ResolutionChain#unnamed_mixin?`),
1666
+ # and any name may come from the unlisted module (a hook's `base.include M` is read so too). `self.include M`
1667
+ # and a receiverless `send(:include, M)` are recorded as `include M` is (#2995), so the ordinary lookup reads
1668
+ # `M`. A mixin whose ORDER alone is unknown (`include M; prepend M`) has its module recorded, so it opens
1669
+ # nothing. A class-object receiver reads its singleton chain instead ({#singleton_mixin_may_answer?}).
1670
+ def unknown_mixin_includer?(class_name, scope, kind)
1382
1671
  includes = scope.includes_of(class_name)
1383
- return false if includes.empty?
1672
+ return true if includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
1673
+ return false if kind == :singleton
1384
1674
 
1385
- includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
1675
+ Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).unnamed_mixin?
1386
1676
  end
1387
1677
 
1388
1678
  def known_mixin?(raw_name, class_name, scope)
@@ -1494,8 +1784,8 @@ module Rigor
1494
1784
  # as a per-instance singleton method and answers the rest through a private `method_missing` (ostruct 0.6), so
1495
1785
  # no signature can enumerate its surface. The only other declaration in the environment is json's add-on
1496
1786
  # reopening (`json_create` / `as_json` / `to_json`), which made the class read as fully declared and every
1497
- # field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`, and
1498
- # an RBS-declared `method_missing` does not by itself open a receiver here, so the class is named. No load
1787
+ # field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`; since
1788
+ # #2792 that alone keeps `call.undefined-method` silent, but the rules below still need the class named. No load
1499
1789
  # gate is needed: the core overlay loads into every environment that has RBS at all, so the list is active
1500
1790
  # whenever the declaration it protects is. Both sides of the class are open, as with
1501
1791
  # {GEM_OVERLAY_OPEN_RECEIVERS}, and an inherited Object / Kernel signature stops deciding `call.wrong-arity`
@@ -1636,38 +1926,51 @@ module Rigor
1636
1926
  registry.open_receiver?(class_name)
1637
1927
  end
1638
1928
 
1639
- # Whether THIS run's RBS environment actually includes one of Rigor's OWN partial declarations of the
1640
- # overlaid gem — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see that
1641
- # constant's comment). `RbsLoader#signature_paths` is the same list `Environment.for_project` builds
1642
- # from `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
1643
- # two of its members can carry that declaration:
1929
+ # Whether THIS run's RBS environment actually includes Rigor's OWN partial declaration of
1930
+ # ACTIVESUPPORT — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see
1931
+ # that constant's comment), and both members of that list are `ActiveSupport` classes, so the gate
1932
+ # asks for the activesupport overlay specifically. Issue #2857 added a SECOND bundled overlay
1933
+ # (`addressable`, eligible under `:rbs_collection` coverage), and a collection project can load it
1934
+ # while the activesupport overlay stays unloaded — keying on "any `data/gem_overlay/` directory"
1935
+ # would then silence `ActiveSupport::Duration#bar`, the ADR-26 false negative this gate exists to
1936
+ # prevent.
1644
1937
  #
1645
- # - a `data/gem_overlay/<gem>/` directory, which is a member if and only if `gem_overlay_paths`
1646
- # picked it (the gem is locked, `:missing` coverage, and its plugin twin is NOT reachable —
1647
- # ADR-72), so the `RbsLoader.under_gem_overlay_root?` prefix match is exact: no other signature
1648
- # source lives under that root;
1649
- # - the bundled plugin twin's own `sig/`, when a project wires it through `signature_paths:` instead
1650
- # of `plugins:`. Before #672 that route loaded BOTH halves and `Duration` collapsed outright, so
1651
- # nothing fired and nothing needed protecting; now the overlay stands down and the twin's
1652
- # declaration is the one that loads — with no manifest behind it, so `open_receiver?` above stays
1653
- # false and this is the only thing standing between a real `Duration` receiver and a false
1654
- # `call.undefined-method` on every member the declaration omits.
1938
+ # `RbsLoader#signature_paths` is the same list `Environment.for_project` builds from
1939
+ # `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
1940
+ # two of its members can carry the activesupport declaration:
1655
1941
  #
1656
- # When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the second
1657
- # goes true — its `sig/` rides in as a plugin signature path — but nothing here decides that case:
1658
- # `open_receiver?`'s manifest-driven check short-circuits above it. The two answers agree, which is
1659
- # the point; this predicate asks only whether a Rigor-authored partial declaration is in the
1660
- # environment, never how the project asked for it.
1942
+ # - `data/gem_overlay/activesupport/` itself — a member if and only if `gem_overlay_paths` picked
1943
+ # it (the gem is locked, its coverage source admits the overlay — `:missing` for activesupport —
1944
+ # and its plugin twin is NOT reachable, ADR-72). {RbsLoader.gem_overlay_sig_paths} rebuilds the
1945
+ # exact Pathname `for_project` appended, so a plain `include?` is the test: no other signature
1946
+ # source can occupy that path;
1947
+ # - the bundled activesupport plugin twin's own `sig/`, when a project wires it through
1948
+ # `signature_paths:` instead of `plugins:`. Before #672 that route loaded BOTH halves and
1949
+ # `Duration` collapsed outright, so nothing fired and nothing needed protecting; now the overlay
1950
+ # stands down and the twin's declaration is the one that loads — with no manifest behind it, so
1951
+ # `open_receiver?` above stays false and this is the only thing standing between a real
1952
+ # `Duration` receiver and a false `call.undefined-method` on every member the declaration
1953
+ # omits. {RbsLoader.gem_overlay_twin_signatures_loaded?} enumerates `GEM_OVERLAY_PLUGIN_IDS`'
1954
+ # values, which today is only the activesupport twin; a second overlay twin would need the same
1955
+ # per-gem keying this method now applies to the directory arm.
1661
1956
  #
1662
- # The two arms are asked in that order, over the WHOLE list each time, rather than interleaved per
1663
- # path: the overlay arm is a string prefix and the twin arm canonicalises paths on the filesystem,
1664
- # so the overwhelmingly common overlay-only run never reaches a syscall here.
1957
+ # When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the
1958
+ # second goes true — its `sig/` rides in as a plugin signature path — but nothing here decides
1959
+ # that case: `open_receiver?`'s manifest-driven check short-circuits above it. The two answers
1960
+ # agree, which is the point; this predicate asks only whether the Rigor-authored activesupport
1961
+ # declaration is in the environment, never how the project asked for it.
1962
+ #
1963
+ # The two arms are asked in that order, over the WHOLE list each time, rather than interleaved
1964
+ # per path: the overlay arm is a Pathname comparison (plus the two `File.directory?` probes
1965
+ # {RbsLoader.gem_overlay_sig_paths} needs to rebuild the path) and the twin arm canonicalises
1966
+ # paths on the filesystem, so an overlay-only run pays the cheaper arm first.
1665
1967
  def gem_overlay_loaded?(scope)
1666
1968
  loader = scope.environment&.rbs_loader
1667
1969
  return false if loader.nil? || !loader.respond_to?(:signature_paths)
1668
1970
 
1669
1971
  paths = loader.signature_paths
1670
- paths.any? { |path| Rigor::Environment::RbsLoader.under_gem_overlay_root?(path) } ||
1972
+ activesupport_overlay = Rigor::Environment::RbsLoader.gem_overlay_sig_paths(["activesupport"])
1973
+ paths.any? { |path| activesupport_overlay.include?(path) } ||
1671
1974
  Rigor::Environment::RbsLoader.gem_overlay_twin_signatures_loaded?(paths)
1672
1975
  end
1673
1976
 
@@ -1750,6 +2053,16 @@ module Rigor
1750
2053
  method_def
1751
2054
  end
1752
2055
 
2056
+ # Issue #2786 — {#trustworthy_signature} on the call's receiver as {CallFacts} types it, resolved once per
2057
+ # call node: `call.wrong-arity` and `call.argument-type-mismatch` both ask it with that receiver type and its
2058
+ # concrete class.
2059
+ def receiver_signature(facts)
2060
+ facts.receiver_signature do
2061
+ receiver_type = facts.receiver_type
2062
+ trustworthy_signature(receiver_type, concrete_class_name(receiver_type), facts.call_node, facts.scope)
2063
+ end
2064
+ end
2065
+
1753
2066
  def unauthoritative_inherited_signature?(class_name, method_def, scope)
1754
2067
  return false unless unbounded_receiver_surface?(class_name, scope)
1755
2068
  return true unless method_def.respond_to?(:defined_in)
@@ -1942,18 +2255,18 @@ module Rigor
1942
2255
  # it returns nil when the call's receiver / RBS coverage /
1943
2256
  # call shape disqualifies the rule.
1944
2257
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1945
- def wrong_arity_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2258
+ def wrong_arity_diagnostic(path, call_node, facts, lexical_sites = nil)
1946
2259
  return nil if call_node.receiver.nil?
1947
2260
  return nil unless plain_positional_call?(call_node)
1948
2261
 
1949
- scope = scope_index[call_node]
2262
+ scope = facts.scope
1950
2263
  return nil if scope.nil?
1951
2264
 
1952
2265
  # ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound; a wrong-arity
1953
2266
  # firing against it is an FP by construction. Decline.
1954
2267
  return nil if inferred_param_receiver?(call_node, scope)
1955
2268
 
1956
- receiver_type = scope.type_of(call_node.receiver)
2269
+ receiver_type = facts.receiver_type
1957
2270
  class_name = concrete_class_name(receiver_type)
1958
2271
  return nil if class_name.nil?
1959
2272
 
@@ -1974,7 +2287,7 @@ module Rigor
1974
2287
  # false positive. Skip arity-checking the chained position.
1975
2288
  return nil if anonymous_struct_new_call?(call_node, class_name, kind)
1976
2289
 
1977
- arity_envelope = arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
2290
+ arity_envelope = arity_envelope_for(facts, class_name, kind)
1978
2291
  return nil if arity_envelope.nil?
1979
2292
 
1980
2293
  actual = (call_node.arguments&.arguments || []).size
@@ -2005,11 +2318,14 @@ module Rigor
2005
2318
  # declaration. `trustworthy_signature` is the same lookup `argument_type_diagnostic` treats as
2006
2319
  # authoritative over a source `def` (see its own comment beside that call), and the two rules must agree
2007
2320
  # on whose contract binds, so a declared method is checked against its declaration.
2008
- def arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
2321
+ def arity_envelope_for(facts, class_name, kind)
2322
+ receiver_type = facts.receiver_type
2323
+ call_node = facts.call_node
2324
+ scope = facts.scope
2009
2325
  if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
2010
2326
  return nil unless definition_available?(receiver_type, class_name, scope)
2011
2327
 
2012
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
2328
+ method_def = receiver_signature(facts)
2013
2329
  if method_def && !Rigor::RbsExtended.inferred_signature?(method_def)
2014
2330
  return nil if undeclared_constructor?(class_name, call_node, kind, method_def)
2015
2331
 
@@ -2194,7 +2510,7 @@ module Rigor
2194
2510
  # and union receivers where every member already
2195
2511
  # disqualifies the call (avoid duplicating the
2196
2512
  # undefined-method diagnostic).
2197
- def nil_receiver_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2513
+ def nil_receiver_diagnostic(path, call_node, facts, lexical_sites = nil)
2198
2514
  return nil if call_node.receiver.nil?
2199
2515
  # Safe-navigation calls (`recv&.method`) already
2200
2516
  # short-circuit on nil at runtime, so a nil-bearing
@@ -2208,7 +2524,7 @@ module Rigor
2208
2524
  # produce false positives we cannot suppress.
2209
2525
  return nil unless call_node.receiver.is_a?(Prism::LocalVariableReadNode)
2210
2526
 
2211
- scope = scope_index[call_node]
2527
+ scope = facts.scope
2212
2528
  return nil if scope.nil?
2213
2529
 
2214
2530
  # ADR-58 WD1 — a receiver whose `nil` constituent is purely
@@ -2234,7 +2550,7 @@ module Rigor
2234
2550
  # construction. Decline.
2235
2551
  return nil if scope.inferred_param?(call_node.receiver.name)
2236
2552
 
2237
- receiver_type = scope.type_of(call_node.receiver)
2553
+ receiver_type = facts.receiver_type
2238
2554
  return nil unless receiver_type.is_a?(Type::Union)
2239
2555
 
2240
2556
  # The rule only fires when the analyzer has access to
@@ -2243,7 +2559,7 @@ module Rigor
2243
2559
  return nil unless Rigor::Reflection.rbs_class_known?("NilClass", scope: scope)
2244
2560
 
2245
2561
  return nil unless nil_bearing_union_witnesses?(receiver_type, call_node.name, scope)
2246
- return nil if nil_receiver_withheld?(call_node, receiver_type, scope, scope_index, lexical_sites)
2562
+ return nil if nil_receiver_withheld?(call_node, receiver_type, scope, facts.scope_index, lexical_sites)
2247
2563
 
2248
2564
  build_nil_receiver_diagnostic(path, call_node)
2249
2565
  end
@@ -2282,8 +2598,7 @@ module Rigor
2282
2598
  # nil so the rule bails (silent). Every other receiver (including a
2283
2599
  # `T | nil` union, which already has no single concrete class) flows
2284
2600
  # through unchanged.
2285
- def safe_navigation_receiver(call_node, scope)
2286
- receiver_type = scope.type_of(call_node.receiver)
2601
+ def safe_navigation_receiver(call_node, receiver_type)
2287
2602
  return receiver_type unless call_node.safe_navigation?
2288
2603
  return receiver_type unless nil_member?(receiver_type)
2289
2604
 
@@ -2605,22 +2920,24 @@ module Rigor
2605
2920
  INTEGER_RAISING_OPERATORS = %i[/ % div modulo divmod].freeze
2606
2921
  private_constant :INTEGER_RAISING_OPERATORS
2607
2922
 
2608
- def always_raises_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2609
- return nil unless integer_zero_division?(call_node, scope_index)
2923
+ def always_raises_diagnostic(path, call_node, facts, lexical_sites = nil, raise_shield = nil)
2924
+ return nil unless integer_zero_division?(call_node, facts)
2610
2925
 
2611
2926
  # ADR-121 WD7 — a refinement of `Integer`'s operator (or one Rigor cannot read) in effect may not raise.
2612
- scope = scope_index[call_node]
2613
- return nil if refined_method_in_effect?("Integer", call_node, scope, :instance, lexical_sites)
2927
+ return nil if refined_method_in_effect?("Integer", call_node, facts.scope, :instance, lexical_sites)
2928
+
2929
+ # Issue #3036 — a raise an enclosing `begin` / `def … rescue` catches flows to the handler.
2930
+ return nil if raise_shield&.rescued?(call_node, facts.scope)
2614
2931
 
2615
2932
  build_always_raises_diagnostic(path, call_node)
2616
2933
  end
2617
2934
 
2618
- def integer_zero_division?(call_node, scope_index)
2935
+ def integer_zero_division?(call_node, facts)
2619
2936
  return false unless raising_call_shape?(call_node)
2620
2937
 
2621
- scope = scope_index[call_node]
2938
+ scope = facts.scope
2622
2939
  return false if scope.nil?
2623
- return false unless integer_rooted_for_diagnostic?(scope.type_of(call_node.receiver))
2940
+ return false unless integer_rooted_for_diagnostic?(facts.receiver_type)
2624
2941
 
2625
2942
  arg = single_argument(call_node)
2626
2943
  arg && integer_zero_constant?(scope.type_of(arg))
@@ -3132,17 +3449,17 @@ module Rigor
3132
3449
  # - Issue #1984 — Ruby hands a private call to `method_missing`, so a `method_missing` or
3133
3450
  # `respond_to_missing?` a project entry on the receiver's chain records, on the side the call reaches,
3134
3451
  # declines the rule ({#missing_hook_on_chain?}).
3135
- def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
3452
+ def visibility_mismatch_diagnostic(path, call_node, facts, lexical_sites = nil)
3136
3453
  return nil unless explicit_non_self_receiver?(call_node.receiver)
3137
3454
 
3138
- scope = scope_index[call_node]
3455
+ scope = facts.scope
3139
3456
  return nil if scope.nil?
3140
3457
 
3141
3458
  # ADR-67 WD6b — an inferred-parameter receiver's nominal type is an open-call-site lower bound; a
3142
3459
  # private-method visibility firing against it is an FP by construction. Decline.
3143
3460
  return nil if inferred_param_receiver?(call_node, scope)
3144
3461
 
3145
- receiver_type = scope.type_of(call_node.receiver)
3462
+ receiver_type = facts.receiver_type
3146
3463
  case receiver_type
3147
3464
  when Type::Nominal then instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
3148
3465
  when Type::Singleton then singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
@@ -3187,7 +3504,14 @@ module Rigor
3187
3504
  build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
3188
3505
  end
3189
3506
 
3190
- PRIVATE_NAMES_MEMO = RunScopedSlots.register(:rigor_check_rules_private_singleton_names)
3507
+ # ADR-129 WD1 — a run-scoped Memo: one `[discovery, names]` slot keyed on the discovery index's identity, the
3508
+ # only input the name set reads.
3509
+ PRIVATE_NAMES_MEMO = TypingState.memo(
3510
+ :check_rules_private_singleton_names,
3511
+ run_scoped: true, audit: "private singleton names",
3512
+ reason: "one [discovery, names] slot keyed on the discovery index's identity; the name " \
3513
+ "set reads only its method-visibility tables"
3514
+ )
3191
3515
  private_constant :PRIVATE_NAMES_MEMO
3192
3516
 
3193
3517
  # Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
@@ -3198,12 +3522,23 @@ module Rigor
3198
3522
  def private_singleton_candidate?(scope, name)
3199
3523
  DependencyRecorder.read_visibility("*", name) if DependencyRecorder.active?
3200
3524
  discovery = scope.discovery
3201
- slot = Thread.current[PRIVATE_NAMES_MEMO]
3525
+ if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
3526
+ return private_recorded_names(discovery.discovered_method_visibilities).include?(name.to_sym)
3527
+ end
3528
+
3529
+ state = TypingState.current
3530
+ slot = state[PRIVATE_NAMES_MEMO]
3202
3531
  unless slot && slot[0].equal?(discovery)
3203
- slot = [discovery, private_recorded_names(discovery.discovered_method_visibilities)]
3204
- Thread.current[PRIVATE_NAMES_MEMO] = slot
3532
+ names = Inference::MemoAudit.key_audit("private singleton names") do
3533
+ private_recorded_names(discovery.discovered_method_visibilities)
3534
+ end
3535
+ slot = [discovery, names]
3536
+ state[PRIVATE_NAMES_MEMO] = slot
3537
+ return slot[1].include?(name.to_sym)
3205
3538
  end
3206
- slot[1].include?(name.to_sym)
3539
+ Inference::MemoAudit.check("private singleton names", slot[1], name) do
3540
+ private_recorded_names(discovery.discovered_method_visibilities)
3541
+ end.include?(name.to_sym)
3207
3542
  end
3208
3543
 
3209
3544
  def private_recorded_names(tables)
@@ -3512,15 +3847,15 @@ module Rigor
3512
3847
  COERCE_DISPATCH_METHODS = %i[+ - * / % ** & | ^ << >> < > <= >=].to_set.freeze
3513
3848
  private_constant :COERCE_DISPATCH_METHODS
3514
3849
 
3515
- def argument_type_diagnostic(path, call_node, scope_index, lexical_sites = nil)
3850
+ def argument_type_diagnostic(path, call_node, facts, lexical_sites = nil)
3516
3851
  return nil if call_node.receiver.nil?
3517
3852
  return nil if UNIVERSAL_EQUALITY_METHODS.include?(call_node.name)
3518
3853
  return nil unless plain_positional_call?(call_node)
3519
3854
 
3520
- scope = scope_index[call_node]
3855
+ scope = facts.scope
3521
3856
  return nil if scope.nil?
3522
3857
 
3523
- receiver_type = scope.type_of(call_node.receiver)
3858
+ receiver_type = facts.receiver_type
3524
3859
  class_name = concrete_class_name(receiver_type)
3525
3860
  return nil if class_name.nil?
3526
3861
 
@@ -3533,11 +3868,11 @@ module Rigor
3533
3868
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
3534
3869
  return nil unless definition_available?(receiver_type, class_name, scope)
3535
3870
 
3536
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
3871
+ method_def = receiver_signature(facts)
3537
3872
  return nil if method_def.nil?
3538
3873
 
3539
3874
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
3540
- mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, scope_index)
3875
+ mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, facts.scope_index)
3541
3876
  return nil if mismatch.nil?
3542
3877
  return nil if argument_report_withheld?(receiver_type, class_name, call_node, mismatch, scope, lexical_sites)
3543
3878
 
@@ -3685,12 +4020,36 @@ module Rigor
3685
4020
  # The non-nil channel (ADR-64 WD2/WD3): a single-concrete-class
3686
4021
  # argument no overload admits, on a non-coerce method.
3687
4022
  def non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
4023
+ return class_object_overload_mismatch(arg, arg_type, params, param_overrides, scope) if
4024
+ arg_type.is_a?(Type::Singleton)
3688
4025
  return nil unless single_concrete_arg_class?(arg_type, scope)
3689
4026
  return nil if params.any? { |param| param_accepts_arg_class?(param, arg_type, param_overrides, scope) }
3690
4027
 
3691
4028
  { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
3692
4029
  end
3693
4030
 
4031
+ # Issue #2828 — the class-object twin of the non-nil channel. A `singleton(T)` overload the `resolv` overlay
4032
+ # prepends turned `getresources(d, String)`'s one-overload check (`expected singleton(Resolv::DNS::Query)`)
4033
+ # into this multi-overload one, which declined every class object. It fires only when every overload's
4034
+ # parameter at the position is a `singleton(...)` (a `singleton(T)` read through its class bound, see
4035
+ # `SingletonTypeParams.selection_param`) that provably refuses the argument's class; any other parameter
4036
+ # shape admits.
4037
+ def class_object_overload_mismatch(arg, arg_type, params, param_overrides, scope)
4038
+ return nil unless Rigor::Reflection.rbs_class_known?(arg_type.class_name, scope: scope)
4039
+ return nil unless params.all? do |param|
4040
+ !param_overrides[param.name] && param.type.is_a?(RBS::Types::ClassSingleton)
4041
+ end
4042
+ return nil unless params.all? { |param| class_singleton_refuses?(param.type, arg_type, scope) }
4043
+
4044
+ expected = params.map { |param| param.type.to_s.sub("singleton(::", "singleton(") }.uniq.join(" | ")
4045
+ { node: arg, name: nil, expected: expected, actual: arg_type }
4046
+ end
4047
+
4048
+ def class_singleton_refuses?(rbs_type, arg_type, scope)
4049
+ translated = translate_param_type(rbs_type, scope.environment)
4050
+ translated.is_a?(Type::Singleton) && Inference::Acceptance.accepts(translated, arg_type, mode: :gradual).no?
4051
+ end
4052
+
3694
4053
  # The matching positional RBS param across every overload, or nil when
3695
4054
  # any overload has no param at `index` (arity divergence — the
3696
4055
  # wrong-arity rule's concern, not this one's).
@@ -3711,6 +4070,7 @@ module Rigor
3711
4070
  # Load-bearing since rbs 4.1 rewrote core signatures into this form
3712
4071
  # (`Array#fetch`'s block overload is `[I < _ToInt, T] (I index) { … }`).
3713
4072
  def resolve_param_bounds(param, method_type)
4073
+ param = Inference::MethodDispatcher::SingletonTypeParams.selection_param(param, method_type)
3714
4074
  bounded = method_type.type_params.select(&:upper_bound)
3715
4075
  return param if bounded.empty?
3716
4076
 
@@ -3997,6 +4357,7 @@ module Rigor
3997
4357
  param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3998
4358
  return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
3999
4359
  return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
4360
+ return nil if gradual_narrowing_accepted?(arg, arg_type, param_type, scope)
4000
4361
  return nil unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
4001
4362
  # Issue #1362 — a value that mixes such a global with something else (`c ? $stdout : StringIO.new`) is judged
4002
4363
  # without the declared members the file never writes, as it was before the join.
@@ -4034,10 +4395,34 @@ module Rigor
4034
4395
  Inference::Acceptance.accepts(param_type, type, mode: :gradual).no?
4035
4396
  end
4036
4397
 
4398
+ # Issue #2656 — a local a class guard narrowed from a `Dynamic` value
4399
+ # ({DeclarationSourcedGuard.gradual_narrowed?}) is judged by its class, not by the literals the parameter
4400
+ # names: `mode(x) if x.is_a?(String)` with `mode: ("r" | "w")` is accepted, as the gradual value was, while
4401
+ # `num(x) if x.is_a?(String)` with `num: (1 | 2)` still reports, since no String is ever `1` or `2`. The
4402
+ # parameter's value-pinned members are widened to their classes ({Type::Combinator.widen_value_pinned}) and
4403
+ # the narrowed type is judged against that. The overload path ({#non_nil_arg_overload_mismatch}) already
4404
+ # judges by class, so it needs no such reading.
4405
+ #
4406
+ # A literal member of the narrowed type did not come from the gradual value (`x = h[:m] || "r"` narrows to
4407
+ # `"r" | String`), so it is judged against the parameter as written.
4408
+ def gradual_narrowing_accepted?(arg, arg_type, param_type, scope)
4409
+ return false unless DeclarationSourcedGuard.gradual_narrowed?(arg, scope)
4410
+
4411
+ members = arg_type.is_a?(Type::Union) ? arg_type.members : [arg_type]
4412
+ literals, rest = members.partition { |member| member.is_a?(Type::Constant) }
4413
+ if literals.any? { |member| Inference::Acceptance.accepts(param_type, member, mode: :gradual).no? }
4414
+ return false
4415
+ end
4416
+ return true if rest.empty?
4417
+
4418
+ widened = Type::Combinator.widen_value_pinned(param_type)
4419
+ !Inference::Acceptance.accepts(widened, Type::Combinator.union(*rest), mode: :gradual).no?
4420
+ end
4421
+
4037
4422
  # The parameter rejects the argument AND the rejection is not a
4038
4423
  # withheld declaration-sourced-nil case.
4039
4424
  def argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
4040
- return false unless Inference::Acceptance.accepts(param_type, arg_type, mode: :gradual).no?
4425
+ return false unless rejects_beyond_class_objects?(param_type, arg_type)
4041
4426
 
4042
4427
  # ADR-58 (N2 extension) — the same declaration-sourced-nil-is-not-
4043
4428
  # diagnostic-fuel criterion that governs `possible-nil-receiver`
@@ -4053,6 +4438,31 @@ module Rigor
4053
4438
  !declaration_sourced_nil_only_mismatch?(arg, arg_type, param_type, scope)
4054
4439
  end
4055
4440
 
4441
+ # Issue #2855 — the parameter rejects the argument, and not only for its `Class` / `Module` members. An
4442
+ # unparameterised `Class` or `Module` nominal ({Acceptance.class_or_module_nominal?}) against a parameter
4443
+ # that admits class objects (`singleton(X)`, alone or in a union) is a class object whose identity inference
4444
+ # lost — a `const_get` result kept by `is_a?(Class)`, an `is_a?(Module)` guard — so it is withheld. The rest
4445
+ # of the argument is still judged: `Class | Integer` against `singleton(X)` reports for its `Integer`.
4446
+ # Acceptance itself keeps answering `no`, so overload selection is unchanged.
4447
+ def rejects_beyond_class_objects?(param_type, arg_type)
4448
+ return false unless Inference::Acceptance.accepts(param_type, arg_type, mode: :gradual).no?
4449
+ return true unless admits_class_objects?(param_type)
4450
+
4451
+ members = arg_type.is_a?(Type::Union) ? arg_type.members : [arg_type]
4452
+ rest = members.reject { |member| Inference::Acceptance.class_or_module_nominal?(member) }
4453
+ return true if rest.size == members.size
4454
+
4455
+ Inference::Acceptance.accepts(param_type, Type::Combinator.union(*rest), mode: :gradual).no?
4456
+ end
4457
+
4458
+ def admits_class_objects?(param_type)
4459
+ case param_type
4460
+ when Type::Singleton then true
4461
+ when Type::Union then param_type.members.any?(Type::Singleton)
4462
+ else false
4463
+ end
4464
+ end
4465
+
4056
4466
  # True when `arg` is a declaration-sourced read whose rejection is
4057
4467
  # caused solely by its nil constituent: stripping nil from the argument
4058
4468
  # type yields a type the parameter accepts (gradual mode). Mirrors the
@@ -4068,7 +4478,12 @@ module Rigor
4068
4478
  non_nil = Type::Combinator.union(*arg_type.members.reject { |m| nil_member?(m) })
4069
4479
  return false if non_nil.is_a?(Type::Bot)
4070
4480
 
4071
- Inference::Acceptance.accepts(param_type, non_nil, mode: :gradual).yes?
4481
+ result = Inference::Acceptance.accepts(param_type, non_nil, mode: :gradual)
4482
+ # Issue #2855 — a `Class?` ivar against `singleton(X)` is withheld as its `Class` alone would be.
4483
+ # Issue #2863 — a `maybe` (a project class the host process has not loaded, so its ancestry is unresolved:
4484
+ # `singleton(Circle)` against `singleton(Shape)`) is not a rejection either: the non-nil part alone would not
4485
+ # fire, so the nil is the only reason and it is withheld.
4486
+ !result.no? || !rejects_beyond_class_objects?(param_type, non_nil)
4072
4487
  end
4073
4488
 
4074
4489
  # What this rule needs is that argument N binds to positional
@@ -4186,7 +4601,7 @@ module Rigor
4186
4601
  # :no → emit at :error (severity_profile may re-stamp; default `balanced` keeps the
4187
4602
  # authored severity).
4188
4603
  # :maybe → silent (see {#compare_return}; ADR-8's `strict` promotion is deferred).
4189
- def return_type_mismatch_diagnostic(path, def_node, scope_index)
4604
+ def return_type_mismatch_diagnostic(path, def_node, scope_index, replaced_defs = nil)
4190
4605
  return nil if def_node.body.nil?
4191
4606
 
4192
4607
  last_expr = body_last_expression(def_node.body)
@@ -4195,7 +4610,7 @@ module Rigor
4195
4610
  inner_scope = scope_index[last_expr] || scope_index[def_node.body] || scope_index[def_node]
4196
4611
  return nil if inner_scope.nil?
4197
4612
 
4198
- declared = declared_return_type(def_node, scope_index)
4613
+ declared = declared_return_type(def_node, scope_index, replaced_defs)
4199
4614
  return nil if declared.nil?
4200
4615
 
4201
4616
  # Issue #1362 — a body that ends on a builtin global still on its declared seed, or on a local copied from
@@ -4242,14 +4657,18 @@ module Rigor
4242
4657
  # `non-empty-array[Integer]`, etc. — are stricter than the underlying RBS class, so a body whose
4243
4658
  # inferred type the bare RBS sig would accept may still fail the refinement (e.g. `def name; "";
4244
4659
  # end` returns `Constant[""]`, accepted by `String` but rejected by `non-empty-string`).
4245
- def declared_return_type(def_node, scope_index)
4660
+ def declared_return_type(def_node, scope_index, replaced_defs = nil)
4246
4661
  scope = scope_index[def_node]
4247
4662
  return nil if scope.nil?
4248
4663
 
4249
4664
  self_type = scope.self_type
4250
4665
  return nil unless self_type.respond_to?(:class_name)
4251
4666
 
4252
- side = def_node.receiver.nil? ? receiverless_def_side(scope, self_type.class_name, def_node) : :singleton
4667
+ side = if def_node.receiver.nil?
4668
+ receiverless_def_side(scope, self_type.class_name, def_node)
4669
+ else
4670
+ receiver_def_side(scope, self_type.class_name, def_node)
4671
+ end
4253
4672
  return nil if side.nil?
4254
4673
 
4255
4674
  method_def =
@@ -4260,6 +4679,7 @@ module Rigor
4260
4679
  end
4261
4680
  return nil if method_def.nil?
4262
4681
  return nil unless defined_on?(method_def, self_type.class_name)
4682
+ return nil if replaced_receiver_def?(def_node, replaced_defs)
4263
4683
 
4264
4684
  override = Rigor::RbsExtended.read_return_type_override(method_def, environment: scope.environment)
4265
4685
  return override if override
@@ -4282,6 +4702,30 @@ module Rigor
4282
4702
  nil
4283
4703
  end
4284
4704
 
4705
+ # Issue #3155 — a singleton def a later certain def replaced ({ReplacedSingletonDefs}). Asked only once the def
4706
+ # has a signature to answer to, so the file walk runs only then.
4707
+ def replaced_receiver_def?(def_node, replaced_defs)
4708
+ !def_node.receiver.nil? && replaced_defs&.replaced?(def_node)
4709
+ end
4710
+
4711
+ # Issue #3155 — `def self.x`, or `def K.x` in `K`'s body, answers to K's singleton signature. A def on another
4712
+ # object (`def String.q`, `def obj.q` in a class body) or `def self.x` in a `class << self` body (a method of
4713
+ # the singleton class's own singleton) answers to none of K's: `nil`, and the caller declines.
4714
+ # {#declared_return_type} also declines, once a signature is found, a singleton def a later certain def of the
4715
+ # name in the same body replaced (`def K.w`; `def self.w`; {ReplacedSingletonDefs}). `def (self).x` reads as
4716
+ # `def self.x`; `K = self; def K.x` is not read.
4717
+ def receiver_def_side(scope, class_name, def_node)
4718
+ receiver = ReplacedSingletonDefs.unparenthesized(def_node.receiver)
4719
+ lexical_self = if receiver.is_a?(Prism::SelfNode)
4720
+ !scope.singleton_class_body?
4721
+ else
4722
+ Inference::ScopeIndexer.def_receiver_targets_lexical_self?(receiver, class_name.split("::"))
4723
+ end
4724
+ return nil unless lexical_self
4725
+
4726
+ :singleton
4727
+ end
4728
+
4285
4729
  # The tables hold the live node or one re-resolved from a `DefHandle`, so identity is the name and span.
4286
4730
  def same_def_node?(recorded, def_node)
4287
4731
  recorded.is_a?(Prism::DefNode) && recorded.name == def_node.name &&
@@ -4425,8 +4869,10 @@ module Rigor
4425
4869
  # defined LATER under that name would change the answer; `DefinerResolution` files the negative class
4426
4870
  # edge of each external entry it tests.
4427
4871
  def overridden_ancestors_start(scope, class_name)
4428
- own = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).index_of(class_name.to_s)
4429
- own && (own + 1)
4872
+ # Issue #2979 — past the entries of the names bound to the same class (`K = Struct.new("Named", …)`):
4873
+ # a `def` there redefines the class's method rather than overriding an ancestor's.
4874
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods)
4875
+ chain.ancestors_start(scope, class_name.to_s)
4430
4876
  end
4431
4877
 
4432
4878
  # `[defining_class, visibility]` for the nearest user-source ancestor that defines an instance
@@ -4521,6 +4967,7 @@ module Rigor
4521
4967
  parent_return = declared_return_union(parent_method, scope.environment, type_vars: parent_type_vars)
4522
4968
  return nil if override_return.nil? || parent_return.nil?
4523
4969
  return nil if dynamic_top?(parent_return) # untyped / unbound-generic parent contract
4970
+ return nil if void_over_nil?(override_method, parent_return)
4524
4971
 
4525
4972
  return nil unless parent_return.accepts(override_return).no?
4526
4973
 
@@ -4529,22 +4976,42 @@ module Rigor
4529
4976
  )
4530
4977
  end
4531
4978
 
4979
+ # A `-> void` override of a parent return of exactly `nil` (or `NilClass`) is exempt (#3046): both
4980
+ # say "no useful value", and callers cannot use a void return. Keyed on the declared `void` of every
4981
+ # arm (the translator reads `void` as top, so a `-> top` override still fires); any non-nil parent
4982
+ # return still fires.
4983
+ def void_over_nil?(override_method, parent_return)
4984
+ return false unless nil_type?(parent_return)
4985
+
4986
+ override_method.method_types.all? { |mt| mt.type.return_type.is_a?(::RBS::Types::Bases::Void) }
4987
+ end
4988
+
4989
+ # `nil`, or the nominal `NilClass`, whose only value it is.
4990
+ def nil_type?(type)
4991
+ return type.value.nil? if type.is_a?(Type::Constant)
4992
+
4993
+ type.is_a?(Type::Nominal) && type.class_name == "NilClass" && type.type_args.empty?
4994
+ end
4995
+
4532
4996
  # `[defining_class, RBS::Definition::Method]` for the nearest project-discovered ancestor whose
4533
4997
  # RBS declares `method_name` (not the starting class's own declaration), or nil.
4534
4998
  def nearest_ancestor_method_def(scope, class_name, method_name)
4535
4999
  from = overridden_ancestors_start(scope, class_name)
4536
5000
  return nil if from.nil?
4537
5001
 
5002
+ passed_owners = []
4538
5003
  case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
4539
5004
  question: :override, from: from) do |ancestor|
4540
- method_def = safe_instance_method_definition(ancestor, method_name, scope)
4541
- # The signature text is the answer two worlds compare: an `RBS::Definition::Method` has no
4542
- # structural equality of its own.
4543
- method_def.method_types.map(&:to_s) if method_def && !defined_on?(method_def, class_name)
5005
+ override_parent_answer(scope, ancestor, method_name, passed_owners)
4544
5006
  end
4545
- in Inference::DefinerResolution::Known(owner:)
4546
- method_def = safe_instance_method_definition(owner, method_name, scope)
4547
- method_def && [owner, method_def]
5007
+ in Inference::DefinerResolution::Known(owner:, answer:)
5008
+ # The nearest declaration must be the one that answers: an ancestor asked past for a project
5009
+ # owner the walk never reached (one the Ruby chain does not hold) leaves the rule silent.
5010
+ unless answer.last == RBS_ONLY_OWNER ||
5011
+ passed_owners.any? { |passed| passed != normalize_class_name(owner) }
5012
+ method_def = safe_instance_method_definition(owner, method_name, scope)
5013
+ method_def && [owner, method_def]
5014
+ end
4548
5015
  in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
4549
5016
  nil
4550
5017
  in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
@@ -4552,6 +5019,38 @@ module Rigor
4552
5019
  end
4553
5020
  end
4554
5021
 
5022
+ # The answer an RBS-only owner gives the override walk: it ends the walk, and the rule stays silent.
5023
+ RBS_ONLY_OWNER = :rbs_only_owner
5024
+ private_constant :RBS_ONLY_OWNER
5025
+
5026
+ # One project ancestor's answer to the override walk. The signature text is the answer two worlds
5027
+ # compare: an `RBS::Definition::Method` has no structural equality of its own. Only an ancestor whose
5028
+ # own RBS declares the method answers with it (#3046). One that inherits the declaration from another
5029
+ # project class or module is asked past, so that owner answers at its own entry; the owner is recorded
5030
+ # in `passed_owners`, which the answering owner must match. One that inherits it from an RBS-only owner
5031
+ # the project chain cannot see ends the walk silent ({RBS_ONLY_OWNER}): that owner is the nearest
5032
+ # declaration, and RBS-only ancestors are #130's.
5033
+ def override_parent_answer(scope, ancestor, method_name, passed_owners)
5034
+ method_def = safe_instance_method_definition(ancestor, method_name, scope)
5035
+ return nil if method_def.nil?
5036
+ return method_def.method_types.map(&:to_s) if declared_by?(method_def, ancestor)
5037
+
5038
+ owner = method_def.implemented_in || method_def.defined_in
5039
+ return RBS_ONLY_OWNER unless owner && scope.discovered_classes.key?(normalize_class_name(owner))
5040
+
5041
+ passed_owners << normalize_class_name(owner)
5042
+ nil
5043
+ end
5044
+
5045
+ # True when `ancestor`'s own RBS declares `method_def`: on its body, or through an interface it
5046
+ # includes itself (RBS records the including class as `implemented_in`).
5047
+ def declared_by?(method_def, ancestor)
5048
+ return true if defined_on?(method_def, ancestor)
5049
+ return false unless method_def.defined_in&.interface?
5050
+
5051
+ normalize_class_name(method_def.implemented_in) == normalize_class_name(ancestor)
5052
+ end
5053
+
4555
5054
  def safe_instance_method_definition(class_name, method_name, scope)
4556
5055
  Reflection.instance_method_definition(class_name, method_name, scope: scope)
4557
5056
  rescue StandardError