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
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ module RebindingBlockCallees
9
+ # Which method a block call reaches, and on what receiver: `callee_may_rebind?` reads the callee name, a
10
+ # callable receiver, and whether Ruby can reach `method_missing` from here.
11
+ module Dispatch
12
+ # Issue #2186 — a block given to `call` / `bind_call` (or `yield` / `===` / `[]`) on a literal `method(:x)`
13
+ # chain goes to the method it names: `Ext.method(:me).call { … }` is `Ext.me { … }`. On a receiver typed as
14
+ # one of CALLABLE_CLASSES it goes to a callable Rigor cannot follow; on any other receiver it is a call by
15
+ # that name.
16
+ METHOD_OBJECT_CALLS = %i[call yield === [] bind_call].freeze
17
+ METHOD_READERS = %i[method public_method singleton_method instance_method public_instance_method].freeze
18
+ # The readers whose receiver is the object the named method runs on.
19
+ BOUND_METHOD_READERS = %i[method public_method singleton_method].freeze
20
+ METHOD_WRAPPERS = %i[bind unbind to_proc curry].freeze
21
+ CALLABLE_CLASSES = %w[Method UnboundMethod Proc].freeze
22
+ private_constant :METHOD_READERS, :BOUND_METHOD_READERS, :METHOD_WRAPPERS, :CALLABLE_CLASSES
23
+
24
+ module_function
25
+
26
+ # `[name, receiver]` of the method the block reaches: the call's own name, the literal name a `send` family
27
+ # call dispatches to, or the method a literal `method(:name)` chain names. The receiver is a node, `:implicit`
28
+ # for a receiverless call, or nil where Rigor cannot say (an `instance_method` chain). The name is nil when a
29
+ # `send` names its method with anything but a valid literal.
30
+ def dispatch(call)
31
+ reader = method_reader(call.receiver) if METHOD_OBJECT_CALLS.include?(call.name)
32
+ if reader
33
+ receiver = reader.receiver || :implicit if BOUND_METHOD_READERS.include?(reader.name)
34
+ return [RebindingBlockCallees.literal_name(reader.arguments&.arguments&.first), receiver]
35
+ end
36
+ [RebindingBlockCallees.send_target(call), call.receiver || :implicit]
37
+ end
38
+
39
+ def method_reader(receiver)
40
+ receiver = receiver.receiver while wrapper?(receiver)
41
+ receiver if receiver.is_a?(Prism::CallNode) && METHOD_READERS.include?(receiver.name)
42
+ end
43
+
44
+ # Whether `receiver` may be a Method, UnboundMethod or Proc, whose `call` runs a callable Rigor cannot follow.
45
+ def callable?(receiver, scope, scope_index)
46
+ return false unless receiver.is_a?(Prism::Node)
47
+
48
+ type = receiver_type(receiver, scope, scope_index)
49
+ members = type.is_a?(Type::Union) ? type.members : [type]
50
+ members.any? do |member|
51
+ member.is_a?(Type::BoundMethod) ||
52
+ (member.is_a?(Type::Nominal) && CALLABLE_CLASSES.include?(member.class_name.to_s))
53
+ end
54
+ end
55
+
56
+ def wrapper?(node) = node.is_a?(Prism::CallNode) && METHOD_WRAPPERS.include?(node.name)
57
+
58
+ # A `call` (or the like) on a receiver that may be a Method, UnboundMethod or Proc.
59
+ def callable_call?(name, receiver, scope, scope_index)
60
+ METHOD_OBJECT_CALLS.include?(name) && callable?(receiver, scope, scope_index)
61
+ end
62
+
63
+ def receiver_type(receiver, scope, scope_index)
64
+ (scope_index&.[](receiver) || scope).type_of(receiver)
65
+ rescue StandardError
66
+ nil
67
+ end
68
+
69
+ # Whether the receiver's type names a class whose RBS or project table gives it `name`, on every member of a
70
+ # union. A receiverless call at the top level asks `Object`, `main`'s class. Any other type cannot say.
71
+ def resolves_on_receiver?(receiver, name, scope, scope_index)
72
+ return scope.toplevel? && class_defines?("Object", :instance, name, scope) if receiver == :implicit
73
+ return false if receiver.nil?
74
+
75
+ type = receiver_type(receiver, scope, scope_index)
76
+ !type.nil? && type_resolves?(type, name, scope)
77
+ rescue StandardError
78
+ false
79
+ end
80
+
81
+ def type_resolves?(type, name, scope)
82
+ case type
83
+ when Type::Union then type.members.all? { |member| type_resolves?(member, name, scope) }
84
+ when Type::Singleton then class_defines?(type.class_name, :singleton, name, scope)
85
+ else
86
+ class_name = receiver_class_name(type)
87
+ !class_name.nil? && class_defines?(class_name, :instance, name, scope)
88
+ end
89
+ end
90
+
91
+ def receiver_class_name(type)
92
+ case type
93
+ when Type::Nominal, Type::StructInstance, Type::DataInstance then type.class_name&.to_s
94
+ when Type::Tuple then "Array"
95
+ when Type::HashShape then "Hash"
96
+ when Type::Constant then type.value.class.name
97
+ end
98
+ end
99
+
100
+ def class_defines?(class_name, side, name, scope)
101
+ rbs = if side == :singleton
102
+ Rigor::Reflection.singleton_method_definition(class_name, name, scope: scope)
103
+ else
104
+ Rigor::Reflection.instance_method_definition(class_name, name, scope: scope)
105
+ end
106
+ !rbs.nil? || scope.discovered_method?(class_name, name, side)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../dependency_recorder"
6
+ require_relative "../../inference/scope_indexer"
7
+
8
+ module Rigor
9
+ module Analysis
10
+ module CheckRules
11
+ # Issue #1817 — `call.unresolved-toplevel` declines inside a block handed to a project method that may run it
12
+ # under another `self`. `def self.define(&blk) = module_eval(&blk)` makes `Ext.define { refine(String) { … } }`
13
+ # call `refine` on `Ext`, not on `main`, so the call is not a top-level one even though the block sits at the
14
+ # top of the file.
15
+ #
16
+ # The reading is by method NAME across every project definition, never by the receiver's type: any project
17
+ # `def` of the block's callee name that may hand the block to something else declines the rule. That
18
+ # over-reads (an unrelated class's same-named method declines too), which only loses a report; resolving the
19
+ # receiver would read one class and could miss the definition Ruby actually dispatches to, which reports on
20
+ # correct code. A definition "may hand the block on" unless every use of it is provably a plain call:
21
+ #
22
+ # - its body only `yield`s, or reads the block parameter only as the receiver of `call` / `()` / `[]` /
23
+ # `yield` / `===` / `arity` / `lambda?` / `parameters` / `nil?` / `!`, or as an `if` / `unless` / `while` /
24
+ # `until` predicate or the left side of `&&` — each of which runs the block with its own `self` or not at
25
+ # all;
26
+ # - any other read (`module_eval(&blk)`, `other(&blk)`, `@blk = blk`, `return blk`), an anonymous `&` or a
27
+ # `...` forwarded anywhere, a `super` that passes the block on implicitly, or a `binding` / `eval` that
28
+ # could reach it, may rebind `self`. Where in the body it sits does not matter: an eval under an `if` may
29
+ # run.
30
+ #
31
+ # A callee name the project gives a method without a readable `def` (`attr_*`, `alias_method`,
32
+ # `define_method`, `define_singleton_method` on any receiver, a `def` written inside a block), or patches
33
+ # through `pre_eval:`, cannot be read and declines too. So does a `send` / `__send__` / `public_send` whose
34
+ # method name is not a literal. Issue #2184 — a project `method_missing` that may hand its block on answers
35
+ # for every name, since Ruby reaches it for any name the receiver lacks; and the block of a `define_method` /
36
+ # `define_singleton_method` call is a method body, run under whatever receives the method. A callee no project
37
+ # file defines keeps the rule's old answer otherwise: Rigor reads only the project's own bodies here.
38
+ module RebindingBlockCallees
39
+ SEND_CALLS = %i[send __send__ public_send].freeze
40
+ # Reading the block through these runs it with its own `self`, or does not run it.
41
+ PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
42
+ # Calls, on any receiver, that can reach a local by name: `binding`, `eval`, `local_variable_get`. The string
43
+ # forms of the `*_eval` family (`STRING_EVAL_CALLS`, with an argument) run in the caller's scope too.
44
+ BINDING_CALLS = %i[binding eval local_variable_get].freeze
45
+ STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
46
+ private_constant :STRING_EVAL_CALLS
47
+ PREDICATE_PARENTS = [Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode].freeze
48
+ # `proc { … }`, `lambda { … }` and `Proc.new { … }` passed with `&` are the block of the call they go to.
49
+ PROC_LITERAL_CALLS = %i[proc lambda].freeze
50
+ # Core methods that run their block under another `self`: as a method body, or under the receiver (#2186:
51
+ # also when the block is a proc passed through a local, `Ext.instance_exec(&blk)`).
52
+ REBINDING_CORE_CALLS = %i[
53
+ define_method define_singleton_method
54
+ instance_eval instance_exec class_eval class_exec module_eval module_exec
55
+ ].freeze
56
+ private_constant :REBINDING_CORE_CALLS
57
+ private_constant :SEND_CALLS, :PLAIN_PROC_CALLS, :BINDING_CALLS, :PREDICATE_PARENTS, :PROC_LITERAL_CALLS
58
+
59
+ # The calls in one file that pass a literal block, each with the offsets that block's body spans, plus the
60
+ # file's memo of callee answers. Built on the first ask, so a file whose would-fire calls all decline earlier
61
+ # never walks. One file is one ADR-46 consumer, so an answer's dependency edges, recorded on its first ask,
62
+ # hold for every later call in the file that reuses it.
63
+ #
64
+ # Issue #2186 — a proc literal that is a value rather than a call's block (`blk = proc { … }`) adds a row for
65
+ # each call that receives it as `&blk`, when every other read of that local only calls or tests it. A literal
66
+ # called in place (`proc { … }.call`) runs with its own `self` and adds none. Any other proc value (stored,
67
+ # passed positionally, read another way, or in a method (or the code around it) that may read locals by name
68
+ # through `binding` / `eval` / `local_variable_get` or a computed `send`) cannot be followed to the call that
69
+ # runs it, so its row is {UNKNOWN}, which declines. A heredoc that opens inside a block adds a row over its
70
+ # body: Prism puts the body after the block's closing brace, but Ruby runs its interpolation inside the block.
71
+ class Sites
72
+ # The target of a row whose block Rigor cannot follow to the call that runs it.
73
+ UNKNOWN = :unknown
74
+
75
+ attr_reader :answers
76
+
77
+ def initialize(root)
78
+ @root = root
79
+ @rows = nil
80
+ @answers = {}
81
+ @def_rows = nil
82
+ end
83
+
84
+ # The block-passing calls (or {UNKNOWN}) whose block contains `node`. Rows are sorted by start, so the scan
85
+ # stops at the first block that starts after `node`.
86
+ def enclosing(node)
87
+ build
88
+ start = node.location.start_offset
89
+ stop = node.location.end_offset
90
+ found = []
91
+ @rows.each do |from, to, call|
92
+ break if from > start
93
+
94
+ found << call if stop <= to
95
+ end
96
+ found
97
+ end
98
+
99
+ # The project's named `def` row counts, keyed by method name and then by owner and side; built once per file.
100
+ def def_rows(scope)
101
+ @def_rows ||= RebindingBlockCallees.def_row_counts(scope)
102
+ end
103
+
104
+ private
105
+
106
+ def build
107
+ return if @rows
108
+
109
+ @rows = []
110
+ @bodies = []
111
+ @heredocs = []
112
+ @proc_values = []
113
+ @local_reads = Hash.new { |hash, name| hash[name] = [] }
114
+ @block_passes = Hash.new { |hash, name| hash[name] = [] }
115
+ @scopes_reading_locals = Set.new.compare_by_identity
116
+ walk unless @root.nil?
117
+ # A block call chained on another (`m { }.then { }`) is met after the call whose block starts later.
118
+ @rows.sort_by!(&:first)
119
+ end
120
+
121
+ def walk
122
+ collect(@root, nil, @root)
123
+ @proc_values.each do |body, parent, local_scope|
124
+ value_targets(parent, local_scope).each { |target| @bodies << [body, target] }
125
+ end
126
+ @heredocs.sort_by!(&:first)
127
+ @bodies.each { |body, target| add_rows(body, target) }
128
+ end
129
+
130
+ def collect(node, parent, local_scope)
131
+ heredoc = RebindingBlockCallees.heredoc_range(node)
132
+ @heredocs << heredoc if heredoc
133
+ note(node, parent, local_scope)
134
+ # A `def` opens a new local-variable scope; blocks and lambdas share the enclosing one. A class body is
135
+ # read as part of the scope around it, which only widens the set a `binding` there may reach.
136
+ inner = node.is_a?(Prism::DefNode) ? node : local_scope
137
+ node.compact_child_nodes.each { |child| collect(child, node, inner) }
138
+ end
139
+
140
+ def note(node, parent, local_scope)
141
+ case node
142
+ when Prism::CallNode then note_call(node, parent, local_scope)
143
+ when Prism::LambdaNode then note_value(node, node, parent, local_scope)
144
+ when Prism::LocalVariableReadNode
145
+ @local_reads[node.name] << [node, parent] unless parent.is_a?(Prism::BlockArgumentNode)
146
+ end
147
+ end
148
+
149
+ def note_call(node, parent, local_scope)
150
+ block = node.block
151
+ body = RebindingBlockCallees.block_body(block)
152
+ @bodies << [body, node] if body
153
+ if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
154
+ @block_passes[block.expression.name] << node
155
+ end
156
+ value = RebindingBlockCallees.proc_literal(node)
157
+ note_value(node, value, parent, local_scope) if value
158
+ target = RebindingBlockCallees.send_target(node)
159
+ @scopes_reading_locals << local_scope if target.nil? || BINDING_CALLS.include?(target)
160
+ end
161
+
162
+ # `literal` is the proc expression and `body` its block. Passed with `&`, it is that call's block.
163
+ def note_value(literal, body, parent, local_scope)
164
+ return if parent.is_a?(Prism::BlockArgumentNode) || RebindingBlockCallees.plain_read?(literal, parent)
165
+
166
+ @proc_values << [body, parent, local_scope]
167
+ end
168
+
169
+ # The calls a proc value assigned to a local reaches as `&local`, or {UNKNOWN}.
170
+ def value_targets(parent, local_scope)
171
+ return [UNKNOWN] if @scopes_reading_locals.include?(local_scope)
172
+ return [UNKNOWN] unless parent.is_a?(Prism::LocalVariableWriteNode)
173
+
174
+ name = parent.name
175
+ followed = @local_reads[name].all? do |read, read_parent|
176
+ RebindingBlockCallees.plain_read?(read, read_parent)
177
+ end
178
+ followed ? @block_passes[name] : [UNKNOWN]
179
+ end
180
+
181
+ # The block's own row, plus one over the body of each heredoc that opens inside it.
182
+ def add_rows(body, target)
183
+ from = body.location.start_offset
184
+ to = body.location.end_offset
185
+ @rows << [from, to, target]
186
+ index = @heredocs.bsearch_index { |(opening, _start, _stop)| opening >= from }
187
+ return if index.nil?
188
+
189
+ while index < @heredocs.size && @heredocs[index].first < to
190
+ _opening, start, stop = @heredocs[index]
191
+ @rows << [start, stop, target]
192
+ index += 1
193
+ end
194
+ end
195
+ end
196
+
197
+ module_function
198
+
199
+ # Does `call_node` sit inside a block that some project definition of its callee may run under another `self`?
200
+ def inside_rebound_block?(call_node, scope, sites, scope_index = nil)
201
+ sites.enclosing(call_node).any? do |block_call|
202
+ block_call == Sites::UNKNOWN || callee_may_rebind?(block_call, scope, sites, scope_index)
203
+ end
204
+ end
205
+
206
+ # `[opening, body start, end]` offsets of a heredoc `node` opens, or nil.
207
+ def heredoc_range(node)
208
+ opening = node.opening_loc if node.respond_to?(:opening_loc)
209
+ closing = node.closing_loc if node.respond_to?(:closing_loc)
210
+ return nil unless opening&.slice&.start_with?("<<") && closing
211
+
212
+ start = case node
213
+ when Prism::StringNode, Prism::XStringNode then node.content_loc.start_offset
214
+ when Prism::InterpolatedStringNode, Prism::InterpolatedXStringNode
215
+ node.parts.first&.location&.start_offset
216
+ end
217
+ [opening.start_offset, start || closing.start_offset, closing.end_offset]
218
+ end
219
+
220
+ # The node whose span is the block `call` passes: its literal block, or a lambda / proc literal passed with `&`.
221
+ def block_body(block)
222
+ case block
223
+ when Prism::BlockNode then block
224
+ when Prism::BlockArgumentNode then proc_literal(block.expression)
225
+ end
226
+ end
227
+
228
+ def proc_literal(node)
229
+ case node
230
+ when Prism::LambdaNode then node
231
+ when Prism::CallNode
232
+ node.block if node.block.is_a?(Prism::BlockNode) && proc_literal_call?(node)
233
+ end
234
+ end
235
+
236
+ def proc_literal_call?(node)
237
+ return PROC_LITERAL_CALLS.include?(node.name) if node.receiver.nil?
238
+
239
+ node.name == :new && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Proc
240
+ end
241
+
242
+ def callee_may_rebind?(block_call, scope, sites, scope_index = nil)
243
+ name, receiver = Dispatch.dispatch(block_call)
244
+ return true if name.nil? || REBINDING_CORE_CALLS.include?(name)
245
+ return true if Dispatch.callable_call?(name, receiver, scope, scope_index)
246
+ return true if answer(name, scope, sites)
247
+ # Ruby reaches `method_missing` only for a name the receiver lacks.
248
+ return false if Dispatch.resolves_on_receiver?(receiver, name, scope, scope_index)
249
+
250
+ answer(:method_missing, scope, sites)
251
+ end
252
+
253
+ def answer(name, scope, sites)
254
+ answers = sites.answers
255
+ return answers[name] if answers.key?(name)
256
+
257
+ answers[name] = project_may_rebind?(name, scope, sites)
258
+ end
259
+
260
+ # The call's own name, or the literal name a `send` family call dispatches to (nil when it is not one).
261
+ def send_target(call)
262
+ return call.name unless SEND_CALLS.include?(call.name)
263
+
264
+ literal_name(call.arguments&.arguments&.first)
265
+ end
266
+
267
+ def literal_name(first)
268
+ case first
269
+ when Prism::SymbolNode, Prism::StringNode
270
+ text = first.unescaped
271
+ text.to_sym if text.valid_encoding?
272
+ end
273
+ end
274
+
275
+ # Reads every project table for `name`, so it records a `defines:<name>` edge whichever way it answers (a file
276
+ # that starts or stops defining the name re-checks this one), plus each read body's own method edge through the
277
+ # scope accessors (an edit of that body re-checks this one).
278
+ def project_may_rebind?(name, scope, sites)
279
+ DependencyRecorder.read_missing(:defines, name) if DependencyRecorder.active?
280
+ return true if patched_anywhere?(name, scope)
281
+
282
+ slots = Set.new
283
+ readers = {
284
+ [scope.discovered_def_nodes, :instance] => ->(owner) { instance_def(scope, owner, name) },
285
+ [scope.discovered_singleton_def_nodes, :singleton] => ->(owner) { scope.singleton_def_for(owner, name) }
286
+ }
287
+ readers.each do |(tables, side), reader|
288
+ tables.each do |owner, table|
289
+ next unless table.key?(name)
290
+
291
+ slots << [owner, side]
292
+ node = reader.call(owner)
293
+ return true if !node.is_a?(Prism::DefNode) || def_may_rebind?(node)
294
+ end
295
+ end
296
+ unread_definition?(sites.def_rows(scope)[name], slots) || bodiless_definition?(name, scope)
297
+ end
298
+
299
+ def instance_def(scope, owner, name)
300
+ return scope.top_level_def_for(name) if owner == Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
301
+
302
+ scope.user_def_for(owner, name)
303
+ end
304
+
305
+ # A def table keeps one body per class and side, so a second `def` of the name for the same slot (in a
306
+ # sibling file, whose order is not load order, or in the other arm of an `if`) is one the tables never
307
+ # showed, and so is a `def` whose class and side the tables hold no slot for. Every `def` outside a deferred
308
+ # body has a named row in the deferred-range table; a `module_function` row stands for both sides. A row with
309
+ # no side (a `def` inside a block, a `define_singleton_method` name) counts under a nil side, which no slot has.
310
+ def unread_definition?(rows, slots)
311
+ return false if rows.nil?
312
+
313
+ rows.any? { |slot, count| count > 1 || !slots.include?(slot) }
314
+ end
315
+
316
+ def def_row_counts(scope)
317
+ counts = Hash.new { |hash, name| hash[name] = Hash.new(0) }
318
+ scope.discovered_deferred_ranges.each_value do |list|
319
+ list.each do |(_start, _stop, name, kind, owner)|
320
+ next if name.nil?
321
+
322
+ key = owner || Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
323
+ sides = kind == :both ? %i[instance singleton] : [kind]
324
+ sides.each { |side| counts[name][[key, side]] += 1 }
325
+ end
326
+ end
327
+ counts.default_proc = nil
328
+ counts
329
+ end
330
+
331
+ # A name some class gives a method with no `def` body Rigor holds (`attr_*`, `alias_method`, `define_method`).
332
+ def bodiless_definition?(name, scope)
333
+ scope.discovered_methods.any? do |owner, table|
334
+ table.key?(name) &&
335
+ !scope.discovered_def_nodes[owner]&.key?(name) &&
336
+ !scope.discovered_singleton_def_nodes[owner]&.key?(name)
337
+ end
338
+ end
339
+
340
+ def patched_anywhere?(name, scope)
341
+ patched = scope.environment&.project_patched_methods
342
+ !patched.nil? && patched.by_key.any? { |(_owner, patched_name, _kind), _entry| patched_name == name }
343
+ end
344
+
345
+ def def_may_rebind?(def_node)
346
+ return false if def_node.body.nil?
347
+
348
+ block = def_node.parameters&.block
349
+ escapes?(def_node.body, nil, block&.name)
350
+ end
351
+
352
+ # `block_name` is the def's named `&` parameter, or nil.
353
+ def escapes?(node, parent, block_name)
354
+ return true if hands_block_on?(node, parent, block_name)
355
+
356
+ node.compact_child_nodes.any? { |child| escapes?(child, node, block_name) }
357
+ end
358
+
359
+ def hands_block_on?(node, parent, block_name)
360
+ case node
361
+ when Prism::ForwardingSuperNode then !node.block.is_a?(Prism::BlockNode)
362
+ when Prism::SuperNode then node.block.nil?
363
+ when Prism::BlockArgumentNode then node.expression.nil?
364
+ when Prism::ForwardingArgumentsNode then true
365
+ when Prism::LocalVariableReadNode then node.name == block_name && !plain_read?(node, parent)
366
+ when Prism::CallNode then reaches_locals?(node)
367
+ else false
368
+ end
369
+ end
370
+
371
+ # Issue #2186 — a call that can reach the block local by name, on any receiver and through `send`:
372
+ # `Kernel.eval("module_eval(&b)")`, `send(:binding).local_variable_get(:b)`, `instance_eval("module_eval(&b)")`.
373
+ # A `send` whose method name is not a literal may be any of them.
374
+ def reaches_locals?(call)
375
+ name = send_target(call)
376
+ return true if name.nil? || BINDING_CALLS.include?(name)
377
+
378
+ STRING_EVAL_CALLS.include?(name) && !call.arguments.nil?
379
+ end
380
+
381
+ def plain_read?(node, parent)
382
+ case parent
383
+ when Prism::CallNode then parent.receiver.equal?(node) && PLAIN_PROC_CALLS.include?(parent.name)
384
+ when Prism::AndNode then parent.left.equal?(node)
385
+ when *PREDICATE_PARENTS then parent.predicate.equal?(node)
386
+ else false
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ require_relative "rebinding_block_callees/dispatch"
@@ -55,8 +55,7 @@ module Rigor
55
55
  collect_ensure_returns(begin_node)
