rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -24,10 +24,10 @@ module Rigor
24
24
  # currently declaration-sourced ivar*, so a second hop (`c = @count; d = c`) and an `||=` rewrite land on
25
25
  # the plain `with_local` path and carry no mark.
26
26
  #
27
- # A branch join is NOT one of those cases, contrary to what this comment claimed when it landed: both
28
- # arms of `if c then r = @count else r = @count end` stamp `(:local, :r)`, and `join_declaration_sourced`
29
- # INTERSECTS, so the mark survives. Only an *asymmetric* join drops it, and by that intersection rather
30
- # than by reaching `with_local`. The normative statement — establishment, drop, join and the full list of
27
+ # A branch join keeps the mark when both arms stamp it: `if c then r = @count else r = @count end` stamps
28
+ # `(:local, :r)` on each arm, and `join_declaration_sourced` INTERSECTS, so the mark survives. Only an
29
+ # *asymmetric* join drops it, by that intersection rather than by reaching `with_local`. The normative
30
+ # statement — establishment, drop, join and the full list of
31
31
  # unmarked shapes, each verified against the implementation — is
32
32
  # `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
33
33
  # comment subordinate to it.
@@ -36,8 +36,8 @@ module Rigor
36
36
  # a Hash literal, loops and blocks included.
37
37
  NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
38
38
 
39
- # Returns `Array<{hash_node:, key_node:, first_key_node:, key_label:}>` — one entry per LATER
40
- # occurrence of a repeated literal key (`key_node` is the repeat the diagnostic points at;
39
+ # One entry per LATER occurrence of a repeated literal key, carrying `hash_node`, `key_node`,
40
+ # `first_key_node` and `key_label` (`key_node` is the repeat the diagnostic points at;
41
41
  # `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
42
42
  # qualifying duplicates.
43
43
  def initialize(scope_index)
@@ -21,6 +21,21 @@ module Rigor
21
21
  # - the direct receiver / subject: `param.foo`, `case param`
22
22
  # - an indexed element: `param[i] - x` (the `-` receiver is the `[]` call whose root is `param`)
23
23
  # - any method chain: `param.foo.bar`, `param.foo(arg).baz`, `param.key?(:x)`
24
+ # - an array or hash literal holding the parameter (issue #2189): `[param].first.upcase`,
25
+ # `{a: param}[:a].upcase`. The engine reads such an element back precisely, so the read is the
26
+ # parameter's lower bound again; any other call on the literal declines too, the FP-safe direction.
27
+ # - an instance variable the same method wrote from such a value (issue #2189): `@x = param; @x.upcase`.
28
+ # This one IS a scope mark — {Inference::StatementEvaluator#eval_ivar_write} stamps `:inferred_param`
29
+ # under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
30
+ # separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
31
+ # ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
32
+ # - `Array(param)` (issue #2280): the conversion passes the parameter's elements through, so
33
+ # `Array(param).first.upcase` reads the lower bound back.
34
+ # - an `if` / `unless` / ternary whose value is the parameter (issue #2280): `(c ? param : param).upcase`.
35
+ # Unlike `&&` / `||`, a branch that does not hold the parameter keeps its diagnostic: the form is rooted
36
+ # when every branch that can run is. A predicate the engine folds to one arm (the same certainty
37
+ # `StatementEvaluator#eval_if` elides the dead arm on) leaves only that arm to decide, so
38
+ # `(c ? param : 1).upcase` declines when `c` folds truthy and reports `Integer` when it folds falsey.
24
39
  #
25
40
  # Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
26
41
  # is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
@@ -32,27 +47,105 @@ module Rigor
32
47
  # length, which is tiny in practice).
33
48
  MAX_DEPTH = 64
34
49
 
35
- def rooted?(node, scope, depth = 0)
50
+ # `fold` is whether `scope` is still the scope the walked node is evaluated in. The callers pass the scope
51
+ # at the start of the expression; once the walk passes something evaluated earlier inside it (a leading
52
+ # statement, the left operand of `&&`, an earlier literal element, a conditional's predicate), that
53
+ # earlier code may have rebound what a nested predicate reads (`(c = false; c ? param : 1)`), so a
54
+ # conditional below no longer folds its predicate in `scope`.
55
+ def rooted?(node, scope, depth = 0, fold: true)
36
56
  return false if node.nil? || depth > MAX_DEPTH
