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
@@ -56,8 +56,7 @@ module Rigor
56
56
  end
57
57
 
58
58
  # @param root — the local-variable scope to scan.
59
- # @param layout_lookup — a `String -> Array[Symbol] | nil` resolver mapping a constant receiver name to its
60
- # struct member list.
59
+ # @param layout_lookup — a resolver mapping a constant receiver name to its struct member list.
61
60
  # @return the fold-safe local names.
62
61
  def fold_safe_locals(root, layout_lookup)
63
62
  return EMPTY if root.nil?
@@ -283,8 +282,7 @@ module Rigor
283
282
  # `[1].each { s.x = v }; s.x` would read the construction value — verified unsound on #525.
284
283
  #
285
284
  # `for` shares the enclosing locals rather than opening a scope, so the block rationale does not apply
286
- # to it; it is a boundary for the same reason `while` is (below), and this comment previously gave the
287
- # wrong reason for it.
285
+ # to it; it is a boundary for the same reason `while` is (below).
288
286
  #
289
287
  # `while` / `until` are boundaries because a single static pass cannot model a member setter across
290
288
  # ITERATIONS. Issue #597 tried to lift this: `eval_loop` does join the body's exit scope with the
@@ -327,8 +325,8 @@ module Rigor
327
325
  return nil unless meta_constant?(call_node.receiver, :Struct)
328
326
 
329
327
  args = call_node.arguments&.arguments || []
330
- # `[0..-2]` cannot return nil for a start of 0, but `Array#[](Range) -> Array[T]?` says it can;
331
- # the `|| []` keeps the read well-typed on that worst case.
328
+ # `[0..-2]` cannot return nil for a start of 0, but the RBS signature of `Array#[]` with a Range says it
329
+ # can; the `|| []` keeps the read well-typed on that worst case.
332
330
  positional = args.last.is_a?(Prism::KeywordHashNode) ? (args[0..-2] || []) : args
333
331
  positional = positional[1..] if positional.first.is_a?(Prism::StringNode)
334
332
  return nil if positional.nil? || positional.empty?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  require_relative "synthetic_method"
4
6
 
5
7
  module Rigor
@@ -65,8 +67,29 @@ module Rigor
65
67
  { "entries" => entries.map(&:to_h), "class_names" => class_names }
66
68
  end
67
69
 
70
+ # What a dispatch can read from the index, digested (nil for the empty index): each key's rows in their
71
+ # first-wins order, with the return type and the Tier B `origin_module` the dispatcher promotes through,
72
+ # and the synthesised class names. The rest of the provenance (the call site's path and line) is left
73
+ # out, so an edit that only moves the `devise` line keeps the digest. `IncrementalSession` compares it
74
+ # across runs (PR #2286 review M1): the index is built from class-body calls that no declaration
75
+ # signature records, so a recheck that sees it move cannot trust its served files.
76
+ def surface_digest
77
+ return nil if empty?
78
+
79
+ rows = entries.each_with_index
80
+ .sort_by { |entry, index| [entry.kind.to_s, entry.class_name, entry.method_name.to_s, index] }
81
+ .map { |entry, _| surface_row(entry) }
82
+ Digest::SHA256.hexdigest((rows + class_names.sort.map { |name| "class\x1f#{name}" }).join("\x00"))
83
+ end
84
+
68
85
  EMPTY_ROW = [].freeze
69
86
 
87
+ def surface_row(entry)
88
+ [entry.kind, entry.class_name, entry.method_name, entry.return_type,
89
+ entry.provenance[:origin_module], entry.provenance[:plugin_id]].join("\x1f")
90
+ end
91
+ private :surface_row
92
+
70
93
  def bucket(entries, kind)
71
94
  h = {}