56
56
  end
57
57
 
58
- # Returns `Array<{return_node:}>` — one entry per offending `return`, frozen the same way
59
- # `#collect` returns it.
58
+ # One `{return_node:}` entry per offending `return`, frozen the same way `#collect` returns them.
60
59
  def results
61
60
  @results.freeze
62
61
  end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "rule_ids"
6
+ require_relative "../dependency_recorder"
6
7
  require_relative "../diagnostic"
7
8
  require_relative "../../inference/version_guard"
8
9
  require_relative "../../source/constant_path"
@@ -243,7 +244,13 @@ module Rigor
243
244
 
244
245
  # Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
245
246
  # side, or at the top level. Asked once per name per file.
247
+ #
248
+ # Issue #2133 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
249
+ # whichever way it answers, as {Inference::BlockCallTiming.project_defines_anywhere?} does: an edit that makes
250
+ # a file start or stop defining the name re-checks the call. A `pre_eval:` patch moves the snapshot's own
251
+ # fingerprint.
246
252
  def project_defines?(scope, name)
253
+ Analysis::DependencyRecorder.read_missing(:defines, name) if Analysis::DependencyRecorder.active?
247
254
  return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
248
255
  return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
249
256
 
@@ -178,7 +178,7 @@ module Rigor
178
178
 
