rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../inference/scope_indexer"
6
+ require_relative "../../source/constant_path"
7
+ require_relative "../../source/node_children"
8
+
9
+ module Rigor
10
+ module Analysis
11
+ module CheckRules
12
+ # Issue #1120 — the two ways one file makes a method callable at some sites and not at others, which
13
+ # `call.undefined-method` has to ask the file's syntax about because no project-wide table can answer:
14
+ #
15
+ # - **Refinements.** `using M` activates M's refinements from the end of the call to the end of the body
16
+ # that holds it — the file's top level or a `class` / `module` body, including everything nested in it —
17
+ # and nowhere else, not even in another file. A `refine X do … end` block is active inside itself. What
18
+ # each module refines is the project-wide `Scope#discovered_refinements`; this answers whether any of
19
+ # those modules is in effect at a call site. A `using` whose argument is not a constant
20
+ # (`using Module.new { refine … }`) names no module, so every refinement counts as in effect throughout
21
+ # its file. A `using` inside a `def` raises in Ruby and activates nothing.
22
+ # - **Singleton defs on locals.** `def o.m` defines `m` on the one object `o` holds. The local's type
23
+ # is not changed; `o.m` is simply not reported within the scope the `def` is written in (the
24
+ # enclosing `def`, `class` / `module` body, or file — a block shares its enclosing scope's locals).
25
+ #
26
+ # Built lazily: the walk runs the first time a would-fire call asks, so a file with no such call pays
27
+ # one small object and nothing else.
28
+ class LexicalMethodSites
29
+ def initialize(root)
30
+ @root = root
31
+ @built = false
32
+ end
33
+
34
+ # Is a refinement from one of `modules` (refining-module names) in effect at `call_node`?
35
+ def refinement_active?(call_node, modules)
36
+ build
37
+ return true if @unresolved_using
38
+
39
+ offset = call_node.location.start_offset
40
+ return true if @refine_blocks.any? { |start, stop| offset >= start && offset < stop }
41
+
42
+ @usings.any? do |start, stop, candidates|
43
+ offset >= start && offset < stop && candidates.any? { |name| modules.include?(name) }
44
+ end
45
+ end
46
+
47
+ # Issue #1367 — is any refinement in effect at `node`: a `using` of any module, a `using` of a non-constant, or
48
+ # the inside of a `refine` block? The `global.*` stream check asks it where it cannot name the refining
49
+ # module (a `define_method` or `alias_method` inside `refine` records none).
50
+ def using_in_effect?(node)
51
+ build
52
+ return true if @unresolved_using
53
+
54
+ offset = node.location.start_offset
55
+ @refine_blocks.any? { |start, stop| offset >= start && offset < stop } ||
56
+ @usings.any? { |start, stop, _candidates| offset >= start && offset < stop }
57
+ end
58
+
59
+ # Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
60
+ # design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
61
+ def refinement_def?(def_node)
62
+ build
63
+ @refinement_defs.include?(def_node.location.start_offset)
64
+ end
65
+
66
+ # Does a `def <local>.<name>` for this call's local receiver and method name sit in the scope the call
67
+ # is in?
68
+ def singleton_local_def?(call_node)
69
+ receiver = call_node.receiver
70
+ return false unless receiver.is_a?(Prism::LocalVariableReadNode)
71
+
72
+ build
73
+ offset = call_node.location.start_offset
74
+ @singleton_defs.any? do |local, method_name, start, stop|
75
+ local == receiver.name && method_name == call_node.name && offset >= start && offset < stop &&
76
+ !nested_scope_holds?(offset, start, stop)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Is `offset` inside a local scope nested in the one `[start, stop)` spans? A `def` in the file's top level
83
+ # does not see the top level's `o`.
84
+ def nested_scope_holds?(offset, start, stop)
85
+ @scopes.any? do |inner_start, inner_stop|
86
+ (inner_start > start || inner_stop < stop) && inner_start >= start && inner_stop <= stop &&
87
+ offset >= inner_start && offset < inner_stop
88
+ end
89
+ end
90
+
91
+ def build
92
+ return if @built
93
+
94
+ @built = true
95
+ @usings = []
96
+ @refine_blocks = []
97
+ @singleton_defs = []
98
+ @scopes = []
99
+ @refinement_defs = Set.new
100
+ @unresolved_using = false
101
+ return if @root.nil?
102
+
103
+ location = @root.location
104
+ span = [location.start_offset, location.end_offset]
105
+ walk(@root, [], span, span, false)
106
+ end
107
+
108
+ # `body` is the `[start, end]` of the body a `using` here stays in effect to the end of; `locals` is the
109
+ # span of the scope a local written here belongs to.
110
+ def walk(node, prefix, body, locals, in_def)
111
+ case node
112
+ when Prism::ClassNode, Prism::ModuleNode
113
+ span = scope_span(node)
114
+ inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
115
+ return walk_children(node.body, inner, span, span, false)
116
+ when Prism::SingletonClassNode
117
+ walk(node.expression, prefix, body, locals, in_def)
118
+ span = scope_span(node)
119
+ return walk_children(node.body, prefix, span, span, false)
120
+ when Prism::DefNode
121
+ record_singleton_def(node, locals)
122
+ return walk_children(node.body, prefix, body, scope_span(node), true)
123
+ when Prism::CallNode
124
+ record_call(node, prefix, body, in_def)
125
+ end
126
+
127
+ walk_children(node, prefix, body, locals, in_def)
128
+ end
129
+
130
+ def walk_children(node, prefix, body, locals, in_def)
131
+ return if node.nil?
132
+
133
+ node.rigor_each_child { |child| walk(child, prefix, body, locals, in_def) }
134
+ end
135
+
136
+ def record_singleton_def(node, locals)
137
+ receiver = node.receiver
138
+ return unless receiver.is_a?(Prism::LocalVariableReadNode)
139
+
140
+ @singleton_defs << [receiver.name, node.name, *locals]
141
+ end
142
+
143
+ def record_call(node, prefix, body, in_def)
144
+ if Inference::ScopeIndexer.refine_target(node)
145
+ @refine_blocks << span_of(node.block)
146
+ record_refinement_defs(node.block.body)
147
+ elsif using_call?(node) && !in_def
148
+ record_using(node, prefix, body)
149
+ end
150
+ end
151
+
152
+ def record_refinement_defs(body)
153
+ return if body.nil?
154
+
155
+ Inference::ScopeIndexer.each_refinement_def(body) do |def_node|
156
+ @refinement_defs << def_node.location.start_offset
157
+ end
158
+ end
159
+
160
+ def using_call?(node)
161
+ node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
162
+ node.arguments&.arguments&.size == 1
163
+ end
164
+
165
+ def record_using(node, prefix, body)
166
+ argument = node.arguments.arguments.first
167
+ if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
168
+ candidates = Inference::ScopeIndexer.constant_receiver_candidates(argument, prefix)
169
+ @usings << [node.location.end_offset, body.last, candidates]
170
+ else
171
+ @unresolved_using = true
172
+ end
173
+ end
174
+
175
+ # The span of a node that opens a local scope, recorded for {#nested_scope_holds?}.
176
+ def scope_span(node)
177
+ span = span_of(node)
178
+ @scopes << span
179
+ span
180
+ end
181
+
182
+ def span_of(node)
183
+ location = node.location
184
+ [location.start_offset, location.end_offset]
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -19,10 +19,12 @@ module Rigor
19
19
  # accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