72
95
  entries.each do |entry|
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../../source/literals"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module SyntheticMethodScanner
9
+ # The instance-method names a project class answers ahead of a module a trait registry includes into it
10
+ # (PR #2286 review S1). Ruby looks a method up on the class itself before any module it includes, and a
11
+ # module the class itself includes or prepends sits ahead of one `devise` included earlier — so where
12
+ # the project names a member, the Tier B entry for it would answer the call from the wrong rung:
13
+ #
14
+ # class User < ApplicationRecord
15
+ # devise :confirmable
16
+ # include PasswordPolicy # defines password_required?
17
+ # def confirmed? = :yes
18
+ # end
19
+ #
20
+ # `User.new.confirmed?` is `:yes` and `password_required?` is the policy's, not `bool` from the Devise
21
+ # module's RBS. {SyntheticMethodScanner} therefore drops a trait entry whose name this index reports.
22
+ #
23
+ # A name counts as the class's when any reopening of the class body (in any project file) writes `def`,
24
+ # `alias` / `alias_method`, `attr_*`, `define_method` or `delegate` for it, or when a project module the
25
+ # class includes or prepends (followed through that module's own `include` / `prepend`) does. The MRO
26
+ # order of an `include` relative to the `devise` call is NOT consulted: a module included before the
27
+ # call sits behind the Devise module at runtime, but the name is declined anyway. Declining is the
28
+ # conservative direction — the call then reaches the tiers below the synthetic one (the discovered
29
+ # method, the project mixin), which answer it as they did before Tier B was reachable. A mixin constant
30
+ # is matched to a project module by name or by `::`-suffix, again widening only what is declined.
31
+ class ProjectMembers
32
+ # The calls that name members through their literal arguments, and the suffixes each name takes.
33
+ SUFFIXES_BY_CALL = Ractor.make_shareable(
34
+ {
35
+ attr_reader: [""], attr: [""], attr_writer: ["="], attr_accessor: ["", "="], delegate: [""]
36
+ }
37
+ )
38
+ NAME_ARG_CALLS = %i[define_method alias_method].freeze
39
+ MIXIN_CALLS = %i[include prepend].freeze
40
+
41
+ Entry = Struct.new(:names, :mixins)
42
+ private_constant :Entry
43
+
44
+ def initialize(asts)
45
+ @classes = Hash.new { |hash, key| hash[key] = Entry.new(Set.new, []) }
46
+ @modules = Hash.new { |hash, key| hash[key] = Entry.new(Set.new, []) }
47
+ asts.each_value { |ast| walk_namespaces(ast, []) unless ast.nil? }
48
+ @declared = {}
49
+ end
50
+
51
+ # True when `class_name`'s own body, or a project module it includes or prepends, names `method_name`.
52
+ def declares?(class_name, method_name)
53
+ (@declared[class_name] ||= declared_names(class_name)).include?(method_name.to_sym)
54
+ end
55
+
56
+ private
57
+
58
+ def declared_names(class_name)
59
+ entry = @classes.fetch(class_name, nil)
60
+ return Set.new.freeze if entry.nil?
61
+
62
+ names = entry.names.dup
63
+ visited = Set.new
64
+ queue = entry.mixins.dup
65
+ until queue.empty?
66
+ module_keys_for(queue.shift).each do |key|
67
+ next unless visited.add?(key)
68
+
69
+ names.merge(@modules[key].names)
70
+ queue.concat(@modules[key].mixins)
71
+ end
72
+ end
73
+ names.freeze
74
+ end
75
+
76
+ def module_keys_for(name)
77
+ @modules.keys.select { |key| key == name || key.end_with?("::#{name}") }
78
+ end
79
+
80
+ def walk_namespaces(node, scope_stack)
81
+ case node
82
+ when Prism::ClassNode, Prism::ModuleNode
83
+ name = SyntheticMethodScanner.class_name_from(node, scope_stack)
84
+ if name
85
+ table = node.is_a?(Prism::ClassNode) ? @classes : @modules
86
+ collect_members(node.body, table[name])
87
+ end
88
+ inner = scope_stack + [node]
89
+ node.body&.rigor_each_child { |child| walk_namespaces(child, inner) }
90
+ else
91
+ node.rigor_each_child { |child| walk_namespaces(child, scope_stack) }
92
+ end
93
+ end
94
+
95
+ # Every member-naming form under the namespace body, at any depth short of a nested namespace (a
96
+ # `private def`, a `def` under a conditional and an `included do ... def` all count). A nested `class`,
97
+ # `module` or `class << self` is another namespace's body.
98
+ def collect_members(node, entry)
99
+ return if node.nil?
100
+
101
+ case node
102
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then return
103
+ when Prism::DefNode then entry.names << node.name if node.receiver.nil?
104
+ when Prism::AliasMethodNode then add_symbol(entry, node.new_name)
105
+ when Prism::CallNode then collect_call(node, entry)
106
+ end
107
+ node.rigor_each_child { |child| collect_members(child, entry) }
108
+ end
109
+
110
+ def collect_call(call, entry)
111
+ return unless call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode)
112
+
113
+ args = call.arguments&.arguments || []
114
+ if MIXIN_CALLS.include?(call.name)
115
+ entry.mixins.concat(args.filter_map { |arg| SyntheticMethodScanner.const_name_string(arg) })
116
+ elsif NAME_ARG_CALLS.include?(call.name)
117
+ add_symbol(entry, args.first)
118
+ elsif (suffixes = SUFFIXES_BY_CALL[call.name])
119
+ args.each { |arg| suffixes.each { |suffix| add_symbol(entry, arg, suffix: suffix) } }
120
+ end
121
+ end
122
+
123
+ def add_symbol(entry, node, suffix: "")
124
+ value = Source::Literals.symbol_or_string_name(node)
125
+ entry.names << :"#{value}#{suffix}" unless value.nil?
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -8,6 +8,7 @@ require_relative "../source/literals"
8
8
  require_relative "../source/node_children"