179
179
  # Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
180
180
  # `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
181
- # wildcard meaning "every rule"), or for unknown tokens.
181
+ # wildcard meaning "every rule"), and an empty array for an unknown token.
182
182
  def self.resolve_rule_token(token)
183
183
  return nil if token == "all"
184
184
  return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
@@ -10,7 +10,7 @@ module Rigor
10
10
  module CheckRules
11
11
  # ADR-53 Track B — the engine-owned AST walk that hosts CheckRules' per-node collectors, so a file is
12
12
  # traversed once for all of them instead of once per collector (the ADR-52 WD4 model applied to the
13
- # built-in rules; this walk and {Plugin::NodeRuleWalk} converge in slice B4).
13
+ # built-in rules; {Plugin::NodeRuleWalk} drives the same collectors, ADR-53 B4).
14
14
  #
15
15
  # The walk is a single visit-before-descend DFS over `compact_child_nodes`. It threads the union of the
16
16
  # context the hosted collectors need — the per-collector hand-rolled walks each tracked some subset of
@@ -146,12 +146,14 @@ module Rigor
146
146
  qualified_prefix = extend_prefix(node, context.qualified_prefix)
147
147
  detached_ivar_facet = context.detached_ivar_facet || detached_ivar_facet?(node)
148
148
 