37
57
 
38
58
  case node
39
- when Prism::LocalVariableReadNode then scope.inferred_param?(node.name)
40
- when Prism::CallNode then rooted?(node.receiver, scope, depth + 1)
41
- # Value-combining forms: the produced value is a union of the operands, so it is a lower bound when
42
- # ANY operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`).
43
- # Declining on the whole expression is FP-safe by the same lower-bound argument.
59
+ when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
60
+ when Prism::CallNode then call_rooted?(node, scope, depth + 1, fold)
61
+ when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
62
+ literal_rooted?(node, scope, depth + 1, fold)
63
+ else combining_rooted?(node, scope, depth + 1, fold)
64
+ end
65
+ end
66
+
67
+ def call_rooted?(node, scope, depth, fold)
68
+ return rooted?(node.receiver, scope, depth, fold: fold) unless array_conversion?(node, scope)
69
+
70
+ rooted?(node.arguments.arguments.first, scope, depth, fold: fold)
71
+ end
72
+
73
+ # A receiverless `Array(x)` reaching Kernel#Array: exactly one plain argument, no block, and no project
74
+ # `Array` on `self`'s class or its project ancestors to bind ahead of Kernel's.
75
+ def array_conversion?(node, scope)
76
+ node.receiver.nil? && node.name == :Array && node.block.nil? &&
77
+ node.arguments&.arguments&.size == 1 && !project_array_method?(scope)
78
+ end
79
+
80
+ def project_array_method?(scope)
81
+ self_type = scope.self_type
82
+ case self_type
83
+ when Type::Nominal then scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :instance)
84
+ when Type::Singleton
85
+ scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :singleton)
86
+ else false
87
+ end
88
+ end
89
+
90
+ # An array or hash literal is rooted when any element is: the engine reads the element back precisely.
91
+ def literal_rooted?(node, scope, depth, fold)
92
+ parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
93
+ parts.each_with_index.any? { |part, index| rooted?(part, scope, depth, fold: fold && index.zero?) }
94
+ end
95
+
96
+ # Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
97
+ # operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
98
+ # on the whole expression is FP-safe by the same lower-bound argument.
99
+ def combining_rooted?(node, scope, depth, fold)
100
+ case node
44
101
  when Prism::RescueModifierNode
45
- rooted?(node.expression, scope, depth + 1) || rooted?(node.rescue_expression, scope, depth + 1)
102
+ rooted?(node.expression, scope, depth, fold: fold) ||
103
+ rooted?(node.rescue_expression, scope, depth, fold: false)
46
104
  when Prism::AndNode, Prism::OrNode
47
- rooted?(node.left, scope, depth + 1) || rooted?(node.right, scope, depth + 1)
48
- when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth + 1)
49
- when Prism::StatementsNode then rooted?(node.body.last, scope, depth + 1)
105
+ rooted?(node.left, scope, depth, fold: fold) || rooted?(node.right, scope, depth, fold: false)
106
+ when Prism::ParenthesesNode then body_rooted?(node.body, scope, depth, fold)
107
+ when Prism::StatementsNode then body_rooted?(node, scope, depth, fold)
108
+ when Prism::IfNode, Prism::UnlessNode then conditional_rooted?(node, scope, depth, fold)
109
+ when Prism::ElseNode then rooted?(node.statements, scope, depth, fold: fold)
50
110
  else false
51
111
  end
52
112
  end
53
113
 
54
- def last_statement(body)
55
- body.is_a?(Prism::StatementsNode) ? body.body.last : body
114
+ # A body's value is its last statement; any statement before it runs first.
115
+ def body_rooted?(body, scope, depth, fold)
116
+ return rooted?(body, scope, depth, fold: fold) unless body.is_a?(Prism::StatementsNode)
117
+
118
+ rooted?(body.body.last, scope, depth, fold: fold && body.body.size == 1)
119
+ end
120
+
121
+ # Every branch that can run must be rooted; an absent branch is `nil`, never rooted. The predicate is
122
+ # typed only when the branches disagree, so a conditional that never mentions a parameter costs nothing.
123
+ # An arm runs after the predicate, so a conditional nested in one never folds.
124
+ def conditional_rooted?(node, scope, depth, fold)
125
+ truthy_rooted, falsey_rooted = arms(node).map { |arm| rooted?(arm, scope, depth, fold: false) }
126
+ return truthy_rooted if truthy_rooted == falsey_rooted
127
+ return false unless fold
128
+
129
+ case predicate_certainty(node.predicate, scope)
130
+ when :truthy then truthy_rooted
131
+ when :falsey then falsey_rooted
132
+ else false
133
+ end
134
+ end
135
+
136
+ # `[runs when the predicate is truthy, runs when it is falsey]`.
137
+ def arms(node)
138
+ node.is_a?(Prism::IfNode) ? [node.statements, node.subsequent] : [node.else_clause, node.statements]
139
+ end
140
+
141
+ # The arm certainty `StatementEvaluator#branch_certainty` elides on: a decidable version guard, else the
142
+ # predicate's folded truthiness unless an optimistic carrier props it up.
143
+ def predicate_certainty(predicate, scope)
144
+ guard = Inference::VersionGuard.verdict(predicate)
145
+ return guard if guard
146
+ return nil unless Inference::OptimisticOrigin.resolve(predicate, scope).nil?
147
+
148
+ Inference::Narrowing.predicate_certainty(scope.type_of(predicate))
56
149
  end
57
150
  end
58
151
  end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "rule_walk"
6
+ require_relative "../../scope/resolution_chain"
6
7
  require_relative "../../source/constant_path"
7
8
  require_relative "../../source/node_children"
8
9
 
@@ -37,36 +38,55 @@ module Rigor
37
38
  # `def` that sits directly in a class / module body, never one nested in another `def`. On the
38
39
  # shared full DFS that prune becomes the `:inside_def` gate; the enclosing class / module name stack
39
40
  # the legacy walk threaded as `qualified_prefix` is now `context.qualified_prefix`.
40
- NODE_CLASSES = [Prism::DefNode].freeze
41
+ #
42
+ # ADR-125 — the walk also visits class-body `CallNode`s (a `def` body is gated off), to record the
43
+ # macro calls whose literal arguments name a method (`before_action :load_roles`).
44
+ NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
41
45
  RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
42
46
 
43
- # Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:}>]]`.
44
- # Empty when the tree has no qualifying writes. `declared` is the translated type of the class's RBS
45
- # declaration of the ivar — an `@x: T` member or the slot an `attr_*` member implies, own or
46
- # inherited — and nil when the RBS declares none.
47
+ # Groups the writes by class qualified name, then by ivar name. Each write carries `node`, `type`,
48
+ # `declared`, `method_name`, `visibility`, `per_invocation` and `action`. Empty when the tree has no
49
+ # qualifying writes.
50
+ # `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
51
+ # the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
52
+ #
53
+ # ADR-125 — `method_name` is the enclosing `def`'s name and `visibility` its instance visibility
54
+ # (`:public` / `:protected` / `:private`, or nil when the discovery table has no settled answer).
55
+ # `per_invocation` is whether the class is, or descends from, a class a loaded plugin declares in
56
+ # `per_invocation_classes:`. `action` is whether the enclosing method is one of that class's entry
57
+ # points: public, and named by none of the declaring plugin's `per_invocation_non_action_macros:` in a
58
+ # class body of this file (`before_action :load_roles`, `helper_method :current_roles`).
47
59
  def initialize(scope_index)
48
60
  @scope_index = scope_index
49
61
  @accumulator = {}
62
+ @per_invocation = {}
63
+ @macro_arguments = {}
50
64
  end
51
65
 
52
66
  # Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
53
67
  # {RuleWalk} against; deleted when Track B completes.
54
68
  def collect(root)
55
69
  walk(root, [])
56
- @accumulator.transform_values(&:freeze).freeze
70
+ results
57
71
  end
58
72
 
59
73
  # {RuleWalk} entry point: the legacy walk's per-`def` logic, invoked at each class-/module-body `def`
60
74
  # under the shared traversal contract. `collect_def_writes`' verbatim body reads the enclosing class
61
75
  # prefix from `context.qualified_prefix`.
62
- def visit(def_node, context)
63
- collect_def_writes(def_node, context.qualified_prefix)
76
+ def visit(node, context)
77
+ if node.is_a?(Prism::CallNode)
78
+ record_macro_call(node, context.qualified_prefix)
79
+ else
80
+ collect_def_writes(node, context.qualified_prefix)
81
+ end
64
82
  end
65
83
 
66
84
  # The accumulated result, frozen the same way `#collect` returns it — used by {RuleWalk}-driven
67
85
  # callers after the walk completes.
68
86
  def results
69
- @accumulator.transform_values(&:freeze).freeze
87
+ @accumulator.to_h do |class_name, writes_by_ivar|
88
+ [class_name, writes_by_ivar.transform_values { |writes| with_actions(class_name, writes) }.freeze]
89
+ end.freeze
70
90
  end
71
91
 
72
92
  private
@@ -87,29 +107,71 @@ module Rigor
87
107
  when Prism::DefNode
88
108
  collect_def_writes(node, qualified_prefix)
89
109
  return
110
+ when Prism::CallNode
111
+ record_macro_call(node, qualified_prefix)
90
112
  end
91
113
 
92
114
  node.rigor_each_child { |child| walk(child, qualified_prefix) }
93
115
  end
94
116
 
117
+ # A receiverless (or `self.`) call in a class body, with the method names its literal Symbol / String
118
+ # arguments spell. Kept for every such call: which macro names count is the registry's answer, read
119
+ # once per class when the writes are finalised.
120
+ def record_macro_call(call_node, qualified_prefix)
121
+ return if qualified_prefix.empty?
122
+ return unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
123
+
124
+ names = (call_node.arguments&.arguments || []).filter_map { |argument| literal_method_name(argument) }
125
+ return if names.empty?
126
+
127
+ ((@macro_arguments[qualified_prefix.join("::")] ||= {})[call_node.name] ||= []).concat(names)
128
+ end
129
+
130
+ # A Symbol / String literal's name, or the name of a `def` passed inline (`helper_method def
131
+ # current_user … end`). A literal with bytes no Symbol can hold (`before_action "\xff"`, #1821) names no
132
+ # method, so it answers nil.
133
+ def literal_method_name(argument)
134
+ return argument.name if argument.is_a?(Prism::DefNode) && argument.receiver.nil?
135
+ return nil unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
136
+
137
+ text = argument.unescaped
138
+ text.valid_encoding? ? text.to_sym : nil
139
+ end
140
+
141
+ # Each write with `action` set (see {#initialize}). A frozen copy, so `#results` stays repeatable.
142
+ def with_actions(class_name, writes)
143
+ macros = @per_invocation[class_name]
144
+ excluded = macros ? non_action_names(class_name, macros) : nil
145
+ writes.map do |write|
146
+ action = !excluded.nil? && write[:visibility] == :public && !excluded.include?(write[:method_name])
147
+ write.merge(action: action).freeze
148
+ end.freeze
149
+ end
150
+
151
+ def non_action_names(class_name, macros)
152
+ (@macro_arguments[class_name] || {}).each_with_object(Set.new) do |(macro, names), set|
153
+ set.merge(names) if macros.include?(macro)
154
+ end
155
+ end
156
+
95
157
  def collect_def_writes(def_node, qualified_prefix)
96
158
  return if def_node.body.nil? || qualified_prefix.empty?
97
159
  return if def_node.receiver.is_a?(Prism::SelfNode)
98
160
 
99
161
  class_name = qualified_prefix.join("::")
100
- gather_writes(def_node.body, class_name)
162
+ gather_writes(def_node.body, class_name, def_node.name)
101
163
  end
102
164
 
103
- def gather_writes(node, class_name)
165
+ def gather_writes(node, class_name, method_name)
104
166
  return unless node.is_a?(Prism::Node)
105
167
 
106
- record_write(node, class_name) if node.is_a?(Prism::InstanceVariableWriteNode)
168
+ record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
107
169
  return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
108
170
 
109
- node.rigor_each_child { |child| gather_writes(child, class_name) }
171
+ node.rigor_each_child { |child| gather_writes(child, class_name, method_name) }
110
172
  end
111
173
 
112
- def record_write(node, class_name)
174
+ def record_write(node, class_name, method_name)
113
175
  scope = @scope_index[node]
114
176
  return if scope.nil?
115
177
 