9
9
  require_relative "synthetic_method"
10
10
  require_relative "synthetic_method_index"
11
+ require_relative "synthetic_method_scanner/project_members"
11
12
 
12
13
  module Rigor
13
14
  module Inference
@@ -63,18 +64,30 @@ module Rigor
63
64
  registries = collect_trait_registries(plugin_registry)
64
65
  nested_templates = collect_nested_class_templates(plugin_registry)
65
66
  return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty? && nested_templates.empty?
66
- # Tier B alone cannot emit without an environment: every trait entry — the direct route and the
67
- # concern-re-targeted one — funnels through `module_instance_method_names`, whose first line
68
- # answers `[]` for a nil environment. The production pre-pass passes `environment: nil` (#476),
69
- # so a project whose only contributing plugin registers trait registries (rigor-devise on a
70
- # Rails app) would otherwise pay a whole-project parse to build a provably empty index. This
71
- # gate mirrors that nil guard and MUST be removed in the change that threads a real environment.
67
+ # Tier B alone cannot emit without an environment: every trait entry funnels through
68
+ # `module_instance_method_names`, which answers `[]` for a nil environment. The production pre-pass always
69
+ # passes one (#476), so this gate guards the direct callers that pass none (a spec, a probe): a
70
+ # registry-only scan would otherwise pay a whole-project parse to build a provably empty index. It mirrors
71
+ # that nil guard and goes with it.
72
72
  return SyntheticMethodIndex::EMPTY if environment.nil? && templates.empty? && nested_templates.empty?
73
73
 
74
- asts = parse_paths(paths, buffer: buffer)
74
+ sources = read_sources(paths, buffer: buffer)
75
+ # With only trait registries contributing, a project that never writes a registry's method name (no
76
+ # `devise` anywhere) cannot produce an entry: a call node named `devise` needs the text `devise` in its
77
+ # file. Skip the parse (the dominant cost) then, and the environment build it would have asked for.
78
+ return SyntheticMethodIndex::EMPTY if templates.empty? && nested_templates.empty? &&
79
+ none_mention_registry_method?(sources, registries)
80
+
81
+ asts = parse_sources(sources)
82
+ entries, class_names = collect_all_entries(asts, templates, registries, nested_templates, environment,
83
+ fact_store)
84
+ SyntheticMethodIndex.new(entries: decline_project_declared(entries, asts), class_names: class_names)
85
+ end
86
+
87
+ # The match + emit walk over every parsed file: `[entries, class_names]`.
88
+ def collect_all_entries(asts, templates, registries, nested_templates, environment, fact_store)
75
89
  hierarchy = build_hierarchy(asts)
76
90
  concern_index = build_concern_index(asts)
77
-
78
91
  entries = []
79
92
  class_names = []
80
93
  asts.each do |path, ast|
@@ -88,8 +101,19 @@ module Rigor
88
101
  end
89
102
  collect_nested_class_entries(entries, class_names, nested_templates, ast, path) unless nested_templates.empty?
90
103
  end
104
+ [entries, class_names]
105
+ end
91
106
 
92
- SyntheticMethodIndex.new(entries: entries, class_names: class_names)
107
+ # PR #2286 review S1 — a Tier B entry whose name the class itself declares, or a project module it includes
108
+ # or prepends declares, is dropped: Ruby answers that call from the project's member, ahead of the module
109
+ # the registry included ({ProjectMembers}). Tier C entries (no `origin_module`) are left as they were.
110
+ def decline_project_declared(entries, asts)
111
+ return entries unless entries.any? { |entry| entry.provenance[:origin_module] }
112
+
113
+ members = ProjectMembers.new(asts)
114
+ entries.reject do |entry|
115
+ entry.provenance[:origin_module] && members.declares?(entry.class_name, entry.method_name)
116
+ end
93
117
  end
94
118
 
95
119
  # Aggregates `(plugin_id, template)` pairs across every plugin's `manifest.heredoc_templates` in registration
@@ -249,16 +273,32 @@ module Rigor
249
273
  call_node.arguments&.arguments&.first
250
274
  end
251
275
 
252
- def parse_paths(paths, buffer: nil)
276
+ # `path => source text` (nil when the file cannot be read), reading an editor buffer's in-flight bytes
277
+ # for the logical path it is bound to.
278
+ def read_sources(paths, buffer: nil)
253
279
  paths.to_h do |path|
254
280
  physical = buffer ? buffer.resolve(path) : path
255
- source = File.read(physical)
256
- [path, Prism.parse(source, filepath: path).value]
281
+ [path, File.read(physical)]
257
282
  rescue StandardError
258
283
  [path, nil]
259
284
  end
260
285
  end
261
286
 
287
+ def parse_sources(sources)
288
+ sources.to_h do |path, source|
289
+ [path, source.nil? ? nil : Prism.parse(source, filepath: path).value]
290
+ rescue StandardError
291
+ [path, nil]
292
+ end
293
+ end
294
+
295
+ def none_mention_registry_method?(sources, registries)
296
+ names = registries.map { |(_plugin_id, registry)| registry.method_name.to_s }
297
+ sources.each_value.none? do |source|
298
+ !source.nil? && names.any? { |name| source.include?(name) }
299
+ end
300
+ end
301
+
262
302
  # ADR-16 slice 4 — Concern re-targeting index.
263
303
  #
264
304
  # Walks every top-level / nested `module M` decl looking for the ActiveSupport::Concern shape:
@@ -493,7 +533,7 @@ module Rigor
493
533
  # (resolved through `registry.modules_by_symbol`; unknown
494
534
  # symbols silently skipped per design decision (2)).
495
535
  #
496
- # Returns an Array<String> of module names in `always_included` order followed by argument order.
536
+ # Returns the module names: the `always_included` ones first, then those for the symbol arguments in order.
497
537
  def resolve_trait_modules(registry, call_node)
498
538
  modules = registry.always_included.dup
499
539
  positional_symbols(call_node, registry).each do |symbol|
@@ -534,6 +574,18 @@ module Rigor
534
574
  # Returns the Symbol method-name list defined on `module_name`'s RBS instance definition. Empty Array when the
535
575
  # module is not in the RBS env (silent skip — the synthetic emit produces nothing rather than fabricating method
536
576
  # names).
577
+ #
578
+ # RBS builds a module's instance definition over its self types, or over `Object` when it declares none:
579
+ # `Kernel#puts`, `Object#hash`, the pretty-printer's mixin — and, for `module Devise::Models::X :
580
+ # ActiveRecord::Base`, every `ActiveRecord::Base` method (`save`, `update`) — appear in its method table
581
+ # beside its own members. They are not what including the module adds, and synthesising them would answer
582
+ # the class's own or inherited methods from the module's view of them. A method is kept only when its
583
+ # defining owner is the module itself or one of the module's own ancestors (its `include` / `prepend`
584
+ # chain, which `definition.ancestors` lists without the self types or `Object`).
585
+ #
586
+ # A non-public method is skipped: the tier answers a call by name, and a private module method answered
587
+ # for `user.secret` would silence the call Ruby rejects. A definition that carries no owner (the specs'
588
+ # stubs) keeps every name.
537
589
  def module_instance_method_names(module_name, environment)
538
590
  return [] if environment.nil?
539
591
 
@@ -543,7 +595,14 @@ module Rigor
543
595
  definition = loader.instance_definition(module_name)
544
596
  return [] if definition.nil?
545
597
 
546
- definition.methods.keys
598
+ own_ancestors = nil
599
+ definition.methods.filter_map do |name, method|
600
+ next name unless method.respond_to?(:defined_in)
601
+ next unless method.public?
602
+
603
+ own_ancestors ||= definition.ancestors.ancestors.to_set { |ancestor| ancestor.name.to_s }
604
+ name if own_ancestors.include?(method.defined_in.to_s)
605
+ end
547
606
  rescue StandardError
548
607
  []
549
608
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../run_scoped_slots"
4
+ require_relative "memo_audit"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #2515 — whether the `<toplevel>` def table's answer for a name IS the method a receiverless call of that
9
+ # name runs, so the callee mutation floor may read the def as written. {.reliable?} is false (the floor reads every
10
+ # argument as written, `StatementEvaluator::FLOOR_EVERY`) when any of these holds for `name`:
11
+ #
12
+ # - {Scope#toplevel_def_ambiguity} names a reason (the slot is contested, several ownerless definition rows of the
13
+ # name exist, a `def self.name` defines it on `main`, an `alias` introduces it, an anonymous class body defines
14
+ # it, `Object` defines it or has a module prepended);
15
+ # - a `pre_eval:` patch defines `name` on `Object`, `Kernel` or `BasicObject`.
16
+ #
17
+ # A visibility toggle (`private :name`, `public :name`) does not change which def answers a receiverless call, and
18
+ # `method_missing` on `Object` only answers a name nothing defines, so neither is a hazard.
19
+ #
20
+ # The answer is a function of the discovery index and the name (the environment is the run's), so it is memoised
21
+ # per index. A recorder is told the `defines:<name>` name edge on every call, hit or not
22
+ # ({Scope#toplevel_def_ambiguity}), so a file that starts or stops defining the name re-checks the caller; the memo
23
+ # is bypassed while one is active, as the callee floor's other memos are.
24
+ module ToplevelDefCensus
25
+ OBJECT_OWNERS = %w[Object Kernel BasicObject].freeze
26
+ RELIABLE_KEY = RunScopedSlots.register(:rigor_toplevel_def_reliable)
27
+ private_constant :OBJECT_OWNERS, :RELIABLE_KEY
28
+
29
+ module_function
30
+
31
+ # True when the `<toplevel>` table's def for `name` is the method a receiverless call of `name` runs.
32
+ def reliable?(scope, name)
33
+ return compute?(scope, name) if Analysis::DependencyRecorder.active? || MemoAudit.suspended?
34
+
35
+ table = (Thread.current[RELIABLE_KEY] ||= ObjectSpace::WeakKeyMap.new)
36
+ by_name = (table[scope.discovery] ||= {})
37
+ return by_name[name] = compute?(scope, name) unless by_name.key?(name)
38
+
39
+ MemoAudit.check("top-level def census", by_name[name], name) { compute?(scope, name) }
40
+ end
41
+
42
+ def compute?(scope, name)
43
+ return false unless scope.toplevel_def_ambiguity(name).nil?
44
+
45
+ !patched?(scope, name)
46
+ end
47
+
48
+ # True when a `pre_eval:` patch defines `name` on an `Object`-level class.
49
+ def patched?(scope, name)
50
+ patched = scope.environment&.project_patched_methods
51
+ return false if patched.nil?
52
+
53
+ patched.by_key.each_key.any? do |(owner, method_name, _kind)|
54
+ method_name == name && OBJECT_OWNERS.include?(owner)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -29,9 +29,10 @@ module Rigor
29
29
  # written in; a refinement seed (`non-empty-array[0]`) still can, since its remover does not close a literal.
30
30
  #
31
31
  # A site whose widening declines leaves the binding as it is, exactly as the straight-line seam does: a
32
- # precise nominal (a declared or inferred `Array[String]` is a claim this seam may not grow), a receiver
33
- # that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a method
34
- # its class does not define). The caller MUST therefore not read an unchanged binding as describing later
32
+ # precise nominal under a site that stores nothing it can see (a declared or inferred `Array[String]` is a claim
33
+ # this seam may not grow; an element rewriter such as `map!` replaces it with the gradual arm, issue #2427), a
34
+ # receiver that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a
35
+ # method its class does not define). The caller MUST therefore not read an unchanged binding as describing later
35
36
  # iterations. An empty-witness refinement under a storing mutator that keeps the witness is not such a decline:
36
37
  # the straight-line widening grows it (issue #936), and gives a site whose arguments describe nothing it stores
37
38
  # (`non-empty-array[String]` under `map!`) the gradual arm itself ({RewriteMutation}),
@@ -64,10 +65,11 @@ module Rigor
64
65
  CalleeStore = Data.define(:call, :arguments)
65
66
 
66
67
  COLLECTION_CLASSES = %w[Array Hash].freeze
68
+ CONTAINER_CLASSES = %w[Array Hash Set].freeze
67
69
  FLOORED_CLASSES = %w[Array Hash String].freeze
68
70
 
69
71
  NO_ARG_NODES = [].freeze
70
- private_constant :COLLECTION_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
72
+ private_constant :COLLECTION_CLASSES, :CONTAINER_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
71
73
 
72
74
  module_function
73
75
 
@@ -99,6 +101,103 @@ module Rigor
99
101
  VALUE_PRESERVING.include?(method_name) && !literal_carrier?(type) ? widened : gradual_content(widened)
100
102
  end
101
103
 
104
+ # Issue #2296 — what the caller's argument `type` can hold after a callee that mutates the parameter inside a
105
+ # block (`def poke(acc) = 2.times { |i| acc[i] = "x" }`). Nothing at the call says what the block stores, so
106
+ # each carrier takes one unknown store and keeps its element type beside the gradual arm, as a block-captured
107
+ # binding does ({.widen}): `[7, 9]` reads `Array[Dynamic[top] | Integer]`, `{ a: 1 }`
108
+ # `Hash[Dynamic[top] | Symbol, Dynamic[top] | Integer]`, `Set[1]` `Set[Dynamic[top] | Integer]`, a literal
109
+ # String `String`. Not {.content_floor}'s bare carrier: dropping the element type there widened
110
+ # `ScopeIndexer.finalize_constant_writes` to `Hash[untyped, untyped]` under sig-gen (#2295). A class-level
111
+ # nominal (`Hash[String, Set[String]]`) is a claim this seam may not grow, and is left alone; a value-pinned one
112
+ # (`Hash["a" | "b", 1]`) holds a first-store answer the unknown store falsifies, and takes the gradual arm, as a
113
+ # block's unknown-store binding does.
114
+ def callee_block_floor(type)
115
+ widened = widen_store(type, :[]=, unknown_arguments(2)) || type
116
+ widened = callee_set_floor(widened) || widened
117
+ dynamic_elements(value_pinned_collection?(widened) ? gradual_content(widened) : widened)
118
+ end
119
+
120
+ # Issue #2296 — `type` after a callee that grows the parameter through a Set-only adder (`s.add(x)`,
121
+ # `s.merge(xs)`): a Set carrier takes one unknown store, and anything else, a Hash under `merge` included, is
122
+ # returned as nil (unchanged).
123
+ def callee_set_floor(type)
124
+ widened = widen_store(type, :add, unknown_arguments(1))
125
+ widened && dynamic_elements(widened)
126
+ end
127
+
128
+ # Issue #2647 — `type` with each container element the unknown store widened (`Dynamic[top] | String`, which an
129
+ # element read back out fails a literal-typed parameter with, although the callee stored a value the call never
130
+ # shows) made a gradual `Dynamic[String]`: the floor may forget a fact, never claim the nominal class. An element
131
+ # that carries no `Dynamic[top]` arm (a class-level `Array[Integer]`, which the floor leaves alone) is returned as
132
+ # it is. Scoped to the callee floor's two entry points, so a local mutation's unknown-store widening keeps its
133
+ # answer.
134
+ def dynamic_elements(type)
135
+ return type unless needs_dynamic?(type)
136
+
137
+ case type
138
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| dynamic_elements(member) })
139
+ when Type::Difference then Type::Combinator.difference(dynamic_elements(type.base), type.removed)
140
+ when Type::Nominal
141
+ return type unless CONTAINER_CLASSES.include?(type.class_name) && !type.type_args.empty?
142
+
143
+ Type::Combinator.nominal_of(type.class_name, type_args: type.type_args.map { |arg| dynamic_element(arg) })
144
+ else type
145
+ end
146
+ end
147
+
148
+ # True when {.dynamic_elements} would change `type`: a container element with a `Dynamic[top]` member and a class
149
+ # member (neither a literal nor `Dynamic[top]`). Reads with `any?` only, so the common unchanged type is not
150
+ # rebuilt.
151
+ def needs_dynamic?(type)
152
+ case type
153
+ when Type::Union then type.members.any? { |member| needs_dynamic?(member) }
154
+ when Type::Difference then needs_dynamic?(type.base)
155
+ when Type::Nominal
156
+ CONTAINER_CLASSES.include?(type.class_name) && type.type_args.any? { |arg| needs_dynamic_element?(arg) }
157
+ else false
158
+ end
159
+ end
160
+
161
+ def needs_dynamic_element?(arg)
162
+ return false unless arg.is_a?(Type::Union)
163
+
164
+ arg.members.any? { |member| gradual_top?(member) } &&
165
+ arg.members.any? { |member| !member.is_a?(Type::Constant) && !gradual_top?(member) }
166
+ end
167
+
168
+ def gradual_top?(member) = member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Top)
169
+
170
+ # `Dynamic[top] | A | B` as `Dynamic[A | B]` for the class members; a literal member (a value-pinned key) and any
171
+ # other element stay as they are.
172
+ def dynamic_element(arg)
173
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
174
+ gradual, known = members.partition { |member| gradual_top?(member) }
175
+ return arg if gradual.empty?
176
+
177
+ literals, classes = known.partition { |member| member.is_a?(Type::Constant) }
178
+ return arg if classes.empty?
179
+
180
+ Type::Combinator.union(*literals, Type::Combinator.dynamic(Type::Combinator.union(*classes)))
181
+ end
182
+
183
+ def unknown_arguments(count) = Array.new(count) { Type::Combinator.untyped }
184
+
185
+ # False only when {.callee_block_floor} and {.callee_set_floor} are sure to leave `type` as it is: a type that is
186
+ # no String, Array, Hash or Set carrier in any form. A cheap gate, so a call passing only such locals skips the
187
+ # census closures those floors read.
188
+ def callee_floorable?(type)
189
+ case type
190
+ when Type::Union then type.members.any? { |member| callee_floorable?(member) }
191
+ when Type::Tuple, Type::HashShape, Type::Difference, Type::Refined, Type::Intersection then true
192
+ when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
193
+ when Type::Nominal then FLOORABLE_NOMINALS.include?(type.class_name)
194
+ else false
195
+ end
196
+ end
197
+
198
+ FLOORABLE_NOMINALS = %w[Array Hash Set String].freeze
199
+ private_constant :FLOORABLE_NOMINALS
200
+
102
201
  # Every `Array` / `Hash` / `String` carrier `type` can be as its bare carrier: `Array[Dynamic[top]]`,
103
202
  # `Hash[Dynamic[top], Dynamic[top]]`, `String`. A literal, a nominal (a precise one included) and a refinement
104
203
  # over one ({.carrier_class}) all floor; so `non-empty-string`, which a callee can empty, reads `String`.
@@ -124,9 +124,8 @@ module Rigor
124
124
 
125
125
  # How a walk position reaches the exit scope: whether a local rebind there is threaded, whether an
126
126
  # instance variable's is, whether a class variable's or global's is, the offset below which a rebind
127
- # precedes a jump,
128
- # whether the position binds targets only (`:multi_assign` under a multi-assign, `:capture` under any
129
- # other binding construct, false otherwise), and the locals a nested block shadows.
127
+ # precedes a jump, whether the position binds targets only (`:multi_assign` under a multi-assign,
128
+ # `:capture` under any other binding construct, false otherwise), and the locals a nested block shadows.
130
129
  Position = Data.define(:local, :ivar, :other, :horizon, :binding, :shadowed)
131
130
  private_constant :Position
132
131
 
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "void_origin"
6
6
  require_relative "../reflection"
7
7
  require_relative "../source/node_children"
8
+ require_relative "../run_scoped_slots"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -62,7 +63,7 @@ module Rigor
62
63
  # graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
63
64
  # {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
64
65
  # return memo relies on.
65
- MEMO_KEY = :__rigor_void_tail_summary_memo__
66
+ MEMO_KEY = RunScopedSlots.register(:__rigor_void_tail_summary_memo__)
66
67
  private_constant :MEMO_KEY
67
68
 
68
69
  def initialize(scope)
@@ -13,9 +13,9 @@ module Rigor
13
13
  # with no `range`) stays supported — it is still legal under incremental sync and is what `didOpen` and a
14
14
  # client-side resync send.
15
15
  class BufferTable
16
- # @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
17
- # @!attribute bytes [String] the current full text of the buffer.
18
- # @!attribute version [Integer] the monotonically increasing LSP version number.
16
+ # @!attribute uri — the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
17
+ # @!attribute bytes — the current full text of the buffer.
18
+ # @!attribute version — the monotonically increasing LSP version number.
19
19
  Entry = Data.define(:uri, :bytes, :version)
20
20
 
21
21
  def initialize
@@ -98,6 +98,12 @@ module Rigor
98
98
  @dirty.key?(uri)
99
99
  end
100
100
 
101
+ # @return true when `uri` is open, its bytes are the ones on disk, and the table holds them faithfully: the
102
+ # buffers a whole-project round analysed as they are.
103
+ def on_disk?(uri)
104
+ open?(uri) && !dirty?(uri) && !desynchronized?(uri)
105
+ end
106
+
101
107
  def [](uri)
102
108
  @entries[uri]
103
109
  end