20
20
  # shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
21
21
  class MainPassCollector
22
- # The node classes the former inline pass branched on. A plain `Prism::IfNode` covers ternaries and
23
- # postfix `if` too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same).
22
+ # The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
23
+ # too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same). The three global-write
24
+ # classes carry issue #1367's `global.*` rules.
24
25
  NODE_CLASSES = [
25
- Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode
26
+ Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
27
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
26
28
  ].freeze
27
29
 
28
30
  # @param node_diagnostics — maps a `Prism::Node` to the array of diagnostics the main pass emits
@@ -6,9 +6,9 @@ module Rigor
6
6
  # Canonical identifiers for each rule. Per ADR-8 § "Diagnostic ID family hierarchy", rule names are
7
7
  # `family.rule-name` two-segment strings; the families group diagnostics by where they originate
8
8
  # (`call.*` for call-site rules, `flow.*` for flow-analysis proofs, `assert.*` for runtime-assertion
9
- # rules, `dump.*` for debug helpers, `def.*` for method-definition rules). Used by the configuration
10
- # `disable:` list and the in-source `# rigor:disable <rule>` suppression comment system; new rules MUST
11
- # register here so user configuration can refer to them.
9
+ # rules, `dump.*` for debug helpers, `def.*` for method-definition rules, `global.*` for writes to special
10
+ # globals). Used by the configuration `disable:` list and the in-source `# rigor:disable <rule>`
11
+ # suppression comment system; new rules MUST register here so user configuration can refer to them.
12
12
  #
13
13
  # ADR-87 WD4 — this pure-data constant table is split out of the (engine-heavy) `check_rules.rb` so
14
14
  # {Analysis::RuleCatalog} — which the CLI's JSON `evidence_tier` / `documentation_url` enrichment reads
@@ -66,6 +66,18 @@ module Rigor
66
66
  # annotation must never turn collection on by itself (that would be a project-wide cost cliff
67
67
  # nobody asked for), and it must equally never be silently inert.
68
68
  RULE_EFFECT_ANNOTATIONS_UNCHECKED = "effect.annotations-unchecked"
69
+ # ADR-112 WD5 / #1075 — two statements of one member's contract that contradict: a `sig/` declaration
70
+ # against an inline `@rbs` / `#:` one (and, once #1073 reads it, `@rbs` / `#:` against `@extrbs`), or a
71
+ # `rigor:v1:` refinement outside the member's own declared type. `rbs.` because the finding is about
72
+ # the RBS the environment is built from, whichever file spelled it; not `rbs_extended.`, whose rows
73
+ # are payload validity, since most contradictions involve no `RBS::Extended` annotation at all.
74
+ RULE_CONTRADICTING_SIGNATURE = "rbs.contradicting-signature"
75
+ # Issue #1367 (ADR-117 WD2) — the `global.*` family: a write to a special global that its setter rejects, so
76
+ # the write raises on every run. Two ids so a project can tune the type check without hiding read-only
77
+ # writes; the `write` wording follows `def.ivar-write-mismatch`. The envelope is the interpreter's setter
78
+ # (`SpecialGlobalSetters`), not the global's RBS declaration.
79
+ RULE_GLOBAL_WRITE_TYPE_MISMATCH = "global.write-type-mismatch"
80
+ RULE_GLOBAL_READONLY_WRITE = "global.readonly-write"
69
81
 
70
82
  ALL_RULES = [
71
83
  RULE_UNDEFINED_METHOD,
@@ -91,6 +103,8 @@ module Rigor
91
103
  RULE_OVERRIDE_RETURN_WIDENED,
92
104
  RULE_OVERRIDE_PARAM_NARROWED,
93
105
  RULE_IVAR_WRITE_MISMATCH,
106
+ RULE_GLOBAL_WRITE_TYPE_MISMATCH,
107
+ RULE_GLOBAL_READONLY_WRITE,
94
108
  RULE_SUPPRESSION_UNKNOWN_RULE,
95
109
  RULE_SUPPRESSION_EMPTY,
96
110
  RULE_SUPPRESSION_UNKNOWN_MARKER,
@@ -98,7 +112,8 @@ module Rigor
98
112
  RULE_EFFECT_ENVELOPE_EXCEEDED,
99
113
  RULE_EFFECT_LISKOV_WIDENED,
100
114
  RULE_EFFECT_UNKNOWN_LABEL,
101
- RULE_EFFECT_ANNOTATIONS_UNCHECKED
115
+ RULE_EFFECT_ANNOTATIONS_UNCHECKED,
116
+ RULE_CONTRADICTING_SIGNATURE
102
117
  ].freeze
103
118
 
104
119
  # Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
@@ -128,7 +143,7 @@ module Rigor
128
143
 
129
144
  # Family wildcard — a `<family>` token in a suppression comment or `disable:` list disables every rule
130
145
  # whose canonical id starts with `<family>.`. Per ADR-8 § "1".
131
- RULE_FAMILIES = %w[call flow assert dump def suppression static effect].freeze
146
+ RULE_FAMILIES = %w[call flow assert dump def global suppression static effect].freeze
132
147
 
133
148
  # Families of diagnostics the engine emits OUTSIDE the CheckRules catalogue (aggregator-level and
134
149
  # reporter-level diagnostics such as `rbs_extended.unsatisfied-conformance`,
@@ -50,6 +50,7 @@ module Rigor
50
50
  Struct: %i[new],
51
51
  Data: %i[define]
52
52
  }.freeze
53
+ Ractor.make_shareable(DETACHED_CLASS_FACTORIES)
53
54
  private_constant :DETACHED_CLASS_FACTORIES
54
55
 
55
56
  EMPTY_METHODS = [].freeze
@@ -108,7 +109,7 @@ module Rigor
108
109
  end
109
110
  end
110
111
 
111
- # The context the children of `node` descend under. Mirrors {RuleWalk.descend}.
112
+ # Mirrors {RuleWalk.descend}.
112
113
  def descend(node, context)
113
114
  RuleWalk.descend(node, context)
114
115
  end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../reflection"
4
+ require_relative "../../source/parameter_envelope"
5
+
6
+ module Rigor
7
+ module Analysis
8
+ module CheckRules
9
+ # Issue #992 — the positional envelope `call.wrong-arity` may check a call against when no signature
10
+ # declares the method: the {Source::ParameterEnvelope} the project's own `def` records in
11
+ # `Scope::DiscoveryIndex#discovered_parameter_envelopes`, and only when nothing the project can see
12
+ # could make a different definition of that name the one that runs.
13
+ #
14
+ # Asked in two stages, because every condition past the first can only WITHHOLD a firing:
15
+ #
16
+ # 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
17
+ # whose project mixins, records the name. Every record at that level must agree — `include` and
18
+ # `prepend` share one table, so a mixin's definition may shadow the class's own — and none may be
19
+ # opaque. A call that fits this envelope is silent, and nothing below is consulted.
20
+ # 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
21
+ # `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
22
+ # project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
23
+ # a plugin's synthetic method names it; when the owner does not make the method public; and when any
24
+ # project subclass of the receiver — whose instances the receiver's type admits — records a different
25
+ # or opaque envelope for it.
26
+ #
27
+ # Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
28
+ # `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
29
+ # arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
30
+ # function with required keywords); and a `class_eval` whose receiver is not a constant, which the
31
+ # discovery walk cannot name.
32
+ class SourceArity
33
+ MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
34
+ private_constant :MISSING_HOOKS
35
+
36
+ # One class on the walk: its own name, the project modules its mixins resolve to (transitively through
37
+ # their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
38
+ Level = Data.define(:class_name, :modules, :externals)
39
+ private_constant :Level
40
+
41
+ def initialize(scope, method_name, kind)
42
+ @scope = scope
43
+ @method_name = method_name.to_sym
44
+ @kind = kind
45
+ @name_memo = {}
46
+ end
47
+
48
+ # The nearest definition's envelope, or nil when there is none or it is not one shape. Remembers the
49
+ # levels it walked for {#authoritative?}.
50
+ #
51
+ # Its ADR-46 reads are withheld ({Analysis::DependencyRecorder.withhold}) until the caller settles
52
+ # the verdict with {#settle_by_definitions} or {#settle_by_walk}.
53
+ def owner_envelope(class_name)
54
+ @owner_entries = []
55
+ @ambiguous = false
56
+ envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
57
+ envelope
58
+ end
59
+
60
+ # True when the nearest level recorded the name but not as one envelope.
61
+ def ambiguous? = @ambiguous
62
+
63
+ # For a silent verdict that only a definition can overturn — the call fits, the owner takes a required
64
+ # keyword, or nothing on the chain records the name: the owner `def`s themselves (their symbol edges)
65
+ # and a definition appearing at a level the walk passed over (a name-keyed negative edge). A mixin or
66
+ # reopening added at the owner's level makes it opaque, and everything {#authoritative?} reads can only
67
+ # withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
68
+ def settle_by_definitions
69
+ return if @withheld.nil?
70
+
71
+ @owner_entries.each do |name, kind|
72
+ if kind == :singleton
73
+ @scope.user_singleton_def_site_for(name, @method_name)
74
+ else
75
+ @scope.user_def_site_for(name, @method_name)
76
+ end
77
+ end
78
+ separator = @kind == :singleton ? "." : "#"
79
+ @passed.each { |name| Analysis::DependencyRecorder.read_missing(:method, "#{name}#{separator}#{@method_name}") }
80
+ end
81
+
82
+ # For every other verdict — a firing, or an opaque owner level whose opacity another file's edit can
83
+ # lift — the walk's reads are the dependency, so they are recorded as read.
84
+ def settle_by_walk
85
+ Analysis::DependencyRecorder.replay(@withheld) unless @withheld.nil?
86
+ end
87
+
88
+ # Stage 2, for the class {#owner_envelope} last answered.
89
+ def authoritative?(class_name)
90
+ !load_order_dependent?(class_name) && !object_extension_may_shadow? &&
91
+ @levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
92
+ chain_free_of_hooks?(class_name) &&
93
+ subclasses_agree?(class_name, @envelope)
94
+ end
95
+
96
+ private
97
+
98
+ def walk_to_owner(class_name)
99
+ @levels = []
100
+ @passed = []
101
+ walked_whole_chain?(class_name) do |current|
102
+ level = level_for(current)
103
+ @levels << level
104
+ found = level_envelopes(level)
105
+ if found.empty?
106
+ @passed.concat([current] + level.modules)
107
+ next
108
+ end
109
+
110
+ envelope = found.first
111
+ @owner_entries = owner_entries(level)
112
+ unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
113
+ @ambiguous = true
114
+ return nil
115
+ end
116
+
117
+ return @envelope = envelope
118
+ end
119
+ nil
120
+ end
121
+
122
+ def owner_entries(level)
123
+ own = @scope.parameter_envelopes_of(level.class_name).key?([@kind, @method_name])
124
+ entries = own ? [[level.class_name, @kind]] : []
125
+ level.modules.each do |mod|
126
+ entries << [mod, :instance] if @scope.parameter_envelopes_of(mod).key?([:instance, @method_name])
127
+ end
128
+ entries
129
+ end
130
+
131
+ # The survey corpus's own lesson. `PStore.new(path).transaction { … }` inside `module TDiary::IO`
132
+ # resolves `PStore` to the project's `TDiary::IO::PStore` when that class's file is loaded, and to the
133
+ # stdlib `::PStore` when it is not — and tdiary loads it only under one `io_class` setting, so under
134
+ # the default one the call is correct code. The analysis universe holds every file at once, so it
135
+ # always picks the inner class. Whenever the receiver's class is one of SEVERAL known classes the
136
+ # call site's `Module.nesting` spells with the same last segment, which one the receiver is depends
137
+ # on load order, and a verdict against the project class's `def` is not one the program backs.
138
+ def load_order_dependent?(class_name)
139
+ segment = class_name.to_s.split("::").last
140
+ candidates = Rigor::Reflection.lexical_nesting_chain(@scope).map { |entry| "#{entry}::#{segment}" }
141
+ candidates << segment
142
+ known = candidates.uniq.select { |name| known_class?(name) }
143
+ known.include?(class_name.to_s) && known.size > 1
144
+ end
145
+
146
+ # A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
147
+ # the name at all, that object answers with the module's definition.
148
+ def object_extension_may_shadow?
149
+ @scope.discovered_parameter_envelopes.any? do |_name, bucket|
150
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
151
+ end
152
+ end
153
+
154
+ def known_class?(name)
155
+ @scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
156
+ end
157
+
158
+ # False when the walk stopped at the ADR-41 budget rather than at the top of the chain: budget
159
+ # exhaustion is uncertainty, and every caller reads it as a reason to decline.
160
+ def walked_whole_chain?(class_name)
161
+ current = class_name.to_s
162
+ seen = {}
163
+ while current && !seen[current]
164
+ return false if seen.size >= Scope::ANCESTOR_WALK_LIMIT
165
+
166
+ seen[current] = true
167
+ yield current
168
+ current = resolve(current, @scope.superclass_of(current))
169
+ end
170
+ true
171
+ end
172
+
173
+ def level_for(class_name)
174
+ modules = []
175
+ externals = []
176
+ collect_mixins(class_name, own_mixins(class_name), modules, externals, {})
177
+ Level.new(class_name: class_name, modules: modules, externals: externals)
178
+ end
179
+
180
+ # Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
181
+ # an extended module's own `include`s reach the same singleton.
182
+ def own_mixins(class_name)
183
+ @kind == :singleton ? (@scope.discovered_extends[class_name] || []) : @scope.includes_of(class_name)
184
+ end
185
+
186
+ # Issue #986 — a name the compact-header rename collision left ambiguous resolves to no ONE class,
187
+ # but BOTH classes it names are ancestors at runtime; only their MRO order is unknowable. Taking
188
+ # both as levels is what keeps this rule alive for the rest of the receiver: a method only one of
189
+ # them declares still has one envelope and still fires, a method they disagree about lands two on
190
+ # `#envelope_for`'s join and declines there, and the class's own `def`s, its superclass's and its
191
+ # unambiguous mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead
192
+ # suppressed the rule for every level of the receiver.
193
+ def collect_mixins(owner, raw_names, modules, externals, seen)
194
+ raw_names.each do |raw|
195
+ resolved = resolve(owner, raw)
196
+ names = resolved ? [resolved] : @scope.ambiguous_ancestor_resolutions(owner, raw)
197
+ next externals << @scope.ancestor_name_candidates(owner, raw) if names.empty?
198
+
199
+ names.each do |name|
200
+ next if seen[name]
201
+
202
+ seen[name] = true
203
+ modules << name
204
+ collect_mixins(name, @scope.includes_of(name), modules, externals, seen)
205
+ end
206
+ end
207
+ end
208
+
209
+ def resolve(owner, raw)
210
+ return nil if raw.nil?
211
+
212
+ bucket = (@name_memo[owner] ||= {})
213
+ return bucket[raw] if bucket.key?(raw)
214
+
215
+ bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
216
+ end
217
+
218
+ def project_class?(name)
219
+ @scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
220
+ end
221
+
222
+ # A module's methods reach the receiver as instance methods whichever side the receiver is on.
223
+ def level_envelopes(level)
224
+ own = @scope.parameter_envelopes_of(level.class_name)[[@kind, @method_name]]
225
+ mixed = level.modules.map { |mod| @scope.parameter_envelopes_of(mod)[[:instance, @method_name]] }
226
+ ([own] + mixed).compact
227
+ end
228
+
229
+ def clean_level?(level)
230
+ ([level.class_name] + level.modules).none? { |name| dynamic_surface?(name) || project_patched?(name) } &&
231
+ level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
232
+ end
233
+
234
+ def dynamic_surface?(class_name)
235
+ Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
236
+ end
237
+
238
+ def project_patched?(class_name)
239
+ environment = @scope.environment
240
+ patched = environment&.project_patched_methods
241
+ return true if patched && !patched.empty? && patch_names_method?(patched, class_name)
242
+
243
+ synthetic = environment&.synthetic_method_index
244
+ return false if synthetic.nil? || synthetic.empty?
245
+
246
+ synthetic.knows_class?(class_name) || !synthetic.lookup_instance(class_name, @method_name).empty? ||
247
+ !synthetic.lookup_singleton(class_name, @method_name).empty?
248
+ end
249
+
250
+ def patch_names_method?(patched, class_name)
251
+ %i[instance singleton].any? do |kind|
252
+ !patched.lookup(class_name: class_name, method_name: @method_name, kind: kind).nil?
253
+ end
254
+ end
255
+
256
+ # A mixin the project does not declare may define the name with any arity, and `prepend` lets it win
257
+ # over the class's own `def`. Only a module RBS knows, and whose declaration lacks the name, is
258
+ # evidence that it does not.
259
+ def external_mixin_lacks_method?(candidates)
260
+ name = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
261
+ return false if name.nil?
262
+
263
+ Rigor::Reflection.instance_method_definition(name, @method_name, scope: @scope).nil?
264
+ rescue StandardError
265
+ false
266
+ end
267
+
268
+ def public_at?(class_name)
269
+ visibility = @scope.discovered_method_visibility(class_name, @method_name)
270
+ visibility.nil? || visibility == :public
271
+ end
272
+
273
+ # A hook anywhere in the chain, including above the owner, and on either side: a class that answers
274
+ # names it does not define is a class whose `def`s are not the whole story of what a call reaches.
275
+ def chain_free_of_hooks?(class_name)
276
+ walked_whole_chain?(class_name) do |current|
277
+ ([current] + level_for(current).modules).each do |name|
278
+ envelopes = @scope.parameter_envelopes_of(name)
279
+ return false if dynamic_surface?(name)
280
+ return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
281
+ end
282
+ end
283
+ end
284
+
285
+ def hook_recorded?(envelopes, hook)
286
+ envelopes.key?([:instance, hook]) || envelopes.key?([:singleton, hook])
287
+ end
288
+
289
+ # Every project class whose superclass chain reaches the receiver's class: a value typed as the
290
+ # receiver may be any of them, and each one's own definition of the name is what runs for it.
291
+ def subclasses_agree?(class_name, envelope)
292
+ each_subclass(class_name) do |subclass|
293
+ level = level_for(subclass)
294
+ return false unless clean_level?(level)
295
+ return false unless level_envelopes(level).all?(envelope)
296
+ end
297
+ true
298
+ end
299
+
300
+ def each_subclass(class_name)
301
+ children = children_by_parent
302
+ queue = children.fetch(class_name.to_s, []).dup
303
+ seen = {}
304
+ until queue.empty?
305
+ subclass = queue.shift
306
+ next if seen[subclass]
307
+
308
+ seen[subclass] = true
309
+ yield subclass
310
+ queue.concat(children.fetch(subclass, []))
311
+ end
312
+ end
313
+
314
+ def children_by_parent
315
+ @scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
316
+ parent = resolve(child, raw_parent)
317
+ (children[parent] ||= []) << child if parent
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end