@@ -117,7 +179,73 @@ module Rigor
117
179
  declared = declared_ivar(scope, class_name, node.name)
118
180
  @accumulator[class_name] ||= {}
119
181
  @accumulator[class_name][node.name] ||= []
120
- @accumulator[class_name][node.name] << { node: node, type: rvalue_type, declared: declared }
182
+ @accumulator[class_name][node.name] << {
183
+ node: node, type: rvalue_type, declared: declared, method_name: method_name,
184
+ visibility: method_visibility(scope, class_name, method_name),
185
+ per_invocation: !per_invocation_macros(scope, class_name).nil?
186
+ }
187
+ end
188
+
189
+ # The enclosing `def`'s instance visibility, or nil when it is not a settled fact: a slot the
190
+ # discovery table does not hold, or one a later `define_method` / alias / unplaceable write contests
191
+ # (#2013). `initialize` and the other lifecycle hooks are private whatever the table says.
192
+ def method_visibility(scope, class_name, method_name)
193
+ return :private if CheckRules::IMPLICITLY_PRIVATE_METHODS.include?(method_name)
194
+ return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
195
+
196
+ scope.discovered_method_visibility(class_name, method_name)
197
+ end
198
+
199
+ # ADR-125 — when `class_name` is, or descends from, a class a loaded plugin declares per-invocation, the
200
+ # non-action macros its declaring manifests list (a Set, possibly empty); nil otherwise. Read off the
201
+ # class's instance {Scope::ResolutionChain}, so a project superclass chain
202
+ # (`UsersController < ApplicationController < ActionController::Base`) is followed as Ruby resolves it.
203
+ # A project entry matches by name. An external superclass matches through the first of its candidate
204
+ # spellings the environment knows, by name or by an RBS-known `:equal` / `:subclass` ordering. When the
205
+ # environment knows none of them (rigor-actionpack leaves `ActionController::Base` undeclared on
206
+ # purpose), a candidate that is literally a declared name matches, unless some candidate names a class
207
+ # the project itself declares: then the spelling may resolve there instead. An ambiguous spelling (no
208
+ # candidates) and a truncated chain answer nil: the carve-out only ever withholds a finding, so an
209
+ # unproved descent keeps the rule firing. Memoised per class.
210
+ def per_invocation_macros(scope, class_name)
211
+ return @per_invocation[class_name] if @per_invocation.key?(class_name)
212
+
213
+ @per_invocation[class_name] = matched_per_invocation_macros(scope, class_name)
214
+ end
215
+
216
+ def matched_per_invocation_macros(scope, class_name)
217
+ environment = scope.environment
218
+ registry = environment&.plugin_registry
219
+ return nil if registry.nil? || registry.per_invocation_classes.empty?
220
+
221
+ chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
222
+ return nil if chain.truncated?
223
+
224
+ declared = chain.search(scope) { |entry| matched_declarations(entry, registry, scope) }
225
+ declared&.each_with_object(Set.new) { |name, set| set.merge(registry.per_invocation_non_action_macros(name)) }
226
+ end
227
+
228
+ # The declared per-invocation names `entry` matches, or nil (so {ResolutionChain#search} moves on).
229
+ def matched_declarations(entry, registry, scope)
230
+ return (registry.per_invocation_class?(entry.name.to_s) ? [entry.name.to_s] : nil) unless entry.external?
231
+ return nil unless entry.superclass_edge
232
+
233
+ environment = scope.environment
234
+ names = entry.candidates.map { |name| name.to_s.delete_prefix("::") }
235
+ known = names.find { |name| environment.class_known?(name) }
236
+ return literal_declarations(names, registry, scope) if known.nil?
237
+
238
+ matched = registry.per_invocation_classes.select do |declared|
239
+ %i[equal subclass].include?(environment.class_ordering(known, declared))
240
+ end
241
+ matched.empty? ? nil : matched
242
+ end
243
+
244
+ def literal_declarations(names, registry, scope)
245
+ return nil if names.any? { |name| scope.discovered_classes.key?(name) }
246
+
247
+ matched = names.select { |name| registry.per_invocation_class?(name) }
248
+ matched.empty? ? nil : matched
121
249
  end
122
250
 
123
251
  def declared_ivar(scope, class_name, ivar_name)
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../../inference/scope_indexer"
6
6
  require_relative "../../source/node_children"
7
+ require_relative "rebinding_block_callees"
7
8
 
8
9
  module Rigor
9
10
  module Analysis
@@ -41,6 +42,9 @@ module Rigor
41
42
 
42
43
  def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
43
44
 
45
+ # Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
46
+ def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
47
+
44
48
  # Does a `def <local>.<name>` for this call's local receiver and method name — or a call that gives the
45
49
  # local's object methods Rigor does not enumerate (issue #1722) — sit in the scope the call is in?
46
50
  def singleton_local_def?(call_node)
@@ -148,7 +152,6 @@ module Rigor
148
152
  end
149
153
  end
150
154
 
151
- # `o.singleton_class`.
152
155
  def singleton_class_of_local?(node)
153
156
  node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? &&
154
157
  node.receiver.is_a?(Prism::LocalVariableReadNode)
@@ -12,16 +12,13 @@ module Rigor
12
12
  # decides, with NO loop / block suppression and NO threaded context — so the collector declares no
13
13
  # `RULE_WALK_GATES` and ignores the walk's `context`.
14
14
  #
15
- # The per-node dispatch itself stays in `CheckRules` (the verbatim `case` from `diagnose`'s former inline
16
- # walk — only the traversal moved, ADR-53 WD4) and is handed in as a callable built in the `CheckRules`
17
- # module context, so its calls to the private diagnostic builders remain implicit-self. The collector
18
- # only owns the traversal hook and accumulation. Unlike the fact collectors its `#results` are the
19
- # accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
20
- # shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
15
+ # The per-node dispatch stays in `CheckRules` (ADR-53 WD4) and is handed in as a callable built in the
16
+ # `CheckRules` module context, so its calls to the private diagnostic builders remain implicit-self. The
17
+ # collector only owns the traversal hook and accumulation; `#results` are the accumulated {Diagnostic}s in
18
+ # the walk's emission order.
21
19
  class MainPassCollector
22
20
  # The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
23
- # too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same). The three global-write
24
- # classes carry issue #1367's `global.*` rules.
21
+ # too. The global-write classes and `MultiWriteNode` carry issue #1367's `global.*` rules.
25
22
  NODE_CLASSES = [
26
23
  Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
27
24
  Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
@@ -19,7 +19,7 @@ module Rigor
19
19
  # branched on, whose declaration the reader's author never opened. A configuration constant read in ten
20
20
  # files would put a warning in all ten.
21
21
  #
22
- # This is [ADR-58](../../../../docs/adr/58-declaration-sourced-ivar-typing.md)'s discipline applied to a
22
+ # This is [ADR-58](../../../../docs/adr/58-ivar-field-typing.md)'s discipline applied to a
23
23
  # different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
24
24
  # can lose precision but can never manufacture a false positive"
25
25
  # (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
@@ -3,6 +3,7 @@
3
3
  require_relative "../../reflection"
4
4
  require_relative "../dependency_recorder"
5
5
  require_relative "../../inference/singleton_hook_decline"
6
+ require_relative "../../inference/object_mixins"
6
7
 
7
8
  module Rigor
8
9
  module Analysis
@@ -68,6 +69,43 @@ module Rigor
68
69
  end
69
70
  end
70
71
 
72
+ # Issue #2094 — the singleton side's twin of {.ancestor_may_answer?}: a module the class or an RBS superclass
73
+ # `extend`s (in its body, as `Float.extend(Ext)`, or as `Float.singleton_class.include(Ext)`) may answer
74
+ # `method_name` on the class object, read as {.mixin_defines?} / {.external_may_answer?} read an instance-side
75
+ # mixin. Only each level's own singleton segment is read: the class's further singleton ancestors are the
76
+ # RBS superclasses this walk visits in turn, and `Class` / `Module` are {.singleton_hook?}'s.
77
+ def singleton_mixin_may_answer?(scope, class_name, method_name)
78
+ singleton_class_names(scope, class_name).any? do |name|
79
+ DependencyRecorder.read_last_segment(:class, name) if DependencyRecorder.active?
80
+
81
+ extends_answer?(scope, name, method_name, open_mixins: !OBJECT_ROOTS.include?(name))
82
+ end
83
+ end
84
+
85
+ def singleton_class_names(scope, class_name)
86
+ loader = scope.environment&.rbs_loader
87
+ pairs = loader ? loader.singleton_ancestors_for(class_name.to_s) : []
88
+ names = pairs.filter_map { |kind, name| name if kind == :singleton }
89
+ names.empty? ? [class_name.to_s] : names
90
+ end
91
+
92
+ def extends_answer?(scope, name, method_name, open_mixins:)
93
+ chain = Scope::ResolutionChain.for(scope, name, :singleton, :methods)
94
+ return true if chain.truncated?
95
+ # A mixin call the indexer could not record (`Integer.extend(m)`, `Symbol.singleton_class.send(:include,
96
+ # M)`) leaves the side tainted: some module the tables do not name may answer any name.
97
+ return true if open_mixins && chain.wildcard_mixin?
98
+
99
+ entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
100
+ entries.any? do |entry|
101
+ if entry.external?
102
+ external_may_answer?(scope, entry, method_name, open_mixins)
103
+ else
104
+ entry.name != name && mixin_defines?(scope, entry, method_name, open_mixins)
105
+ end
106
+ end
107
+ end
108
+
71
109
  # Whether a project subclass of `class_name`, at any depth, defines `method_name` (through its own
72
110
  # ancestry, which reaches its project mixins) or a `method_missing`. Its parent is read as written and
73
111
  # resolved the way the class header resolves it: the first project class the spelling can denote, else the
@@ -132,19 +170,44 @@ module Rigor
132
170
  if Rigor::Reflection.rbs_class_known?(owner, scope: scope)
133
171
  scope.discovered_method?(owner, method_name, :instance)
134
172
  else
135
- project_defines?(scope, owner, method_name) ||
136
- (open_mixins && Inference::SingletonHookDecline.entry_hook_capable?(scope, entry))
173
+ project_defines?(scope, owner, method_name) || macro_names?(scope, owner, method_name) ||
174
+ (open_mixins && (Inference::SingletonHookDecline.entry_hook_capable?(scope, entry) ||
175
+ open_extend?(scope, owner)))
137
176
  end
138
177
  end
139
178
 
179
+ # Issue #2094 — a class-body macro of the module names the method ({Inference::ObjectMixins.macro_named?}).
180
+ # Answers the named method only, so it holds on `Object`, `Kernel` and `BasicObject` too, where a module
181
+ # that defines the name answers.
182
+ def macro_names?(scope, owner, method_name)
183
+ Inference::ObjectMixins.macro_named?(scope, owner, method_name)
184
+ end
185
+
186
+ # Issue #2094 — the module `extend`s a module whose surface is open: one neither RBS nor the project declares
187
+ # (a gem's DSL with no signatures), or a project module {Inference::SingletonHookDecline} reads as
188
+ # hook-capable. Any macro the module's body calls may come from it and define any name, and its `extended`
189
+ # hook may too, so the module answers any name, the reading an undeclared `include` already gets. A chain cut
190
+ # at its budget may hold such a module past the cut.
191
+ def open_extend?(scope, owner)
192
+ chain = Scope::ResolutionChain.for(scope, owner, :singleton, :methods)
193
+ return true if chain.truncated?
194
+
195
+ entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
196
+ entries.any? { |extended| Inference::SingletonHookDecline.entry_hook_capable?(scope, extended) }
197
+ end
198
+
140
199
  # An external the project declares (a module with no `def`, which the chain does not hold as a project entry)
141
- # is known, and answers only through RBS.
200
+ # is known, and answers through RBS, or through what its own body's macros and `extend`s may define (issue
201
+ # #2094: {.macro_names?} and {.open_extend?}, asked of each candidate the project declares).
142
202
  def external_may_answer?(scope, entry, method_name, open_mixins)
143
203
  candidates = entry.candidates
144
204
  return open_mixins if candidates.empty?
145
205
  return true if candidates.any? do |candidate|
146
206
  Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
147
207
  end
208
+ return true if candidates.any? do |candidate|
209
+ macro_names?(scope, candidate, method_name) || (open_mixins && open_extend?(scope, candidate))
210
+ end
148
211
 
149
212
  open_mixins && candidates.none? { |candidate| declared?(scope, candidate) }
150
213
  end