149
- Context.new(
150
- in_loop_or_block: in_loop_or_block,
151
- qualified_prefix: qualified_prefix,
152
- inside_def: inside_def,
153
- detached_ivar_facet: detached_ivar_facet
154
- )
149
+ # Nearly every node leaves all four fields as they were, and a {Context} is immutable, so the parent's
150
+ # is the child's. This runs once per node and was a top allocation site (a Data per node).
151
+ if in_loop_or_block == context.in_loop_or_block && inside_def == context.inside_def &&
152
+ qualified_prefix.equal?(context.qualified_prefix) && detached_ivar_facet == context.detached_ivar_facet
153
+ return context
154
+ end
155
+
156
+ Context.new(in_loop_or_block, qualified_prefix, inside_def, detached_ivar_facet)
155
157
  end
156
158
 
157
159
  # True for a node whose descendants write ivars to a store the enclosing class name does not name:
@@ -123,10 +123,9 @@ module Rigor
123
123
  @scope_index[node] || @scope_index[node.statements] || @scope_index[node.rescue_clause]
124
124
  end
125
125
 
126
- # Resolves one clause to `{clause:, names: [String, …]}` where `names` are the certified exception
127
- # CLASS names it rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat /
128
- # module — the comparisons involving it stay silent). A bare `rescue` names the implicit
129
- # `StandardError`.
126
+ # Resolves one clause to `{clause:, names:}`, where `names` are the certified exception CLASS names it
127
+ # rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat / module — the
128
+ # comparisons involving it stay silent). A bare `rescue` names the implicit `StandardError`.
130
129
  def clause_info(clause, prefix, scope)
131
130
  exceptions = clause.exceptions
132
131
  if exceptions.empty?
@@ -9,9 +9,9 @@ module Rigor
9
9
  # the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
10
10
  #
11
11
  # The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
12
- # `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
13
- # declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
14
- # that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
12
+ # `$;` is declared as a Regexp, a String or nil, but its setter also takes any object with `to_str`, and
13
+ # `$stdout` is declared as an IO, but its setter takes anything that responds to `write`. Each entry names
14
+ # the CRuby function that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
15
15
  #
16
16
  # A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
17
17
  # (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises