rigortype 0.3.8 → 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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
data/lib/rigor/scope.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "analysis/fact_store"
7
7
  require_relative "analysis/dependency_recorder"
8
8
  require_relative "inference/expression_typer"
9
9
  require_relative "inference/flow_tracer"
10
+ require_relative "inference/optimistic_origin"
10
11
  require_relative "inference/statement_evaluator"
11
12
  require_relative "inference/def_node_resolver"
12
13
 
@@ -21,12 +22,14 @@ module Rigor
21
22
  attr_reader :environment, :locals, :fact_store, :self_type,
22
23
  :ivars, :cvars, :globals,
23
24
  :indexed_narrowings, :method_chain_narrowings,
24
- :declaration_sourced,
25
+ :declaration_sourced, :published_constant_sourced,
25
26
  :source_path, :discovery, :struct_fold_safe_locals,
26
- :opaque_block_self, :lexical_nesting,
27
+ :opaque_block_self, :singleton_class_body, :lexical_nesting,
27
28
  :dynamic_origins, :local_origins, :ivar_origins,
28
29
  :void_origins, :plugin_typed_calls,
29
- :optimistic_origins, :optimistic_locals, :optimistic_ivars
30
+ :optimistic_origins, :optimistic_locals, :optimistic_ivars,
31
+ :repeated_or_writes, :match_frame,
32
+ :constant_narrowings, :guard_records
30
33
 
31
34
  # ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
32
35
  # reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
@@ -47,14 +50,58 @@ module Rigor
47
50
  def discovered_def_sources = @discovery.discovered_def_sources
48
51
  def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources
49
52
  def discovered_method_visibilities = @discovery.discovered_method_visibilities
53
+ def discovered_parameter_envelopes = @discovery.discovered_parameter_envelopes
50
54
  def discovered_superclasses = @discovery.discovered_superclasses
55
+
56
+ # Issue #1097 — `{file path => [[start_offset, end_offset, name, kind, owner], ...]}`, the def /
57
+ # block / lambda body ranges {singleton_def_shadows_call?} / {instance_def_shadows_call?} order a
58
+ # project-defined override against.
59
+ def discovered_deferred_ranges = @discovery.discovered_deferred_ranges
60
+ # Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
61
+ # `refine X do … end` block defines, visible only where a `using` of its module is in effect.
62
+ def discovered_refinements = @discovery.discovered_refinements
63
+ # Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
64
+ def discovered_global_write_census = @discovery.discovered_global_write_census
51
65
  def discovered_includes = @discovery.discovered_includes
66
+ # Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
67
+ # instance-ancestor search order (nearest prepend first). Feeds {#prepends_of}, the one table that tells
68
+ # a `prepend` from an `include` at `def`-priority level.
69
+ def discovered_prepends = @discovery.discovered_prepends
70
+ def discovered_extends = @discovery.discovered_extends
52
71
  def discovered_class_sources = @discovery.discovered_class_sources
53
72
  # Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
54
73
  def constant_sources = @discovery.constant_sources
55
74
  def published_constant_names = @discovery.published_constant_names
56
75
  def local_constant_names = @discovery.local_constant_names
57
76
 
77
+ # Issue #617 — the census names sharing `name`'s last segment that bind, spelled as the census spells
78
+ # them (a `*::LIMIT` wildcard included), so the caller still decides which of them a reference resolves
79
+ # to. A `||=`-only name is left out while no other file memoizes its segment: the memoization idiom's
80
+ # own write, however often one file repeats it, is not what binds it.
81
+ def bound_constant_names(name)
82
+ @discovery.constant_writers[name.split("::").last] || EMPTY_BOUND_CONSTANT_NAMES
83
+ end
84
+
85
+ # Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
86
+ # assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
87
+ # (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
88
+ # as it stands rather than split.
89
+ def shadowing_constant_names(name)
90
+ segment = name.include?("::") ? name.split("::").last : name
91
+ @discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
92
+ end
93
+
94
+ EMPTY_BOUND_CONSTANT_NAMES = [].freeze
95
+ private_constant :EMPTY_BOUND_CONSTANT_NAMES
96
+
97
+ # Issue #667 — the instance-variable names of `class_name` whose class-ivar seed comes from a foreign
98
+ # published constant (`@mode = AppConfig::MODE` in `initialize`, read in a sibling method).
99
+ # `StatementEvaluator#seed_instance_ivars` stamps the flow mark for these at method-body entry, the way
100
+ # ADR-58's seed stamps its own; a method-local write or narrowing then drops it through `with_ivar`.
101
+ def published_constant_ivars_for(class_name)
102
+ @discovery.published_constant_ivars[class_name] || EMPTY_PUBLISHED_CONSTANT_IVARS
103
+ end
104
+
58
105
  # Issue #644 — true when the constant reference `name` (as written: `MODE` or `AppConfig::MODE`) is
59
106
  # published by the cross-file value-constant table and is NOT assigned by the file being analysed: a
60
107
  # value this file's author cannot see the declaration of. {Analysis::CheckRules::PublishedConstantGuard}
@@ -76,14 +123,27 @@ module Rigor
76
123
  # over already-qualified names rather than a second lexical ladder: the engine's ladder
77
124
  # (`Reflection.resolve_constant_type`) answers with a TYPE and not with the candidate that won, and
78
125
  # reimplementing the walk here is the very thing that made the receiver resolution wrong once already.
126
+ #
127
+ # Issue #667 — a third answer sits beside those two. `MODE2 = AppConfig::MODE` is assigned by this file,
128
+ # so the exemption above releases it, yet its value is still one the author never saw: the alias only
129
+ # renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
130
+ # those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
131
+ # safe one.
79
132
  def published_constant?(name)
80
133
  names = @discovery.published_constant_names
134
+ return true if published_constant_alias?(name)
81
135
  return false if names.empty?
82
136
  return false unless names.include?(name.split("::").last)
83
137
 
84
138
  !locally_declared_constant?(name)
85
139
  end
86
140
 
141
+ def published_constant_alias?(name)
142
+ aliases = @discovery.published_constant_alias_names
143
+ !aliases.empty? && aliases.include?(name.split("::").last)
144
+ end
145
+ private :published_constant_alias?
146
+
87
147
  def locally_declared_constant?(reference_name)
88
148
  local = @discovery.local_constant_names
89
149
  return false if local.empty?
@@ -153,9 +213,40 @@ module Rigor
153
213
  # threaded by reference through transitions; reset per method body (a fresh entry scope drops it), so the
154
214
  # name keys never collide across bodies.
155
215
  EMPTY_ORIGINS = {}.freeze
216
+ # Issue #667 — the set of variable references currently bound to a value COPIED out of a constant the
217
+ # project published and this file does not declare. Members are frozen `[kind, name]` pairs, the same
218
+ # spelling {EMPTY_DECLARATION_SOURCED} uses, but a DELIBERATELY SEPARATE carrier: this mark joins by
219
+ # UNION where ADR-58's kinds intersect, and it answers a different question (what a value's *constancy*
220
+ # rests on, not what a binding's *optionality* rests on) for a different consumer. Folding it into the
221
+ # ADR-58 set would be the third establishing transition its *Non-transitivity* passage reserves as a
222
+ # decision, on a Set that already carries two opposite join policies.
223
+ EMPTY_PUBLISHED_CONSTANT_SOURCED = Set.new.freeze
224
+ # Issue #667 — the empty answer of {#published_constant_ivars_for}, so a class with no such ivar (every
225
+ # class in a project that publishes nothing) allocates none.
226
+ EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
227
+ # The index `||=` sites of a repeating block body whose slot an earlier run of the body may have filled,
228
+ # keyed by node identity and laid by a block-return pass at the body's entry
229
+ # ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which). The memoizing `||=` reading
230
+ # (`StatementEvaluator#index_compound_write_value`) is withheld at such a site. The mark belongs to the
231
+ # site, so no rebind or narrowing of a variable drops it, and a join keeps a site either arm holds: the
232
+ # mark only ever withholds that reading, whose answer is the narrower one.
233
+ EMPTY_REPEATED_OR_WRITES = {}.compare_by_identity.freeze
234
+ # Issue #1429 — the narrowing a guard leaves on a constant reference (`STDOUT.is_a?(StringIO)`, `if CONFIG`),
235
+ # keyed by the reference's spelling ({Inference::Narrowing.constant_key}: `"STDOUT"`, `"::Foo::BAR"`). A constant
236
+ # has no binding of its own in the scope, so the table is what a read in the guarded edge answers
237
+ # (`ExpressionTyper#type_of_constant_read`); a spelling it does not hold resolves as before.
238
+ EMPTY_CONSTANT_NARROWINGS = {}.freeze
239
+ # Issue #1429 — what each global and constant a guard narrowed was bound to before the guard, keyed
240
+ # `[:global, :$name]` / `[:constant, key]`. Ruby may rebind a global (or `const_set` a constant) whenever code the
241
+ # analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
242
+ # binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
243
+ # the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
244
+ EMPTY_GUARD_RECORDS = {}.freeze
156
245
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
157
246
  :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
158
- :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS
247
+ :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
248
+ :EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
249
+ :EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS
159
250
 
160
251
  class << self
161
252
  def empty(environment: Environment.default, source_path: nil)
@@ -207,9 +298,11 @@ module Rigor
207
298
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
208
299
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
209
300
  declaration_sourced: EMPTY_DECLARATION_SOURCED,
301
+ published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
210
302
  source_path: nil,
211
303
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
212
304
  opaque_block_self: false,
305
+ singleton_class_body: false,
213
306
  lexical_nesting: nil,
214
307
  dynamic_origins: {}.compare_by_identity,
215
308
  local_origins: EMPTY_ORIGINS,
@@ -218,7 +311,11 @@ module Rigor
218
311
  plugin_typed_calls: {}.compare_by_identity,
219
312
  optimistic_origins: {}.compare_by_identity,
220
313
  optimistic_locals: EMPTY_ORIGINS,
221
- optimistic_ivars: EMPTY_ORIGINS
314
+ optimistic_ivars: EMPTY_ORIGINS,
315
+ repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
316
+ match_frame: nil,
317
+ constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
318
+ guard_records: EMPTY_GUARD_RECORDS
222
319
  )
223
320
  @environment = environment
224
321
  @locals = locals
@@ -231,9 +328,11 @@ module Rigor
231
328
  @indexed_narrowings = indexed_narrowings
232
329
  @method_chain_narrowings = method_chain_narrowings
233
330
  @declaration_sourced = declaration_sourced
331
+ @published_constant_sourced = published_constant_sourced
234
332
  @source_path = source_path
235
333
  @struct_fold_safe_locals = struct_fold_safe_locals
236
334
  @opaque_block_self = opaque_block_self
335
+ @singleton_class_body = singleton_class_body
237
336
  @lexical_nesting = lexical_nesting
238
337
  @dynamic_origins = dynamic_origins
239
338
  @local_origins = local_origins
@@ -243,6 +342,10 @@ module Rigor
243
342
  @optimistic_origins = optimistic_origins
244
343
  @optimistic_locals = optimistic_locals
245
344
  @optimistic_ivars = optimistic_ivars
345
+ @repeated_or_writes = repeated_or_writes
346
+ @match_frame = match_frame
347
+ @constant_narrowings = constant_narrowings
348
+ @guard_records = guard_records
246
349
  freeze
247
350
  end
248
351
 
@@ -255,19 +358,30 @@ module Rigor
255
358
  self
256
359
  end
257
360
 
258
- def optimistic_local(name) = @optimistic_locals[name.to_sym]
259
- def optimistic_ivar(name) = @optimistic_ivars[name.to_sym]
361
+ def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
362
+ def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
363
+
364
+ # Issue #1302 — what the value a marked local / ivar is bound to answers on a miss, as its binding recorded
365
+ # it beside the mark, or {Inference::OptimisticOrigin::UNKNOWN_MISS}. The answer lives in the mark's own
366
+ # table entry ({Inference::OptimisticOrigin::BoundMark}), so every transition that keeps or drops the mark
367
+ # keeps or drops the answer with it.
368
+ def optimistic_local_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_locals[name.to_sym])
369
+ def optimistic_ivar_miss(name) = Inference::OptimisticOrigin.bound_miss(@optimistic_ivars[name.to_sym])
260
370
 
261
- def with_optimistic_local(name, cause)
371
+ # `miss` is what the bound value answers when the bet fails ({Inference::OptimisticOrigin.miss_answer}); the
372
+ # default records none, so a read through the binding widens as an untold miss does.
373
+ def with_optimistic_local(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
262
374
  return self if cause.nil?
263
375
 
264
- rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => cause).freeze)
376
+ mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
377
+ rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => mark).freeze)
265
378
  end
266
379
 
267
- def with_optimistic_ivar(name, cause)
380
+ def with_optimistic_ivar(name, cause, miss: Inference::OptimisticOrigin::UNKNOWN_MISS)
268
381
  return self if cause.nil?
269
382
 
270
- rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => cause).freeze)
383
+ mark = Inference::OptimisticOrigin.bound_mark(cause, miss)
384
+ rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
271
385
  end
272
386
 
273
387
  # ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
@@ -295,6 +409,13 @@ module Rigor
295
409
  end
296
410
 
297
411
  def with_local(name, type)
412
+ bind_local(name, type, keep_marks: false)
413
+ end
414
+
415
+ # The one body behind {#with_local} and {#with_mutated_local}, which differ only in whether the two marks a
416
+ # write drops survive. Passing the kept sets through unchanged, rather than dropping and re-adding them, keeps
417
+ # a mutation's rebind to the one `rebuild` a write costs.
418
+ def bind_local(name, type, keep_marks:)
298
419
  # `rigor trace` — the moment a local enters the scope.
299
420
  Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
300
421
  new_locals = @locals.merge(name.to_sym => type).freeze
@@ -313,10 +434,15 @@ module Rigor
313
434
  rebuild(locals: new_locals, fact_store: new_fact_store,
314
435
  indexed_narrowings: new_indexed_narrowings,
315
436
  method_chain_narrowings: new_chain_narrowings,
316
- declaration_sourced: drop_declaration_sourced_for(:local, name),
437
+ declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
438
+ # Issue #667 — rebinding is flow-live for the published-constant mark too: the new value need
439
+ # not be a copy of anything. `with_published_constant_mark` re-stamps afterward when the write's
440
+ # rvalue is one.
441
+ published_constant_sourced: drop_published_constant_sourced_for(:local, name),
317
442
  local_origins: drop_origin(@local_origins, name),
318
- optimistic_locals: drop_origin(@optimistic_locals, name))
443
+ optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name))
319
444
  end
445
+ private :bind_local
320
446
 
321
447
  def with_fact(fact)
322
448
  rebuild(fact_store: fact_store.with_fact(fact))
@@ -380,6 +506,27 @@ module Rigor
380
506
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
381
507
  def opaque_block_self? = @opaque_block_self
382
508
 
509
+ # Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
510
+ # Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
511
+ # which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
512
+ # body it defines a CLASS method (`self` there is the singleton class), while in every other body reached
513
+ # with a `Singleton[X]` self — a `class X` body, `def self.x`, a `def` inside `class << self` — `self` is the
514
+ # class object and the same call defines an INSTANCE method.
515
+ #
516
+ # The mark is stamped at singleton-class-body entry and inherited by every scope derived inside it, blocks
517
+ # included; a `def` body starts from a fresh scope and therefore clears it by construction, which is exactly
518
+ # the boundary the distinction needs. A meta-class body (`Class.new do ... end`) clears it explicitly: that
519
+ # block is a class body of its own, whatever encloses it.
520
+ def with_singleton_class_body(flag)
521
+ return self if @singleton_class_body == flag
522
+
523
+ rebuild(singleton_class_body: flag)
524
+ end
525
+
526
+ # True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
527
+ # answers false).
528
+ def singleton_class_body? = @singleton_class_body
529
+
383
530
  # True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
384
531
  # aliased / escaped).
385
532
  def struct_fold_safe?(name)
@@ -419,6 +566,7 @@ module Rigor
419
566
  indexed_narrowings: new_indexed_narrowings,
420
567
  method_chain_narrowings: new_chain_narrowings,
421
568
  declaration_sourced: drop_declaration_sourced_for(:ivar, name),
569
+ published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
422
570
  ivar_origins: drop_origin(@ivar_origins, name),
423
571
  optimistic_ivars: drop_origin(@optimistic_ivars, name))
424
572
  end
@@ -445,9 +593,41 @@ module Rigor
445
593
  rebuild(declaration_sourced: add_declaration_sourced(:local, name))
446
594
  end
447
595
 
596
+ # Issue #1287 — rebinds `name` for an in-place mutation of the object it already holds: a mutator's widening
597
+ # (`r << x`), a content floor after a closure or callee mutated it, an element or member write through it. The
598
+ # binding still names the same object, so this is not a flow-live write, and the marks `with_local` drops stay:
599
+ # ADR-58's declaration-sourced mark and issue #286's optimistic nil-freeness mark, with the miss answer issue
600
+ # #1302 records in it. A source-level write keeps going through `with_local`, which drops both.
601
+ #
602
+ # Two other per-local tables are deliberately still dropped. An ADR-82 `local_origins` cause explains the
603
+ # `Dynamic` the ASSIGNMENT bound, and a floor's `Dynamic[top]` has a different cause. Issue #667's
604
+ # published-constant mark can never be live here: only a frozen scalar publishes, and a frozen value is not
605
+ # mutated in place.
606
+ def with_mutated_local(name, type)
607
+ bind_local(name, type, keep_marks: true)
608
+ end
609
+
610
+ # Issue #667 — record that `name` is currently bound to a value copied out of a foreign published
611
+ # constant. Always applied AFTER the `with_local` / `with_ivar` that binds the value (both drop the mark
612
+ # unconditionally), exactly as {#with_local_declaration_mark} is.
613
+ def with_published_constant_mark(kind, name)
614
+ rebuild(published_constant_sourced: add_published_constant_sourced(kind, name))
615
+ end
616
+
617
+ # Issue #667 — true when `(kind, name)`'s current binding is a copy of a constant the project published
618
+ # and this file does not declare. Asked through
619
+ # {Analysis::CheckRules::PublishedConstantGuard}, never directly from a rule.
620
+ def published_constant_sourced?(kind, name)
621
+ return false if @published_constant_sourced.empty?
622
+
623
+ @published_constant_sourced.include?([kind.to_sym, name.to_sym])
624
+ end
625
+
448
626
  # ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely declaration-sourced (no flow-live
449
627
  # write/narrowing has touched it).
450
628
  def declaration_sourced?(kind, name)
629
+ return false if @declaration_sourced.empty?
630
+
451
631
  @declaration_sourced.include?([kind.to_sym, name.to_sym])
452
632
  end
453
633
 
@@ -491,24 +671,287 @@ module Rigor
491
671
  rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
492
672
  end
493
673
 
674
+ # Issue #1362 — a write or narrowing of a global is flow-live, so it drops the ADR-58 `:global` mark
675
+ # {#seed_declaration_sourced_global} stamped on the program-global seed.
676
+ #
677
+ # Issue #1429 — `$stdout` and `$>` are one variable, so a write to either also restores a guard's narrowing of the
678
+ # other ({#forget_guard_narrowing_of}).
494
679
  def with_global(name, type)
495
- rebuild(globals: @globals.merge(name.to_sym => type).freeze)
680
+ name = name.to_sym
681
+ written = rebuild(globals: @globals.merge(name => type).freeze,
682
+ declaration_sourced: drop_declaration_sourced_for(:global, name),
683
+ guard_records: drop_guard_record(:global, name))
684
+ alias_name = STDOUT_ALIASES[name]
685
+ alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
686
+ end
687
+
688
+ STDOUT_ALIASES = { :$stdout => :$>, :$> => :$stdout }.freeze
689
+ private_constant :STDOUT_ALIASES
690
+
691
+ # Issue #1429 — {#forget_guard_narrowings} for the one name `[kind, name]`.
692
+ def forget_guard_narrowing_of(kind, name)
693
+ key = [kind, name]
694
+ pre_guard = @guard_records[key]
695
+ return self if pre_guard.nil?
696
+
697
+ if kind == :global
698
+ current = @globals[name]
699
+ globals = current ? @globals.merge(name => Type::Combinator.union(pre_guard, current)).freeze : @globals
700
+ rebuild(globals: globals, guard_records: @guard_records.except(key).freeze)
701
+ else
702
+ current = @constant_narrowings[name]
703
+ constants = if current
704
+ @constant_narrowings.merge(name => Type::Combinator.union(pre_guard, current)).freeze
705
+ else
706
+ @constant_narrowings
707
+ end
708
+ rebuild(constant_narrowings: constants, guard_records: @guard_records.except(key).freeze)
709
+ end
710
+ end
711
+
712
+ # Issue #1429 — binds the global `name` to `type` on a guard's edge, recording `pre_guard`, the binding the guard
713
+ # narrowed, unless an earlier guard already recorded one ({EMPTY_GUARD_RECORDS}). `record: false` is the
714
+ # frame-local specials' form, which binds without a record: their own machinery forgets them.
715
+ def with_guarded_global(name, type, pre_guard, record: true)
716
+ name = name.to_sym
717
+ records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
718
+ rebuild(globals: @globals.merge(name => type).freeze,
719
+ declaration_sourced: drop_declaration_sourced_for(:global, name),
720
+ guard_records: records)
721
+ end
722
+
723
+ # Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
724
+ def constant_narrowing(key)
725
+ return nil if @constant_narrowings.empty?
726
+
727
+ @constant_narrowings[key]
728
+ end
729
+
730
+ # Issue #1429 — the constant reference `key` read as `type` on a guard's edge, recording `pre_guard` as
731
+ # {#with_guarded_global} does.
732
+ def with_constant_narrowing(key, type, pre_guard)
733
+ rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
734
+ guard_records: add_guard_record([:constant, key].freeze, pre_guard))
735
+ end
736
+
737
+ # Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
738
+ # named `name` leaves, since `Foo::BAR` and `BAR` may name the one it wrote.
739
+ def without_constant_narrowings_named(name)
740
+ keys = @constant_narrowings.keys.select { |key| key.split("::").last == name }
741
+ return self if keys.empty?
742
+
743
+ records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
744
+ rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze)
745
+ end
746
+
747
+ # Issue #1429 — the constant reference `key` with no narrowing.
748
+ def without_constant_narrowing(key)
749
+ return self unless @constant_narrowings.key?(key)
750
+
751
+ rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
752
+ guard_records: @guard_records.except([:constant, key]).freeze)
753
+ end
754
+
755
+ # True when a guard's narrowing of a global or constant is live, the state {#forget_guard_narrowings} drops and
756
+ # so the gate on every scan that decides whether to.
757
+ def guard_narrowed?
758
+ !@guard_records.empty?
759
+ end
760
+
761
+ # Issue #1429 — this scope past code that may rebind a global or a constant: each one a guard narrowed reads
762
+ # the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
763
+ # pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
764
+ # helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
765
+ def forget_guard_narrowings
766
+ return self if @guard_records.empty?
767
+
768
+ globals = @globals
769
+ constants = @constant_narrowings
770
+ @guard_records.each do |(kind, name), pre_guard|
771
+ if kind == :global
772
+ current = globals[name]
773
+ globals = globals.merge(name => Type::Combinator.union(pre_guard, current)) if current
774
+ else
775
+ current = constants[name]
776
+ constants = constants.merge(name => Type::Combinator.union(pre_guard, current)) if current
777
+ end
778
+ end
779
+ rebuild(globals: globals.frozen? ? globals : globals.freeze,
780
+ constant_narrowings: constants.frozen? ? constants : constants.freeze,
781
+ guard_records: EMPTY_GUARD_RECORDS)
782
+ end
783
+
784
+ # Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
785
+ # global Ruby's own signatures declare to its declared type joined with the file's writes, and to record that the
786
+ # binding is still that seed. The declared members are real type information but not diagnostic fuel
787
+ # ({Analysis::CheckRules::DeclarationSourcedGuard}); a write or narrowing drops the mark ({#with_global}).
788
+ def seed_declaration_sourced_global(name, type)
789
+ rebuild(globals: @globals.merge(name.to_sym => type).freeze,
790
+ declaration_sourced: add_declaration_sourced(:global, name))
791
+ end
792
+
793
+ # Issue #1362 — a local written from a read of a marked global (`sep = $/`) carries ADR-58's `:local` mark, and
794
+ # this records the globals it may copy, so a consumer can compare against the file's writes to them
795
+ # ({#declaration_sourced_global_copies}). Applied after {#with_declaration_sourced_local}; the local's next
796
+ # rebinding drops both ({#bind_local}), and a join of two copies keeps both branches' globals.
797
+ def with_global_copy_marks(name, globals)
798
+ name = name.to_sym
799
+ added = globals.map { |global| [:global_copy, name, global.to_sym].freeze }
800
+ .reject { |ref| @declaration_sourced.include?(ref) }
801
+ return self if added.empty?
802
+
803
+ rebuild(declaration_sourced: @declaration_sourced.dup.merge(added).freeze)
804
+ end
805
+
806
+ # Issue #1362 — this scope with the local `name`'s ADR-58 mark and copy record dropped and its binding kept: a
807
+ # retried pass that re-enters with a binding the accumulated one accepts, but from another source.
808
+ def without_local_declaration_marks(name)
809
+ dropped = drop_local_declaration_marks(name)
810
+ dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
811
+ end
812
+
813
+ # The globals the local `name` is a marked copy of ({#with_global_copy_marks}), empty for a local that copies
814
+ # no marked global.
815
+ def declaration_sourced_global_copies(name)
816
+ return EMPTY_GLOBAL_COPIES unless declaration_sourced?(:local, name)
817
+
818
+ name = name.to_sym
819
+ @declaration_sourced.filter_map { |ref| ref[2] if ref[0] == :global_copy && ref[1] == name }
820
+ end
821
+ EMPTY_GLOBAL_COPIES = [].freeze
822
+ private_constant :EMPTY_GLOBAL_COPIES
823
+
824
+ # Mark `nodes`, index `||=` sites, as ones whose slot an earlier run of a repeating block body may have
825
+ # filled ({EMPTY_REPEATED_OR_WRITES}).
826
+ def with_repeated_or_writes(nodes)
827
+ return self if nodes.all? { |node| @repeated_or_writes.key?(node) }
828
+
829
+ marked = @repeated_or_writes.dup
830
+ nodes.each { |node| marked[node] = true }
831
+ rebuild(repeated_or_writes: marked.freeze)
832
+ end
833
+
834
+ # True when {#with_repeated_or_writes} marked the index `||=` node `node` (by identity).
835
+ def repeated_or_write?(node)
836
+ return false if @repeated_or_writes.empty?
837
+
838
+ @repeated_or_writes.key?(node)
496
839
  end
497
840
 
498
841
  # Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match and last-paren back-references). Narrowed
499
- # on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`); any
500
- # subsequent method call could run another match and rebind every one of them, so `eval_call` forgets the
501
- # narrowed facts here. Always safe — only drops facts, so a subsequent read falls back to the default
502
- # `String | nil`. Program-level `$GLOBAL = ...` seeds use other names and are untouched.
842
+ # on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`). They live in
843
+ # the method frame's special-variable slot, which the method's blocks and closures share (issue #1358), so a
844
+ # later call that may run a match in this frame — a match-capable call, a block that may match, a call once
845
+ # the frame has made a closure that may — rebinds every one of them, and `eval_call` forgets the narrowed
846
+ # facts here. Always safe — only drops facts, so a subsequent read falls back to the default `String | nil`.
847
+ # Program-level `$GLOBAL = ...` seeds use other names and are untouched.
503
848
  MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
504
849
  private_constant :MATCH_DATA_GLOBALS
505
850
 
506
851
  def forget_match_globals
507
- return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) }
852
+ return self unless match_globals_bound?
508
853
 
509
854
  rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
510
855
  end
511
856
 
857
+ # Issue #1361 — this scope with every bound match-data global rebound to `Dynamic[top]`, and an unbound one left
858
+ # unbound: the view a `define_method` / `define_singleton_method` body enters with
859
+ # ({Inference::FreshFrameBlocks.entry}). The body reads the defining frame's slot whenever the method is called,
860
+ # which the narrowing where it is written neither proves nor refutes, so it is neither narrowed nor flagged.
861
+ def untyped_match_globals
862
+ return self unless match_globals_bound?
863
+
864
+ untyped = Type::Combinator.untyped
865
+ rebound = MATCH_DATA_GLOBALS.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
866
+ rebuild(globals: @globals.merge(rebound).freeze)
867
+ end
868
+
869
+ # True when any match-data global holds a binding, which a match edge or {#untyped_match_globals} makes: the only
870
+ # state {#forget_match_globals} can drop, and so the gate on every scan that decides whether to.
871
+ def match_globals_bound?
872
+ !@globals.empty? && MATCH_DATA_GLOBALS.any? { |name| @globals.key?(name) }
873
+ end
874
+
875
+ # Issue #1359 — the last line read, `$_`, lives in the same frame slot as the match globals, and so on the same
876
+ # terms: a `gets`-family call, a write, or a block or closure of the frame that may run either rebinds it
877
+ # ({Inference::LastLine}). It is bound only where a condition on a reader narrows it, or where code writes it.
878
+ LAST_LINE = :$_
879
+ private_constant :LAST_LINE
880
+
881
+ def forget_last_line
882
+ return self unless last_line_bound?
883
+
884
+ rebuild(globals: @globals.except(LAST_LINE).freeze)
885
+ end
886
+
887
+ # The `$_` half of {#untyped_match_globals}: a bound `$_` rebound to `Dynamic[top]`, an unbound one left alone.
888
+ def untyped_last_line
889
+ return self unless last_line_bound?
890
+
891
+ rebuild(globals: @globals.merge(LAST_LINE => Type::Combinator.untyped).freeze)
892
+ end
893
+
894
+ # The gate on every scan that decides whether to forget `$_`, as {#match_globals_bound?} is for the match globals.
895
+ def last_line_bound?
896
+ !@globals.empty? && @globals.key?(LAST_LINE)
897
+ end
898
+
899
+ # Issue #1360 — the exception being rescued, `$!`, and its backtrace, `$@`. Ruby finds them through the nearest
900
+ # rescue frame of the running execution context, not through the method frame, so they are bound only inside a
901
+ # `rescue` clause or a rescue modifier's fallback and read their earlier binding again once it exits
902
+ # ({Inference::ErrorInfo}). `$?`, the status of the last child process, is thread-local, and is bound after a
903
+ # subprocess call ({Inference::LastStatus}).
904
+ ERROR_INFO_GLOBALS = %i[$! $@].freeze
905
+ LAST_STATUS_GLOBALS = %i[$?].freeze
906
+ private_constant :ERROR_INFO_GLOBALS, :LAST_STATUS_GLOBALS
907
+
908
+ # This scope with `$!` and `$@` unbound: the view of a body that runs outside the rescue clause it is written in.
909
+ def forget_error_info = forget_globals(ERROR_INFO_GLOBALS)
910
+
911
+ # This scope with a bound `$!` or `$@` rebound to `Dynamic[top]`, and an unbound one left unbound, as
912
+ # {#untyped_match_globals} gives a `define_method` body.
913
+ def untyped_error_info = untyped_globals(ERROR_INFO_GLOBALS)
914
+
915
+ # This scope with `$?` unbound: the view of a body that may run on another thread.
916
+ def forget_last_status = forget_globals(LAST_STATUS_GLOBALS)
917
+
918
+ # The `$?` half of {#untyped_error_info}.
919
+ def untyped_last_status = untyped_globals(LAST_STATUS_GLOBALS)
920
+
921
+ def forget_globals(names)
922
+ return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
923
+
924
+ rebuild(globals: @globals.except(*names).freeze)
925
+ end
926
+
927
+ def untyped_globals(names)
928
+ return self if @globals.empty? || names.none? { |name| @globals.key?(name) }
929
+
930
+ untyped = Type::Combinator.untyped
931
+ rebound = names.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
932
+ rebuild(globals: @globals.merge(rebound).freeze)
933
+ end
934
+ private :forget_globals, :untyped_globals
935
+
936
+ # Issue #1358 — stamps the frame `body` runs in ({Inference::MatchRebinding::Frame}) on a method, class or
937
+ # file body's entry scope; a method passes its `parameters` too, whose defaults run in the same frame. Every
938
+ # scope derived from it, a block's included, runs in that frame.
939
+ def with_match_frame(body, parameters = nil)
940
+ rebuild(match_frame: Inference::MatchRebinding::Frame.new(body, parameters))
941
+ end
942
+
943
+ # True when this scope's frame makes a closure that may rebind its match globals whenever it is invoked
944
+ # ({Inference::MatchRebinding.matching_closure?}). False where no body stamped a frame.
945
+ def match_rebinding_closure?
946
+ !@match_frame.nil? && @match_frame.matching_closure?(self)
947
+ end
948
+
949
+ # True when this scope's frame makes a closure that may set its `$_` whenever it is invoked
950
+ # ({Inference::LastLine.closure?}). False where no body stamped a frame.
951
+ def last_line_closure?
952
+ !@match_frame.nil? && @match_frame.last_line_closure?(self)
953
+ end
954
+
512
955
  # Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
513
956
  # the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
514
957
  # that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
@@ -699,6 +1142,86 @@ module Rigor
699
1142
  site
700
1143
  end
701
1144
 
1145
+ # Issue #1097 — whether `class_name`'s own singleton `def method_name` has RUN by the time `call_node`
1146
+ # executes. Such a def precedes every `extend` in the singleton ancestry, so once it exists it owns the
1147
+ # call — but `sig { ... }` written BEFORE `def self.sig` in the same body still resolves through the
1148
+ # already-extended module, because `def` takes effect at execution. The site table stores
1149
+ # `"path:line"`: ordering applies only to calls executed eagerly in the same file's class body — a
1150
+ # call inside a def body runs at invocation time, and a def in another file can never be ordered
1151
+ # against the call site, so both conservatively count as shadowing. A nil `call_node`
1152
+ # (position-less dispatch probes) does the same.
1153
+ def singleton_def_shadows_call?(class_name, method_name, call_node)
1154
+ def_shadows_call?(user_singleton_def_site_for(class_name, method_name), class_name, method_name,
1155
+ :singleton, call_node)
1156
+ end
1157
+
1158
+ # The instance-side twin of {#singleton_def_shadows_call?}: whether `class_name`'s `def method_name`
1159
+ # has run by call time — for `extend M` edges, where M's instance surface is what answers.
1160
+ def instance_def_shadows_call?(class_name, method_name, call_node)
1161
+ def_shadows_call?(user_def_site_for(class_name, method_name), class_name, method_name, :instance,
1162
+ call_node)
1163
+ end
1164
+
1165
+ # Shared ordering half of the two `*_def_shadows_call?` predicates, over this file's
1166
+ # `discovered_deferred_ranges` (issue #1097). `exists` — a recorded `"path:line"` site or the
1167
+ # method-existence table — says a def of `method_name`/`kind` on `class_name` is known at all; a
1168
+ # discovered def with no site (an `attr_*` sibling, a bundle seed that predates the table) counts
1169
+ # through `discovered_method?`. The ranges then answer the timing questions, and can supply defs
1170
+ # the site table missed (a `module_function`-installed `sig` never enters the singleton-def
1171
+ # table). A foreign-file site cannot be ordered against the call — shadowing. A nil `call_node`
1172
+ # (position-less dispatch probes) or a file the index never saw falls back to `exists`.
1173
+ def def_shadows_call?(site, class_name, method_name, kind, call_node)
1174
+ exists = !site.nil? || discovered_method?(class_name, method_name, kind)
1175
+ return exists if call_node.nil?
1176
+
1177
+ path = source_path
1178
+ return exists unless path
1179
+
1180
+ if site
1181
+ site_path, = site.rpartition(":")
1182
+ return true unless site_path == path
1183
+ end
1184
+
1185
+ ranges = @discovery.discovered_deferred_ranges[path]
1186
+ return exists if ranges.nil?
1187
+
1188
+ deferred_ranges_shadow_call?(ranges, method_name.to_sym, kind, class_name.to_s,
1189
+ call_node.location, exists)
1190
+ end
1191
+ private :def_shadows_call?
1192
+
1193
+ # The range scan behind {#def_shadows_call?}. A call CONTAINED in any def / block / lambda /
1194
+ # `END` range is deferred — it runs at invocation time — so it is shadowed iff a matching def is
1195
+ # known (`exists`, or a row the site table missed). An EAGER call is shadowed iff the earliest
1196
+ # same-name, same-kind (`:both` matches either), same-OWNER row starts before it: owner scoping
1197
+ # keeps `class A`'s defs from ordering `class F`'s calls, and earliest-of keeps
1198
+ # `def self.sig; sig {}; def self.sig` honest where the def-node table is later-wins. With no
1199
+ # matching row the answer is `exists` — "a def exists but cannot be ordered" stays conservative,
1200
+ # "no def at all" stays permissive.
1201
+ def deferred_ranges_shadow_call?(ranges, method_name, kind, class_name, call_loc, exists)
1202
+ contained = false
1203
+ matching = false
1204
+ earliest = nil
1205
+ ranges.each do |(start, finish, name, def_kind, owner)|
1206
+ contained ||= start <= call_loc.start_offset && call_loc.end_offset <= finish
1207
+ next unless deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
1208
+
1209
+ matching = true
1210
+ earliest = start if earliest.nil? || start < earliest
1211
+ end
1212
+ return exists || matching if contained
1213
+
1214
+ earliest.nil? ? exists : earliest <= call_loc.start_offset
1215
+ end
1216
+ private :deferred_ranges_shadow_call?
1217
+
1218
+ # Whether a range row can order the call: same method name, same qualified owner, and a def kind
1219
+ # that answers this predicate's side (`:both` — `module_function` — matches either).
1220
+ def deferred_row_orders_call?(name, def_kind, owner, method_name, kind, class_name)
1221
+ name == method_name && owner == class_name && (def_kind == kind || def_kind == :both)
1222
+ end
1223
+ private :deferred_row_orders_call?
1224
+
702
1225
  # ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
703
1226
  # at the `class Foo < Bar` declaration (`"Bar"`, possibly a qualified `"A::B"`). Populated by `ScopeIndexer` —
704
1227
  # per-file plus the cross-file project pre-pass — and consumed by
@@ -735,15 +1258,74 @@ module Rigor
735
1258
  end
736
1259
 
737
1260
  # ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of
738
- # module names it `include`s / `prepend`s, AS WRITTEN at the mixin call. Populated by `ScopeIndexer` (per-file
1261
+ # module names it `include`s / `prepend`s, in instance-ancestor SEARCH order: prepended modules first
1262
+ # (Ruby inserts them ahead of the class itself), then included modules nearest-first — `include A;
1263
+ # include B` searches B before A, while `include A, B` keeps `["A", "B"]` (each statement's argument
1264
+ # list lands as one unit, ahead of the earlier statements'). Populated by `ScopeIndexer` (per-file
739
1265
  # plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an
740
- # implicit-self call resolves against an included module's `def`s, not just the superclass chain. As-written
741
- # names are resolved to qualified classes at walk time.
1266
+ # implicit-self call resolves against an included module's `def`s, not just the superclass chain.
1267
+ # As-written names are resolved to qualified classes at walk time. Issue #1173 — the list was call
1268
+ # order until then, a divergence from Ruby's later-include-wins the order-sensitive consumers (the
1269
+ # BFS mixin step, the external-ancestor walk, override visibility) silently absorbed.
742
1270
  def includes_of(class_name)
743
1271
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
744
1272
  @discovery.discovered_includes[class_name.to_s] || []
745
1273
  end
746
1274
 
1275
+ # Issue #1123 — the modules `class_name` `prepend`s, in instance-ancestor SEARCH order: the nearest
1276
+ # prepend first, so `prepend A; prepend B` answers `["B", "A"]` and `prepend A, B` keeps `["A", "B"]`
1277
+ # (each Ruby statement's argument list lands as one unit, ahead of the earlier statements'). Read by
1278
+ # {#user_def_through_ancestors}'s prepend wedge — Ruby inserts a prepended module, and its own ancestry,
1279
+ # IMMEDIATELY BEFORE the class that prepends it, so these names are searched ahead of the class's own
1280
+ # `def`s. Empty for a class the project never prepends, which is the behaviour every class had before
1281
+ # `prepend` was ordered at all.
1282
+ #
1283
+ # The same names ALSO appear in {#includes_of} — that table answers "which modules does this class
1284
+ # carry", which is what the arity, visibility, reflection and constant-scope consumers ask, and a
1285
+ # prepended module does contribute instance methods. This table adds the ORDER and the KIND; it never
1286
+ # removes a name from the other one.
1287
+ #
1288
+ # The class dependency is recorded only on a HIT, as {#data_member_layout} does: the wedge asks this
1289
+ # for every class the walk visits, so recording on a miss would give a caller file an ancestry edge on
1290
+ # a class that prepends nothing — an edge where ADR-46 slice 4 keeps method-body reads at
1291
+ # symbol granularity.
1292
+ def prepends_of(class_name)
1293
+ names = @discovery.discovered_prepends[class_name.to_s]
1294
+ return EMPTY_MIXIN_NAMES if names.nil?
1295
+
1296
+ record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
1297
+ names
1298
+ end
1299
+ private :prepends_of
1300
+
1301
+ # Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
1302
+ # as-written superclass chain because a singleton class inherits its superclass's singleton class
1303
+ # (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
1304
+ # true). Empty for a class the project never extends, and for every scope that saw no seeding pass.
1305
+ #
1306
+ # This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
1307
+ # argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
1308
+ # `class << self` body) written inside a declaration body spells, so a runtime `Widget.extend(m)` and an
1309
+ # `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
1310
+ # in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
1311
+ # Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
1312
+ # only to WITHHOLD a `Bot`, and never to assert that a guard matches.
1313
+ def singleton_extends_of(class_name)
1314
+ table = @discovery.discovered_extends
1315
+ return [] if table.empty?
1316
+
1317
+ names = []
1318
+ current = class_name.to_s
1319
+ seen = {}
1320
+ while current && !seen[current] && seen.size <= ANCESTOR_WALK_LIMIT
1321
+ seen[current] = true
1322
+ record_class_dependency(current) if Analysis::DependencyRecorder.active?
1323
+ names.concat(table[current] || [])
1324
+ current = @discovery.discovered_superclasses[current]
1325
+ end
1326
+ names.uniq
1327
+ end
1328
+
747
1329
  # ADR-24 slice 2 — the user-side ancestor walk: resolves `method_name` against `class_name`'s own `def`s,
748
1330
  # then breadth-first through its included / prepended modules (mixins first, as Ruby orders them) and its
749
1331
  # superclass chain. Returns `[def_node, owner]`, or `[nil, nil]` when nothing in the project defines it.
@@ -760,6 +1342,15 @@ module Rigor
760
1342
  # `name_memo` is a CACHE, not semantics: the per-edge as-written-name resolutions, so a caller resolving
761
1343
  # many methods against one class pays each ancestor edge once. Omit it and the walk is identical, just
762
1344
  # uncached.
1345
+ #
1346
+ # Issue #1123 — a prepended module is searched through {#prepends_of} AHEAD of the class's own `def`s
1347
+ # ({#prepend_wedge_names}), because Ruby inserts it, and its own ancestry, immediately before the class
1348
+ # that prepends it: `class Base; def speak = "base"; end; Base.prepend(Loud)` answers `Loud#speak`, and
1349
+ # the `super` in that body then reaches `Base#speak`. An included module still
1350
+ # sits AFTER the class in this walk, but — since #1173 — `includes_of` feeds the mixin step in
1351
+ # instance-ancestor search order, so `include A; include B` searches B first the way CRuby does. The
1352
+ # wedge is searched at EVERY node, not just the entry class: a prepend on an ancestor (`class Sub <
1353
+ # Base` where `Base` prepends a module) wins for the subclass too, exactly as Ruby dispatches it.
763
1354
  def user_def_through_ancestors(class_name, method_name, name_memo: {})
764
1355
  queue = [class_name.to_s]
765
1356
  seen = {}
@@ -772,6 +1363,9 @@ module Rigor
772
1363
  visited += 1
773
1364
  return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
774
1365
 
1366
+ wedge_hit = search_prepend_wedge(current, method_name, name_memo, seen)
1367
+ return wedge_hit if wedge_hit
1368
+
775
1369
  found = user_def_for(current, method_name)
776
1370
  return [found, current] if found
777
1371
 
@@ -780,6 +1374,63 @@ module Rigor
780
1374
  [nil, nil]
781
1375
  end
782
1376
 
1377
+ # Issue #1123 — the `[def_node, owner]` pair a class's prepend wedge answers `method_name` with, or nil
1378
+ # when nothing in the wedge defines it and the walk should ask the class's own `def`s next.
1379
+ #
1380
+ # Every name the wedge takes is marked on `seen`, the SAME set the walk's breadth-first step uses: a
1381
+ # prepended module also sits in `includes_of`, so the mixin step would otherwise re-ask a name the wedge
1382
+ # already answered — and, more importantly, marking it there keeps a `prepend` graph that loops back on
1383
+ # the walk (a prepended module reached again from somewhere else) from being expanded twice.
1384
+ def search_prepend_wedge(class_name, method_name, name_memo, seen)
1385
+ prepend_wedge_names(class_name, name_memo).each do |name|
1386
+ next if seen[name]
1387
+
1388
+ seen[name] = true
1389
+ found = user_def_for(name, method_name)
1390
+ return [found, name] if found
1391
+ end
1392
+ nil
1393
+ end
1394
+
1395
+ # Issue #1123 — the qualified names the instance MRO searches ahead of `class_name`'s own definitions,
1396
+ # in the order it searches them: for each of `class_name`'s prepends, nearest first, that module's own
1397
+ # prepend wedge, then the module itself, then its includes and superclass.
1398
+ #
1399
+ # A prepended module's own ancestry is part of the wedge because Ruby inserts the whole sub-chain with
1400
+ # it — `module Loud; include Loud::Prefix; end` prepended into `Base` puts `Loud::Prefix` ahead of
1401
+ # `Base`, so the wedge has to reach it. `seen` is shared across one wedge computation and guards a cyclic
1402
+ # `prepend`/`include` graph (and the overlap with `includes_of`, which carries the prepend names too);
1403
+ # the walk's own set is applied by {#search_prepend_wedge}, and each name is expanded at most once.
1404
+ def prepend_wedge_names(class_name, name_memo, seen = {})
1405
+ out = []
1406
+ prepends_of(class_name).each do |raw|
1407
+ resolved = resolve_ancestor_class_name(class_name, raw, name_memo)
1408
+ next if resolved.nil? || seen[resolved]
1409
+
1410
+ seen[resolved] = true
1411
+ out.concat(subchain_names(resolved, name_memo, seen))
1412
+ end
1413
+ out
1414
+ end
1415
+
1416
+ # Issue #1123 — appends `node`'s own instance search order to `out`: its prepend wedge, the node
1417
+ # itself, then its includes and superclass, each expanded the same way. Since #1173 the include
1418
+ # half is `includes_of`'s search order — the ORDER inside a prepended module's own include list is
1419
+ # not re-derived here, only read.
1420
+ def subchain_names(node, name_memo, seen)
1421
+ out = prepend_wedge_names(node, name_memo, seen)
1422
+ out << node
1423
+ (includes_of(node) + [superclass_of(node)].compact).each do |raw|
1424
+ resolved = resolve_ancestor_class_name(node, raw, name_memo)
1425
+ next if resolved.nil? || seen[resolved]
1426
+
1427
+ seen[resolved] = true
1428
+ out.concat(subchain_names(resolved, name_memo, seen))
1429
+ end
1430
+ out
1431
+ end
1432
+ private :search_prepend_wedge, :prepend_wedge_names, :subchain_names
1433
+
783
1434
  # Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: resolves `method_name` against
784
1435
  # `class_name`'s own `def self.` / `class << self` bodies, then up the SUPERCLASS chain. Returns
785
1436
  # `[def_node, owner]`, or `[nil, nil]`.
@@ -810,9 +1461,78 @@ module Rigor
810
1461
  [nil, nil]
811
1462
  end
812
1463
 
1464
+ # Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
1465
+ # `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the two walks above
1466
+ # deliberately drop, gathered depth-first over the project ancestry (an included module's own
1467
+ # sub-chain precedes the includer's superclass edge) so an inherited edge counts too — and, since
1468
+ # #1173, so the groups arrive in instance-ancestor search order for the caller that adopts the
1469
+ # FIRST answering one.
1470
+ #
1471
+ # Each entry is the CANDIDATE LIST for one as-written name ({#ancestor_name_candidates}: the nesting
1472
+ # spellings first, the bare name last), not a resolved class — resolving it means asking the RBS
1473
+ # environment, which this frozen-index walk does not read. The caller takes the first candidate its own
1474
+ # oracle knows and ignores the rest, the same "most-qualified first" order the project-side resolution
1475
+ # uses.
1476
+ # Issue #527 slice 1 — `mixins: false` follows the SUPERCLASS chain only. Ruby reaches an included
1477
+ # module's methods too, so the default answers the whole ancestry; a consumer resolving one KIND of
1478
+ # inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows
1479
+ # it, the same way {#singleton_def_through_ancestors} narrows {#enqueue_ancestors}.
1480
+ def external_ancestor_name_candidates(class_name, name_memo: {}, mixins: true)
1481
+ groups = []
1482
+ collect_external_ancestors(class_name.to_s, groups, name_memo, mixins, {}, [0])
1483
+ groups
1484
+ end
1485
+
1486
+ # One node of {#external_ancestor_name_candidates}: the project-declared ancestors continue the walk,
1487
+ # everything else is reported as a candidate list.
1488
+ #
1489
+ # Issue #1173 — the walk is depth-first pre-order, not breadth-first: a resolved ancestor's own
1490
+ # edges are followed BEFORE the next sibling edge, because Ruby inserts an included module's whole
1491
+ # sub-chain ahead of the includer's superclass (`class C < Hash; include M` where `M` itself
1492
+ # includes `Widen` chains `C → M → Widen → Hash`; BFS would report `Hash` ahead of `Widen`). Each
1493
+ # node's edges still go includes-then-superclass, and `includes_of` is already search order, so the
1494
+ # emitted groups arrive in MRO order — which the #1173 include arm depends on when two ancestors
1495
+ # both declare the name.
1496
+ def collect_external_ancestors(current, groups, name_memo, mixins, seen, visited)
1497
+ return if current.nil? || seen[current]
1498
+
1499
+ seen[current] = true
1500
+ visited[0] += 1
1501
+ if visited[0] > ANCESTOR_WALK_LIMIT
1502
+ # Issue #527 — the give-up is a budget event, not an answer. Consumers read the groups as
1503
+ # "the ancestors this class reaches", so a truncated list must be visible in `--stats`
1504
+ # alongside the other walks' exhaustions rather than silently short. Hit once: every node
1505
+ # visited past the limit returns here too, and only the first should count.
1506
+ Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if visited[0] == ANCESTOR_WALK_LIMIT + 1
1507
+ return
1508
+ end
1509
+
1510
+ raw_names = mixins ? includes_of(current).dup : []
1511
+ raw_super = superclass_of(current)
1512
+ raw_names << raw_super if raw_super
1513
+ raw_names.each do |raw|
1514
+ resolved = resolve_ancestor_class_name(current, raw, name_memo)
1515
+ if resolved
1516
+ collect_external_ancestors(resolved, groups, name_memo, mixins, seen, visited)
1517
+ else
1518
+ groups << ancestor_name_candidates(current, raw)
1519
+ end
1520
+ end
1521
+ end
1522
+ private :collect_external_ancestors
1523
+
813
1524
  # The BFS node cap; a hierarchy past it gives up rather than walking unboundedly (ADR-41 WD4).
814
1525
  ANCESTOR_WALK_LIMIT = 100
815
1526
 
1527
+ EMPTY_HEADER_NESTING = [].freeze
1528
+ private_constant :EMPTY_HEADER_NESTING
1529
+
1530
+ # The answer for a class that prepends nothing — the common case, so the wedge does not allocate a
1531
+ # fresh empty list per visited node. Shared and frozen, and never handed to a caller that could
1532
+ # mutate it ({#prepends_of} is private to this walk).
1533
+ EMPTY_MIXIN_NAMES = [].freeze
1534
+ private_constant :EMPTY_MIXIN_NAMES
1535
+
816
1536
  def ancestor_walk_gave_up
817
1537
  Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
818
1538
  [nil, nil]
@@ -929,10 +1649,86 @@ module Rigor
929
1649
  return [raw.delete_prefix("::")] if raw.start_with?("::")
930
1650
 
931
1651
  recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
932
- entries = recorded || peeled_header_nesting(subclass_qualified)
1652
+ entries = recorded ? recorded_header_nesting(recorded, raw) : peeled_header_nesting(subclass_qualified)
1653
+ if !entries.empty? && DiscoveryIndex.ambiguous_header_nesting?(entries)
1654
+ return ambiguous_ancestor_candidates(entries, raw_ancestor)
1655
+ end
1656
+
1657
+ entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
1658
+ end
1659
+
1660
+ # Issue #986 — the candidate list for a raw name two declaration sites of ONE class wrote in different
1661
+ # crefs (the compact-header rename pass landed both on this key). Each alternative chain gets its own
1662
+ # candidate list, in the order this walk would have used for that site alone.
1663
+ #
1664
+ # When two of them resolve to two DIFFERENT project classes there is no candidate list to return: at
1665
+ # runtime both `include`s run, and which of the two same-named modules ends up nearer in the MRO is the
1666
+ # load order of the two files, which this walk cannot know. Answering with either is a WRONG ANCESTOR,
1667
+ # and a wrong ancestor is a false-positive source rather than a missed one — two same-named modules can
1668
+ # declare the same method at different arities, and `call.wrong-arity` then fires on a correct program.
1669
+ # The empty list declines instead: the name resolves to no project class, the receiver's methods stay
1670
+ # `Dynamic`, and every rule reading this walk goes quiet.
1671
+ #
1672
+ # Where the alternatives AGREE, or only one of them resolves at all, there is nothing to adjudicate: the
1673
+ # answer is the union of their chains, most-qualified first — the same list a class whose sites needed
1674
+ # no rename gets — so an unambiguous collision is unchanged, and an EXTERNAL ancestor keeps the full
1675
+ # candidate list `#external_ancestor_name_candidates` hands the gem / RBS probe.
1676
+ def ambiguous_ancestor_candidates(alternatives, raw_ancestor)
1677
+ return [] if ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor).size > 1
1678
+
1679
+ entries = alternatives.reduce([], :|).sort_by { |entry| [-entry.split("::").size, entry] }
933
1680
  entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
934
1681
  end
935
1682
 
1683
+ def ambiguous_ancestor_resolutions_of(alternatives, raw_ancestor)
1684
+ alternatives.filter_map do |chain|
1685
+ (chain.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s)
1686
+ .find { |candidate| known_user_class?(candidate) }
1687
+ end.uniq
1688
+ end
1689
+ private :ambiguous_ancestor_candidates, :ambiguous_ancestor_resolutions_of
1690
+
1691
+ # Issue #986 — the several project classes an ancestor name resolves to when the compact-header rename
1692
+ # collision left it ambiguous, and `EMPTY_HEADER_NESTING` for every other name. {#ancestor_name_candidates}
1693
+ # declines such a name because no ONE class is its answer; a caller that also knows which METHOD it is
1694
+ # looking up can do better than that decline, and a rule that reports on a method must, because the
1695
+ # decline otherwise silences it for the whole receiver — its own `def`s and its unambiguous ancestors
1696
+ # included.
1697
+ #
1698
+ # Both of these classes are ancestors at runtime: both `include`s run, and only their MRO ORDER is the
1699
+ # load order this walk cannot see. So a method only one of them declares is answered by that one
1700
+ # whatever the order, and only a method they BOTH declare is unanswerable.
1701
+ # `Analysis::CheckRules::SourceArity` reads this for exactly that: it takes both as mixin levels and
1702
+ # declines on the disagreement its own envelope join already knows how to spot.
1703
+ def ambiguous_ancestor_resolutions(subclass_qualified, raw_ancestor)
1704
+ raw = raw_ancestor.to_s
1705
+ return EMPTY_HEADER_NESTING if raw.start_with?("::")
1706
+
1707
+ recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
1708
+ return EMPTY_HEADER_NESTING if recorded.nil?
1709
+
1710
+ entries = recorded_header_nesting(recorded, raw)
1711
+ return EMPTY_HEADER_NESTING if entries.empty? || !DiscoveryIndex.ambiguous_header_nesting?(entries)
1712
+
1713
+ resolved = ambiguous_ancestor_resolutions_of(entries, raw_ancestor)
1714
+ resolved.size > 1 ? resolved : EMPTY_HEADER_NESTING
1715
+ end
1716
+
1717
+ # Issue #728 — the chain of the declaration site that WROTE `raw`, which is the cref Ruby resolves that
1718
+ # one name in. A class's sites need not agree: `class Foo < Base` at the top level and a rooted
1719
+ # `class ::Foo; include Helper; end` inside `module Outer` are two sites of `Foo`, and the union of
1720
+ # their chains put `Outer::Base` — a class Ruby never looks at — ahead of `::Base` for the superclass
1721
+ # the top-level site wrote. That is a WRONG CLASS, not a wider candidate list.
1722
+ #
1723
+ # The unkeyed entry is the union, and answers a name no site recorded under its own key: an `include`
1724
+ # attributed to this class from OUTSIDE its declaration (`Recv.class_eval { include M }`, which
1725
+ # `Inference::ScopeIndexer#walk_class_includes` names but the header walk cannot see), or a dynamic
1726
+ # mixin argument. It is the pre-#728 answer, so those are unchanged rather than degraded.
1727
+ def recorded_header_nesting(bucket, raw)
1728
+ bucket[raw] || bucket[DiscoveryIndex::UNKEYED_HEADER_NESTING] || EMPTY_HEADER_NESTING
1729
+ end
1730
+ private :recorded_header_nesting
1731
+
936
1732
  # The pre-#682 reading of a qualified class name: every proper prefix of it, innermost first, as if the
937
1733
  # class had been declared one `module` keyword per segment.
938
1734
  def peeled_header_nesting(subclass_qualified)
@@ -974,6 +1770,21 @@ module Rigor
974
1770
  end
975
1771
  private :record_class_dependency
976
1772
 
1773
+ # Issue #639 — the EXISTENCE edge for a bare class reference, `class:<last segment>`, the hit-side twin of
1774
+ # the miss-side key `Inference::ExpressionTyper` already records. ADR-46 slice 1c left the existence hit
1775
+ # edgeless on the reasoning that a file merely referencing `Post` depends on `Post`'s methods, not on its
1776
+ # bare existence; that stops holding the moment the DECLARING FILE IS DELETED, because existence is then
1777
+ # exactly what changed and a reference-only consumer had recorded nothing to be re-checked by.
1778
+ #
1779
+ # NAME-keyed rather than a positive edge to the declaring file, which is the distinction the ADR's cost
1780
+ # argument turns on: the declared-class set of a file does not move when a method body in it is edited,
1781
+ # so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
1782
+ # would have re-checked every bare referent of every class the file declares on any edit to it.
1783
+ def record_class_existence(class_name)
1784
+ segment = class_name.to_s.delete_prefix("::").split("::").last
1785
+ Analysis::DependencyRecorder.read_name(:class, segment) if segment
1786
+ end
1787
+
977
1788
  # Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
978
1789
  # calls this the moment a candidate resolves through `in_source_constants`, so the reader depends on the
979
1790
  # file that wrote the constant: editing the literal, or deleting the file, re-checks the reader. Recorded
@@ -990,6 +1801,17 @@ module Rigor
990
1801
  sites.each { |site| Analysis::DependencyRecorder.read_site(site) }
991
1802
  end
992
1803
 
1804
+ # Issue #992 — one class's `discovered_parameter_envelopes` bucket (`{[kind, method_name] => envelope}` plus
1805
+ # the class-wide marks), or an empty Hash. Records the ADR-46 class edge first: a verdict read off the
1806
+ # bucket depends on every file that declares the class, and a `memoize :f` added to one of them moves it.
1807
+ def parameter_envelopes_of(class_name)
1808
+ record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
1809
+ @discovery.discovered_parameter_envelopes[class_name.to_s] || EMPTY_PARAMETER_ENVELOPES
1810
+ end
1811
+
1812
+ EMPTY_PARAMETER_ENVELOPES = {}.freeze
1813
+ private_constant :EMPTY_PARAMETER_ENVELOPES
1814
+
993
1815
  # v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
994
1816
  # `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding
995
1817
  # `private` / `protected` / `public` modifier state plus any post-hoc `private :name, ...` named-argument
@@ -1110,10 +1932,37 @@ module Rigor
1110
1932
  joined_locals = join_bindings(locals, other.locals)
1111
1933
  joined_ivars = join_bindings(ivars, other.ivars)
1112
1934
  joined_cvars = join_bindings(cvars, other.cvars)
1113
- joined_globals = join_bindings(globals, other.globals)
1935
+ joined_globals = unbind_split_last_line(join_bindings(globals, other.globals), other)
1114
1936
  build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
1115
1937
  end
1116
1938
 
1939
+ # Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
1940
+ # bind, a constant both arms narrow (one an arm does not narrow reads its resolved type after the join, so its
1941
+ # narrowing is gone). An arm without a record contributes nothing to the restore target, whose union with the
1942
+ # joined binding {#forget_guard_narrowings} takes, so it still covers that arm's binding.
1943
+ def join_guard_records(other, joined_globals, joined_constants)
1944
+ mine = @guard_records
1945
+ theirs = other.guard_records
1946
+ return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
1947
+
1948
+ merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
1949
+ kept = merged.select do |(kind, name), _|
1950
+ kind == :global ? joined_globals.key?(name) : joined_constants.key?(name)
1951
+ end
1952
+ kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
1953
+ end
1954
+ private :join_guard_records
1955
+
1956
+ # Issue #1359 — arms that bind `$_` apart join with it unbound rather than to their union. The arms of a reader
1957
+ # condition bind `String` and `nil`, and `String?` after `if gets … end` would report correct code that proves the
1958
+ # line some other way (`ok = gets ? true : false; return unless ok; line = $_; line.chomp`).
1959
+ def unbind_split_last_line(joined, other)
1960
+ return joined unless joined.key?(LAST_LINE) && @globals[LAST_LINE] != other.globals[LAST_LINE]
1961
+
1962
+ joined.except(LAST_LINE).freeze
1963
+ end
1964
+ private :unbind_split_last_line
1965
+
1117
1966
  def ==(other)
1118
1967
  other.is_a?(Scope) &&
1119
1968
  environment.equal?(other.environment) &&
@@ -1125,7 +1974,7 @@ module Rigor
1125
1974
  @globals == other.globals &&
1126
1975
  @indexed_narrowings == other.indexed_narrowings &&
1127
1976
  @method_chain_narrowings == other.method_chain_narrowings &&
1128
- @declaration_sourced == other.declaration_sourced
1977
+ same_marks?(other)
1129
1978
  end
1130
1979
  alias eql? ==
1131
1980
 
@@ -1135,6 +1984,18 @@ module Rigor
1135
1984
 
1136
1985
  private
1137
1986
 
1987
+ # The marks {#==} compares: ADR-58's, issue #667's and the repeated `||=` sites, and #1429's guard state.
1988
+ def same_marks?(other)
1989
+ @declaration_sourced == other.declaration_sourced &&
1990
+ @published_constant_sourced == other.published_constant_sourced &&
1991
+ @repeated_or_writes == other.repeated_or_writes &&
1992
+ same_guard_state?(other)
1993
+ end
1994
+
1995
+ def same_guard_state?(other)
1996
+ @constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records
1997
+ end
1998
+
1138
1999
  def rebuild(
1139
2000
  locals: @locals, fact_store: @fact_store, self_type: @self_type,
1140
2001
  ivars: @ivars, cvars: @cvars, globals: @globals,
@@ -1142,9 +2003,11 @@ module Rigor
1142
2003
  indexed_narrowings: @indexed_narrowings,
1143
2004
  method_chain_narrowings: @method_chain_narrowings,
1144
2005
  declaration_sourced: @declaration_sourced,
2006
+ published_constant_sourced: @published_constant_sourced,
1145
2007
  source_path: @source_path,
1146
2008
  struct_fold_safe_locals: @struct_fold_safe_locals,
1147
2009
  opaque_block_self: @opaque_block_self,
2010
+ singleton_class_body: @singleton_class_body,
1148
2011
  lexical_nesting: @lexical_nesting,
1149
2012
  dynamic_origins: @dynamic_origins,
1150
2013
  local_origins: @local_origins,
@@ -1153,7 +2016,11 @@ module Rigor
1153
2016
  plugin_typed_calls: @plugin_typed_calls,
1154
2017
  optimistic_origins: @optimistic_origins,
1155
2018
  optimistic_locals: @optimistic_locals,
1156
- optimistic_ivars: @optimistic_ivars
2019
+ optimistic_ivars: @optimistic_ivars,
2020
+ repeated_or_writes: @repeated_or_writes,
2021
+ match_frame: @match_frame,
2022
+ constant_narrowings: @constant_narrowings,
2023
+ guard_records: @guard_records
1157
2024
  )
1158
2025
  self.class.new(
1159
2026
  environment: environment, locals: locals,
@@ -1163,9 +2030,11 @@ module Rigor
1163
2030
  indexed_narrowings: indexed_narrowings,
1164
2031
  method_chain_narrowings: method_chain_narrowings,
1165
2032
  declaration_sourced: declaration_sourced,
2033
+ published_constant_sourced: published_constant_sourced,
1166
2034
  source_path: source_path,
1167
2035
  struct_fold_safe_locals: struct_fold_safe_locals,
1168
2036
  opaque_block_self: opaque_block_self,
2037
+ singleton_class_body: singleton_class_body,
1169
2038
  lexical_nesting: lexical_nesting,
1170
2039
  dynamic_origins: dynamic_origins,
1171
2040
  local_origins: local_origins,
@@ -1174,7 +2043,11 @@ module Rigor
1174
2043
  plugin_typed_calls: plugin_typed_calls,
1175
2044
  optimistic_origins: optimistic_origins,
1176
2045
  optimistic_locals: optimistic_locals,
1177
- optimistic_ivars: optimistic_ivars
2046
+ optimistic_ivars: optimistic_ivars,
2047
+ repeated_or_writes: repeated_or_writes,
2048
+ match_frame: match_frame,
2049
+ constant_narrowings: constant_narrowings,
2050
+ guard_records: guard_records
1178
2051
  )
1179
2052
  end
1180
2053
 
@@ -1194,7 +2067,7 @@ module Rigor
1194
2067
 
1195
2068
  def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
1196
2069
  self.class.new(
1197
- environment: environment,
2070
+ environment: @environment,
1198
2071
  locals: joined_locals.freeze,
1199
2072
  fact_store: fact_store.join(other.fact_store),
1200
2073
  self_type: self_type == other.self_type ? self_type : nil,
@@ -1208,7 +2081,13 @@ module Rigor
1208
2081
  # path made the binding flow-live (a method-local nil write / failed-guard narrowing), the merge is
1209
2082
  # flow-live and `possible-nil-receiver` fires as before.
1210
2083
  declaration_sourced: join_declaration_sourced(other),
1211
- source_path: source_path,
2084
+ # Issue #667 — UNION, the opposite of the line above, which is the first reason this mark does not
2085
+ # ride the ADR-58 Set. The mark only ever WITHHOLDS a firing, so keeping it when either arm bound the
2086
+ # name from a published constant is the false-positive-safe merge: `if c then m = MODE else m = MODE2
2087
+ # end; m == :x` must not warn because one arm's copy is invisible to the reader's author. ADR-67
2088
+ # WD6b's `:inferred_param` taint takes the same direction for the same reason.
2089
+ published_constant_sourced: join_published_constant_sourced(other),
2090
+ source_path: @source_path,
1212
2091
  # Issue #589 — the fold-safe set MUST survive a merge. It was simply absent from this constructor
1213
2092
  # call, so it fell back to the empty default and every `if` / `while` in a method silently revoked
1214
2093
  # struct member folding for the whole body after it: `s = S.new("r"); while c; i += 1; end; s.raw`
@@ -1234,6 +2113,10 @@ module Rigor
1234
2113
  # block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
1235
2114
  # the same value and the `||` is that value.
1236
2115
  opaque_block_self: @opaque_block_self || other.opaque_block_self,
2116
+ # Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
2117
+ # value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
2118
+ # declines the `define_method` narrowing, which is the pre-#963 answer.
2119
+ singleton_class_body: @singleton_class_body || other.singleton_class_body,
1237
2120
  # Issue #652 — the recorded `Module.nesting`, stamped once at body entry and threaded by `rebuild`
1238
2121
  # exactly as the fold-safe set is. A join is a control-flow merge INSIDE one body, so both arms
1239
2122
  # always carry the identical chain; taking this scope's is that chain. Dropping it would silently
@@ -1245,11 +2128,38 @@ module Rigor
1245
2128
  void_origins: @void_origins,
1246
2129
  plugin_typed_calls: @plugin_typed_calls,
1247
2130
  optimistic_origins: @optimistic_origins,
1248
- optimistic_locals: join_origins(@optimistic_locals, other.optimistic_locals),
1249
- optimistic_ivars: join_origins(@optimistic_ivars, other.optimistic_ivars)
2131
+ optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
2132
+ optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
2133
+ # UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
2134
+ # reading, so keeping a site either arm holds is the wider answer.
2135
+ repeated_or_writes: join_repeated_or_writes(other),
2136
+ # Issue #1358 — the frame the body runs in, stamped at its entry like the nesting above, so both arms
2137
+ # of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
2138
+ # it only loses the frame's resets.
2139
+ match_frame: @match_frame || other.match_frame,
2140
+ # Issue #1429 — the guard narrowings of globals and constants and their pre-guard records.
2141
+ **join_guard_narrowings(other, joined_globals)
1250
2142
  )
1251
2143
  end
1252
2144
 
2145
+ def join_guard_narrowings(other, joined_globals)
2146
+ joined_constants = join_constant_narrowings(other)
2147
+ { constant_narrowings: joined_constants,
2148
+ guard_records: join_guard_records(other, joined_globals, joined_constants) }
2149
+ end
2150
+
2151
+ # Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
2152
+ # type again, which the other arm reads too.
2153
+ def join_constant_narrowings(other)
2154
+ mine = @constant_narrowings
2155
+ theirs = other.constant_narrowings
2156
+ return mine if mine.equal?(theirs)
2157
+ return EMPTY_CONSTANT_NARROWINGS if mine.empty? || theirs.empty?
2158
+
2159
+ joined = join_bindings(mine, theirs)
2160
+ joined.empty? ? EMPTY_CONSTANT_NARROWINGS : joined
2161
+ end
2162
+
1253
2163
  # Issue #589 — intersect the struct fold-safe grants. Zero-alloc on the common path, where both arms
1254
2164
  # carry the same frozen set the body scan stamped.
1255
2165
  # A `nil` on EITHER side yields no grants. The field is keyword-defaulted to `EMPTY_FOLD_SAFE` and
@@ -1277,6 +2187,17 @@ module Rigor
1277
2187
  theirs.merge(mine).freeze
1278
2188
  end
1279
2189
 
2190
+ # Issue #286's mark tables join as {#join_origins} does, except for the miss answer a mark may carry (issue
2191
+ # #1302): a name both arms mark keeps its answer only when the arms agree on it. A name one arm marks keeps
2192
+ # that arm's answer, since a miss runs through that arm only.
2193
+ def join_optimistic_marks(mine, theirs)
2194
+ return mine if mine.equal?(theirs) || theirs.empty?
2195
+ return theirs if mine.empty?
2196
+
2197
+ theirs.merge(mine) { |_name, their_mark, my_mark| Inference::OptimisticOrigin.join_bound_marks(my_mark, their_mark) }
2198
+ .freeze
2199
+ end
2200
+
1280
2201
  # ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
1281
2202
  # afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
1282
2203
  # carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
@@ -1293,6 +2214,11 @@ module Rigor
1293
2214
  # value on the branch where it derives from an inferred parameter, so if EITHER branch taints it, a
1294
2215
  # downstream use could observe the lower-bound value and firing on it is an FP (`x = param else x = 5;
1295
2216
  # x.foo`). Intersecting would lose the taint at the merge and re-surface the false positive.
2217
+ #
2218
+ # Issue #1362 — a local both branches mark as copies of global seeds keeps its mark with the union of the globals
2219
+ # the two record, so a consumer compares against the writes to every one of them. A `(:local, name)` mark only
2220
+ # one branch backs with a record is dropped with it: that branch copies a global and the other an ivar, so no
2221
+ # comparison answers for the merge and it is flow-live.
1296
2222
  def join_declaration_sourced(other)
1297
2223
  mine = @declaration_sourced
1298
2224
  theirs = other.declaration_sourced
@@ -1305,12 +2231,33 @@ module Rigor
1305
2231
  if mine.empty? || theirs.empty?
1306
2232
  EMPTY_DECLARATION_SOURCED
1307
2233
  else
1308
- mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }
2234
+ join_copy_records(mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }, mine, theirs)
1309
2235
  end
1310
2236
  merged = inferred.merge(intersected)
1311
2237
  merged.empty? ? EMPTY_DECLARATION_SOURCED : merged.freeze
1312
2238
  end
1313
2239
 
2240
+ # `kept`, the refs both branches carry, with the `[:global_copy, …]` records rejoined ({#join_declaration_sourced}).
2241
+ def join_copy_records(kept, mine, theirs)
2242
+ mine_copies = global_copy_records(mine)
2243
+ theirs_copies = global_copy_records(theirs)
2244
+ return kept if mine_copies.empty? && theirs_copies.empty?
2245
+
2246
+ joined = kept.reject { |ref| ref[0] == :global_copy }
2247
+ (mine_copies.keys | theirs_copies.keys).each do |name|
2248
+ if mine_copies.key?(name) && theirs_copies.key?(name) && joined.include?([:local, name])
2249
+ joined.concat(mine_copies[name] | theirs_copies[name])
2250
+ else
2251
+ joined.delete([:local, name])
2252
+ end
2253
+ end
2254
+ joined
2255
+ end
2256
+
2257
+ def global_copy_records(refs)
2258
+ refs.each_with_object({}) { |ref, acc| (acc[ref[1]] ||= []) << ref if ref[0] == :global_copy }
2259
+ end
2260
+
1314
2261
  def indexed_key(receiver_kind, receiver_name, key)
1315
2262
  IndexedKey.new(
1316
2263
  receiver_kind: receiver_kind.to_sym,
@@ -1339,6 +2286,43 @@ module Rigor
1339
2286
  end
1340
2287
 
1341
2288
  # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
2289
+ # Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
2290
+ # {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
2291
+ # policies, so sharing the storage would make every future edit to either one a decision about both.
2292
+ def add_published_constant_sourced(kind, name)
2293
+ ref = [kind.to_sym, name.to_sym].freeze
2294
+ return @published_constant_sourced if @published_constant_sourced.include?(ref)
2295
+
2296
+ (@published_constant_sourced.dup << ref).freeze
2297
+ end
2298
+
2299
+ def drop_published_constant_sourced_for(kind, name)
2300
+ return @published_constant_sourced if @published_constant_sourced.empty?
2301
+
2302
+ ref = [kind.to_sym, name.to_sym]
2303
+ return @published_constant_sourced unless @published_constant_sourced.include?(ref)
2304
+
2305
+ (@published_constant_sourced - [ref]).freeze
2306
+ end
2307
+
2308
+ def join_published_constant_sourced(other)
2309
+ mine = @published_constant_sourced
2310
+ theirs = other.published_constant_sourced
2311
+ return mine if mine.equal?(theirs) || theirs.empty?
2312
+ return theirs if mine.empty?
2313
+
2314
+ (mine | theirs).freeze
2315
+ end
2316
+
2317
+ def join_repeated_or_writes(other)
2318
+ mine = @repeated_or_writes
2319
+ theirs = other.repeated_or_writes
2320
+ return mine if mine.equal?(theirs) || theirs.empty?
2321
+ return theirs if mine.empty?
2322
+
2323
+ mine.merge(theirs).freeze
2324
+ end
2325
+
1342
2326
  def add_declaration_sourced(kind, name)
1343
2327
  ref = [kind.to_sym, name.to_sym]
1344
2328
  return @declaration_sourced if @declaration_sourced.include?(ref)
@@ -1357,6 +2341,31 @@ module Rigor
1357
2341
  dropped.freeze
1358
2342
  end
1359
2343
 
2344
+ # The `(:local, name)` mark and, when it was there, the `[:global_copy, name, global]` records that only ever sit
2345
+ # beside it ({#with_global_copy_marks}).
2346
+ def drop_local_declaration_marks(name)
2347
+ dropped = drop_declaration_sourced_for(:local, name)
2348
+ return dropped if dropped.equal?(@declaration_sourced)
2349
+
2350
+ name = name.to_sym
2351
+ return dropped unless dropped.any? { |ref| ref[0] == :global_copy && ref[1] == name }
2352
+
2353
+ dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
2354
+ end
2355
+
2356
+ def add_guard_record(key, pre_guard)
2357
+ return @guard_records if @guard_records.key?(key)
2358
+
2359
+ @guard_records.merge(key => pre_guard).freeze
2360
+ end
2361
+
2362
+ def drop_guard_record(kind, name)
2363
+ return @guard_records if @guard_records.empty?
2364
+
2365
+ key = [kind, name]
2366
+ @guard_records.key?(key) ? @guard_records.except(key).freeze : @guard_records
2367
+ end
2368
+
1360
2369
  def drop_chain_narrowings_for(receiver_kind, receiver_name)
1361
2370
  return @method_chain_narrowings if @method_chain_narrowings.empty?
1362
2371