rigortype 0.4.6 → 0.4.8

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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -17,8 +17,8 @@ module Rigor
17
17
  #
18
18
  # assert_kind_of(T, x) → narrow x to T
19
19
  # assert_instance_of(T, x) → narrow x to T
20
- # assert_nil(x) → narrow x to Constant<nil>
21
- # assert_equal(literal, x) → meet x with Constant<literal> (see "Equality is not identity")
20
+ # assert_nil(x) → narrow x to nil
21
+ # assert_equal(literal, x) → meet x with the literal (see "Equality is not identity")
22
22
  # assert_match(regex, x) → narrow x to String
23
23
  #
24
24
  # ### (2) Minitest / Test::Unit `refute_*` / `assert_not_*` (negative) ###
@@ -26,7 +26,7 @@ module Rigor
26
26
  # refute_kind_of(T, x) → narrow x AWAY from T
27
27
  # refute_instance_of(T, x) → narrow x AWAY from T
28
28
  # refute_nil(x) → narrow x AWAY from nil
29
- # refute_equal(literal, x) → negative Constant<literal> fact (the engine keeps x unchanged)
29
+ # refute_equal(literal, x) → negative fact on the literal (the engine keeps x unchanged)
30
30
  #
31
31
  # Test::Unit's `assert_not_nil(x)` / `assert_not_equal(...)` / `assert_not_kind_of(...)` /
32
32
  # `assert_not_instance_of(...)` share the recognizer with `refute_*` (they're aliases).
@@ -35,8 +35,8 @@ module Rigor
35
35
  #
36
36
  # _(x).must_be_kind_of(T) → narrow x to T
37
37
  # _(x).must_be_instance_of(T) → narrow x to T
38
- # _(x).must_be_nil → narrow x to Constant<nil>
39
- # _(x).must_equal(literal) → meet x with Constant<literal> (see "Equality is not identity")
38
+ # _(x).must_be_nil → narrow x to nil
39
+ # _(x).must_equal(literal) → meet x with the literal (see "Equality is not identity")
40
40
  # _(x).must_match(regex) → narrow x to String
41
41
  #
42
42
  # The legacy bare `x.must_be_kind_of(T)` form (Minitest < 6.0 monkey-patched onto Object) is
@@ -48,7 +48,7 @@ module Rigor
48
48
  #
49
49
  # _(x).wont_be_kind_of(T) → narrow x AWAY from T
50
50
  # _(x).wont_be_nil → narrow x AWAY from nil
51
- # _(x).wont_equal(literal) → negative Constant<literal> fact (the engine keeps x unchanged)
51
+ # _(x).wont_equal(literal) → negative fact on the literal (the engine keeps x unchanged)
52
52
  #
53
53
  # ## Equality is not identity
54
54
  #
@@ -10,7 +10,8 @@ module Rigor
10
10
  module Plugin
11
11
  class Pundit < Rigor::Plugin::Base
12
12
  # Walks the configured policy-search paths via the plugin's `IoBoundary`, parses each `.rb` file with
13
- # Prism, and collects classes whose immediate superclass is one of the configured base classes.
13
+ # Prism, and collects classes whose immediate superclass is one of the configured base classes and
14
+ # whose name ends in `Policy`.
14
15
  #
15
16
  # For each discovered policy class, the discoverer collects every instance-side `def name?` predicate
16
17
  # method. Non-predicate methods (`initialize`, `resolve`, helper methods) are ignored — Pundit's
@@ -192,15 +192,10 @@ module Rigor
192
192
  first.unescaped
193
193
  end
194
194
 
195
- # Pulls the interpolation hash from the call's arguments. The trailing `Hash` argument (or
195
+ # Pulls the interpolation hash from the call's arguments. The trailing hash argument (or
196
196
  # `Prism::KeywordHashNode`) carries both reserved I18n options (`default:`, `scope:`, …) and
197
- # interpolation variables. Returns:
198
- # {
199
- # :has_default => bool,
200
- # :all_keys => Set<Symbol> (every assoc key in the hash),
201
- # :non_reserved => Set<Symbol> (keys NOT in RESERVED_OPTION_KEYS),
202
- # :hash_node => Prism::Node (or nil)
203
- # }
197
+ # interpolation variables. Returns `:has_default`, `:all_keys` (every assoc key), `:non_reserved`
198
+ # (keys outside RESERVED_OPTION_KEYS) and `:hash_node` (nil when there is no hash argument).
204
199
  #
205
200
  # Note: a reserved option key (e.g. `count:`) can also serve as an interpolation value when the
206
201
  # locale's leaf string has `%{count}`. The analyzer therefore checks the missing-placeholder set
@@ -11,9 +11,8 @@ module Rigor
11
11
  # placeholder names. The actual translated values are not retained — the analyzer doesn't need them
12
12
  # and keeping them would bloat the cache slice.
13
13
  class LocaleIndex
14
- # `placeholders` is a Hash: `locale_name => Set<String>`. `array_value` is true when at least one
15
- # locale's leaf is an Array (used by `l(time, format:)` and similar). `value_kinds` is a Hash:
16
- # `locale_name => Symbol` (`:string` / `:array` / `:hash`).
14
+ # `placeholders` maps each locale name to the `%{var}` placeholder names of its leaf. `value_kinds`
15
+ # maps each locale name to the leaf's kind (`:string` / `:array` / `:hash` / `:scalar`).
17
16
  Entry = Data.define(:dotted_key, :placeholders, :value_kinds) do
18
17
  def locales
19
18
  placeholders.keys
@@ -36,8 +36,8 @@ module Rigor
36
36
  end
37
37
 
38
38
  def load
39
- per_key = {} # dotted_key => { locale => Set<String> }
40
- per_key_kinds = {} # dotted_key => { locale => :string|:array|:hash }
39
+ per_key = {} # dotted_key => { locale => placeholder names }
40
+ per_key_kinds = {} # dotted_key => { locale => leaf kind }
41
41
  locales = Set.new
42
42
 
43
43
  locale_files.each do |path|
@@ -98,12 +98,9 @@ module Rigor
98
98
  # leaves are *not* recorded as entries themselves — only their descendants — but every leaf scalar
99
99
  # / array IS recorded.
100
100
  #
101
- # `breadcrumbs` is a single mutable stack reused across the whole walk (push before recursing, pop
102
- # after): for the 530-file / 14 MB Mastodon locale corpus the old
103
- # `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape allocated a fresh breadcrumb Array at
104
- # every node plus an intermediate result Array at every level — millions of short-lived objects and
105
- # the run's top allocation site. The dotted key is still materialised once per leaf (it has to
106
- # be); everything else is now allocation-free traversal.
101
+ # `breadcrumbs` is one mutable stack reused across the walk (push before recursing, pop after). A
102
+ # per-node Array allocation was the run's top allocation site on the 530-file Mastodon corpus; only
103
+ # the dotted key is materialised, once per leaf.
107
104
  def each_flattened(node, breadcrumbs, &)
108
105
  if node.is_a?(Hash)
109
106
  node.each do |k, v|
@@ -65,7 +65,7 @@ module Rigor
65
65
  # implicit `*_path` / `*_url` helper call, is shadowed by a same-file binding, or is in a
66
66
  # suppressed directory. ADR-37: the engine owns the walk; the same-file `shadowing` set is built
67
67
  # once as the node-rule file context.
68
- def violations_for(call_node:, helper_table:, shadowing:, path:)
68
+ def violations_for(call_node:, helper_table:, shadowing:, path:, scope: nil, context: nil)
69
69
  return [] unless call_node.is_a?(Prism::CallNode) && implicit_helper_call?(call_node)
70
70
 
71
71
  name = call_node.name.to_s
@@ -88,11 +88,209 @@ module Rigor
88
88
  # Recognised custom / dynamic helper (no arity to check), or a directory where helpers aren't
89
89
  # customarily resolved.
90
90
  []
91
+ elsif self_resolves_method?(name, scope, context)
92
+ []
91
93
  else
92
94
  [unknown_helper_violation(name, helper_table)]
93
95
  end
94
96
  end
95
97
 
98
+ # True when `name` is already a method on the call's `self`, asked of the engine rather than a
99
+ # plugin-side name list: the project's discovered methods (`def`, `attr_*`, the singleton side) through
100
+ # the project's own ancestry, then the RBS ancestry. A truncated walk (`maybe`) declines too — a definer
101
+ # past the budget cannot be ruled out, and a false positive outranks a missed typo.
102
+ #
103
+ # `self` is read from the node's lexical ancestors (`NodeContext#ancestors`), because the scope a
104
+ # `node_rule` receives is the file's, not the call site's. {#call_site} names the enclosing class or
105
+ # module and the kinds `self` may be; where the kind is not decidable (a block in a class body, a
106
+ # `def obj.m`) both are asked, so a definer on either side declines and a typo on neither still reports.
107
+ def self_resolves_method?(name, scope, context)
108
+ return false if scope.nil? || context.nil?
109
+
110
+ site = call_site(context.ancestors)
111
+ return false if site.nil?
112
+ # An owner no constant spelling names (`class foo::Bar`) cannot be asked about.
113
+ return true if site.names.empty?
114
+ # A module is mixed into classes that cannot be seen from where it is defined (a controller concern).
115
+ return true if site.module_owner && FRAMEWORK_METHODS.key?(name)
116
+
117
+ site.names.product(site.kinds).any? { |class_name, kind| resolves_on?(class_name, name, kind, scope) }
118
+ end
119
+
120
+ def resolves_on?(class_name, name, kind, scope)
121
+ return true unless Rigor::Reflection.project_method_defined(class_name, name, kind: kind, source: :either,
122
+ scope: scope).no?
123
+ return true unless Rigor::Reflection.rbs_method_defined(class_name, name, kind: kind, scope: scope).no?
124
+
125
+ bases = FRAMEWORK_METHODS[name]
126
+ !bases.nil? && !Rigor::Reflection.ancestry_reaches(class_name, bases, kind: kind, scope: scope).no?
127
+ end
128
+
129
+ # Rails' own `*_path` / `*_url` methods on a controller or mailer, read from the Rails source (rails/rails
130
+ # main, 1105756f42), each with the framework classes a project class names directly as its superclass. A
131
+ # project without that framework's RBS cannot see these through the ancestry, so a class whose ancestry
132
+ # reaches one of the listed bases declines for that name; a module declines for every name, being mixed
133
+ # into classes that cannot be seen.
134
+ #
135
+ # - `controller_path`: AbstractController::Base, actionpack/lib/abstract_controller/base.rb:157 (instance),
136
+ # :117 (class); every framework base below inherits it (metal.rb:185, base.rb:206, api.rb:91,
137
+ # actionmailer/lib/action_mailer/base.rb:479).
138
+ # - `append_view_path` / `prepend_view_path`: `ActionView::ViewPaths` (actionview/lib/action_view/
139
+ # view_paths.rb:113, :124 instance, :46, :57 class), which `AbstractController::Rendering` includes
140
+ # (abstract_controller/rendering.rb:18); `ActionController::Base` (base.rb:270), `::API` (api.rb:115) and
141
+ # `ActionMailer::Base` (action_mailer/base.rb:489) include that.
142
+ # - `helpers_path`: a class attribute of `ActionController::Helpers` (metal/helpers.rb:68), included by
143
+ # `ActionController::Base` (base.rb:273).
144
+ # - `edit_` / `new_` `polymorphic_path` / `polymorphic_url`: actionpack/lib/action_dispatch/routing/
145
+ # polymorphic_routes.rb:163-166, through `UrlFor` in `ActionController::Base` (base.rb:274) and `::API`
146
+ # (api.rb:117), and in `ActionMailer::Base`, which actionmailer/lib/action_mailer/railtie.rb:40
147
+ # includes `AbstractController::UrlFor` into, whose `ActionDispatch::Routing::UrlFor` includes
148
+ # `PolymorphicRoutes` (action_dispatch/routing/url_for.rb:92). (`polymorphic_path` / `polymorphic_url` are in {BUILTIN_PASSTHROUGH}.)
149
+ FRAMEWORK_CONTROLLERS = %w[
150
+ AbstractController::Base ActionController::Metal ActionController::Base ActionController::API
151
+ ActionMailer::Base
152
+ ].freeze
153
+ VIEW_PATH_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
154
+ URL_FOR_BASES = %w[ActionController::Base ActionController::API ActionMailer::Base].freeze
155
+ FRAMEWORK_METHODS = {
156
+ "controller_path" => FRAMEWORK_CONTROLLERS,
157
+ "append_view_path" => VIEW_PATH_BASES,
158
+ "prepend_view_path" => VIEW_PATH_BASES,
159
+ "helpers_path" => %w[ActionController::Base].freeze,
160
+ "edit_polymorphic_path" => URL_FOR_BASES,
161
+ "edit_polymorphic_url" => URL_FOR_BASES,
162
+ "new_polymorphic_path" => URL_FOR_BASES,
163
+ "new_polymorphic_url" => URL_FOR_BASES
164
+ }.freeze
165
+
166
+ # What a call site's lexical position says about `self`: the names the enclosing class or module can be
167
+ # known by, whether the owner is a module, and the kinds (`:instance`, `:singleton`) `self` may be.
168
+ CallSite = Struct.new(:names, :module_owner, :kinds, keyword_init: true)
169
+
170
+ # The {CallSite} of a node under `ancestors` (outermost first), or nil when it is in no class or module,
171
+ # which leaves the route-table verdict as it was.
172
+ def call_site(ancestors)
173
+ owner_index = ancestors.rindex { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
174
+ member_site = member_class_site(ancestors, owner_index)
175
+ return member_site if member_site
176
+ return nil if owner_index.nil?
177
+
178
+ CallSite.new(names: owner_names(ancestors, owner_index),
179
+ module_owner: ancestors[owner_index].is_a?(Prism::ModuleNode),
180
+ kinds: self_kinds(ancestors[(owner_index + 1)..]))
181
+ end
182
+
183
+ BOTH_KINDS = %i[instance singleton].freeze
184
+ MEMBER_CLASS_CONSTRUCTORS = { Struct: :new, Data: :define }.freeze
185
+
186
+ # The call site of a node inside the block of `Struct.new(...) { }` / `Data.define(...) { }`, which is the
187
+ # body of a class the constructor creates: `self` is that class, not the lexical `class` around it (or
188
+ # none). A plain constant write (`Point = …`, `Outer::Point = …`, nested under the lexical owner) names it,
189
+ # and the engine knows its members. Everything else declines (`names` empty): an unassigned block, `P ||= …`,
190
+ # `… end.freeze`, a dynamic path. nil when the node is not in such a block below its lexical owner.
191
+ def member_class_site(ancestors, owner_index)
192
+ index = ancestors.rindex { |n| member_class_call?(n) && ancestors[ancestors.index(n) + 1].equal?(n.block) }
193
+ return nil if index.nil? || (owner_index && index < owner_index)
194
+
195
+ kinds = self_kinds(ancestors[(index + 1)..])
196
+ names = index.positive? ? written_class_names(ancestors, owner_index, ancestors[index - 1], ancestors[index]) : []
197
+ names = struct_class_names(ancestors[index]) if names.empty?
198
+ CallSite.new(names: names, module_owner: false, kinds: kinds)
199
+ end
200
+
201
+ # Issue #2968 — `Struct.new("Named", :a) { }` names its class `Struct::Named` without a constant write.
202
+ def struct_class_names(call)
203
+ first = call.arguments&.arguments&.first
204
+ return [] unless call.name == :new && first.is_a?(Prism::StringNode)
205
+
206
+ name = first.unescaped
207
+ name.match?(/\A[A-Z][A-Za-z0-9_]*\z/) ? ["Struct::#{name}"] : []
208
+ end
209
+
210
+ def written_class_names(ancestors, owner_index, written, call)
211
+ local =
212
+ case written
213
+ when Prism::ConstantWriteNode then written.value.equal?(call) ? written.name.to_s : nil
214
+ when Prism::ConstantPathWriteNode then written.value.equal?(call) ? declared_name(written.target) : nil
215
+ end
216
+ return [] if local.nil?
217
+ return [local.delete_prefix("::")] if local.start_with?("::")
218
+
219
+ prefixes = owner_index ? owner_names(ancestors, owner_index) : [""]
220
+ nested = prefixes.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
221
+ # `Outer::R = …` resolves `Outer` lexically or at the root, as a compact `class Foo::Bar` does.
222
+ written.is_a?(Prism::ConstantPathWriteNode) ? (nested + [local]).uniq : nested
223
+ end
224
+
225
+ def member_class_call?(node)
226
+ return false unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
227
+
228
+ receiver = node.receiver
229
+ receiver_name =
230
+ case receiver
231
+ when Prism::ConstantReadNode then receiver.name
232
+ when Prism::ConstantPathNode then receiver.parent.nil? ? receiver.name : nil
233
+ end
234
+ !receiver_name.nil? && MEMBER_CLASS_CONSTRUCTORS[receiver_name] == node.name
235
+ end
236
+
237
+ # The kinds `self` may be for a node under the nodes between its owner and itself. The nearest `def`
238
+ # decides (blocks inside a method keep its `self`); with no `def`, a bare class body is the class object
239
+ # itself, while a block or lambda there may run on another object (`before_action { }`, `included do`,
240
+ # `define_method`, `Class.new`), so both kinds are asked.
241
+ def self_kinds(inner)
242
+ def_index = inner.rindex { |n| n.is_a?(Prism::DefNode) }
243
+ if def_index
244
+ node = inner[def_index]
245
+ return BOTH_KINDS if node.receiver && !node.receiver.is_a?(Prism::SelfNode)
246
+ return [:singleton] if node.receiver
247
+
248
+ return inner[0...def_index].any?(Prism::SingletonClassNode) ? [:singleton] : [:instance]
249
+ end
250
+ return BOTH_KINDS if inner.any? { |n| block_scope?(n) }
251
+ if inner.any? { |n| n.is_a?(Prism::SingletonClassNode) && !n.expression.is_a?(Prism::SelfNode) }
252
+ return BOTH_KINDS
253
+ end
254
+
255
+ [:singleton]
256
+ end
257
+
258
+ def block_scope?(node)
259
+ node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
260
+ end
261
+
262
+ # The names the enclosing class or module can be known by, `[]` when a declaration's name is dynamic. A
263
+ # bare `class Foo` is exactly `Enclosing::Foo`. A compact `class Foo::Bar` may resolve `Foo` lexically or
264
+ # at the root, so both spellings are tried. A rooted declaration (`class ::Foo`) names the root constant
265
+ # alone.
266
+ def owner_names(ancestors, owner_index)
267
+ chain = ancestors[0..owner_index].select { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
268
+ names = [""]
269
+ chain.each do |decl|
270
+ local = declared_name(decl.constant_path)
271
+ return [] if local.nil?
272
+
273
+ names = if local.start_with?("::")
274
+ [local.delete_prefix("::")]
275
+ else
276
+ nested = names.map { |prefix| prefix.empty? ? local : "#{prefix}::#{local}" }
277
+ decl.constant_path.is_a?(Prism::ConstantPathNode) ? (nested + [local]).uniq : nested
278
+ end
279
+ end
280
+ names
281
+ end
282
+
283
+ def declared_name(node)
284
+ case node
285
+ when Prism::ConstantReadNode then node.name.to_s
286
+ when Prism::ConstantPathNode
287
+ return "::#{node.name}" if node.parent.nil?
288
+
289
+ parent = declared_name(node.parent)
290
+ parent && "#{parent}::#{node.name}"
291
+ end
292
+ end
293
+
96
294
  # Walks the AST once and returns the Set of names that shadow a route helper for this file. Includes:
97
295
  #
98
296
  # - `def name` declarations (any level — the per-method-scope visibility model Ruby uses means a
@@ -124,7 +322,6 @@ module Rigor
124
322
  if name
125
323
  names << name
126
324
  elsif call_node.name == :subject && call_node.arguments.nil?
127
- # Bare `subject { ... }` defines `:subject` itself.
128
325
  names << "subject"
129
326
  end
130
327
  end
@@ -201,8 +398,7 @@ module Rigor
201
398
  end
202
399
 
203
400
  def unknown_helper_violation(name, helper_table)
204
- # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester replaces the hand-rolled
205
- # Levenshtein this module used to carry.
401
+ # ADR-39 / boilerplate 0c — the shared DidYouMean-backed suggester.
206
402
  suggestion = Rigor::Plugin::Base.suggest(name, helper_table.names)
207
403
  message = "no route helper `#{name}`"
208
404
  message += " (did you mean `#{suggestion}`?)" if suggestion
@@ -16,7 +16,7 @@ module Rigor
16
16
  #
17
17
  # Scope:
18
18
  #
19
- # - Recognises the standard six controllers (`:sessions`, `:passwords`, `:confirmations`, `:unlocks`,
19
+ # - Recognises the standard controllers (`:sessions`, `:passwords`, `:confirmations`, `:unlocks`,
20
20
  # `:registrations`, `:omniauth_callbacks`).
21
21
  # - Honours `skip: [...]` to suppress controllers the project disables.
22
22
  # - Honours `path: "..."` to remap the URL path (does NOT remap the helper-name prefix — Devise uses
@@ -162,8 +162,7 @@ module Rigor
162
162
  OMNIAUTH_SUFFIXES
163
163
  end
164
164
 
165
- # ADR-39 — delegates to the shared inflector (real `ActiveSupport::Inflector` when available),
166
- # replacing the tiny AS-replica this module used to duplicate from `RoutesParser`.
165
+ # ADR-39 — delegates to the shared inflector (real `ActiveSupport::Inflector` when available).
167
166
  def singularize(word)
168
167
  Rigor::Plugin::Inflector.singularize(word)
169
168
  end
@@ -7,7 +7,7 @@ require "prism"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class RailsRoutes < Rigor::Plugin::Base
10
- # Walks `app/helpers/**/*.rb` (or any configured set of helper directories) and extracts every public
10
+ # Walks the configured `helper_paths:` directories (default `app/`) and extracts every public
11
11
  # module-level method name so the analyzer's `unknown-helper` rule does not false-fire on calls to
12
12
  # project-defined `*_path` / `*_url` helpers (the canonical Rails pattern: a `UrlHelper` module
13
13
  # exposing `full_asset_url`, `host_to_url`, `frontend_asset_url`, and so on).
@@ -409,14 +409,9 @@ module Rigor
409
409
  end
410
410
  end
411
411
 
412
- # ADR-39: inflection delegates to the shared `Rigor::Plugin::Inflector`, which calls the real
413
- # `ActiveSupport::Inflector` (the authority Rails itself uses to generate helper names) when
414
- # available, falling back to a built-in approximation otherwise. This replaces the hand-tuned
415
- # AS-replica these methods used to carry — every special case they accreted (`news` uncountable,
416
- # `media`→`medium`, `databases`→`database`, `custom_css` ss-preservation,
417
- # `async_refresh`→`async_refreshes`) was reverse-engineered `ActiveSupport::Inflector` behaviour,
418
- # so the real inflector produces the same result for those and the correct one for the words the
419
- # replica never covered.
412
+ # ADR-39: inflection delegates to the shared `Rigor::Plugin::Inflector`, which uses the real
413
+ # `ActiveSupport::Inflector` (the authority Rails uses to generate helper names) when available,
414
+ # and a built-in approximation otherwise.
420
415
  def singularize(word)
421
416
  Rigor::Plugin::Inflector.singularize(word)
422
417
  end
@@ -1286,7 +1281,7 @@ module Rigor
1286
1281
  return nil unless plural
1287
1282
 
1288
1283
  # Rails appends `_index_path` to the index helper name when the singular form of the resource
1289
- # matches the plural form AND the noun isn't in the canonical UNCOUNTABLE list. The collision
1284
+ # matches the plural form (uncountable nouns included). The collision
1290
1285
  # would otherwise put both the index (`:id`-less) and show (`:id`-bearing) helpers under the same
1291
1286
  # name, and Rails disambiguates by suffixing the index form. Mastodon's `resources
1292
1287
  # :reblogged_by, controller: :reblogged_by_accounts, only: :index` and similar rely on this —
@@ -1432,8 +1427,6 @@ module Rigor
1432
1427
  end
1433
1428
 
1434
1429
  # ADR-39 — delegates to the shared inflector (real `ActiveSupport::Inflector` when available).
1435
- # Previously a hand-tuned AS-replica kept in sync with `Context#singularize`; both now share the
1436
- # one authoritative implementation.
1437
1430
  def singularize_word(word)
1438
1431
  Rigor::Plugin::Inflector.singularize(word)
1439
1432
  end
@@ -14,7 +14,7 @@ module Rigor
14
14
  # rigor-rails-routes — validates Rails route-helper calls (`users_path`, `edit_user_path(@user)`, …)
15
15
  # against the project's `config/routes.rb`.
16
16
  #
17
- # Tier 1A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
+ # Tier 1A of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
18
18
  # Statically interprets the routes DSL via Prism — no `rails` runtime dependency. Recognised v0.1.0
19
19
  # surface:
20
20
  #
@@ -205,11 +205,12 @@ module Rigor
205
205
  Analyzer.collect_shadowing_names(root)
206
206
  end
207
207
 
208
- node_rule Prism::CallNode do |node, _scope, path, shadowing|
208
+ node_rule Prism::CallNode do |node, scope, path, shadowing, context|
209
209
  table = helper_table_or_nil
210
210
  next [] if table.nil? || table.empty?
211
211
 
212
- Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path).map do |violation|
212
+ Analyzer.violations_for(call_node: node, helper_table: table, shadowing: shadowing, path: path,
213
+ scope: scope, context: context).map do |violation|
213
214
  diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
214
215
  end
215
216
  end
@@ -35,7 +35,8 @@ module Rigor
35
35
  class RbsInline < Rigor::Plugin::Base
36
36
  # Synthesizer callable invoked once per project Ruby source file by `Environment.for_project` at
37
37
  # env-build time. Returns the synthesised RBS source as a String, or `nil` when the file contributes
38
- # nothing (no magic comment in the default mode, empty annotation set, parse error per WD6).
38
+ # nothing (no annotation in the default mode, no magic comment under `require_magic_comment: true`,
39
+ # or a parse error per WD6).
39
40
  class Synthesizer
40
41
  # An RDoc directive comment: `#:` followed by a bare word (RDoc allows a hyphen, as in `:call-seq:`)
41
42
  # and a closing colon. RDoc predates rbs-inline by about two decades and `#:<name>:` is
@@ -286,8 +286,8 @@ module Rigor
286
286
  # project class, which says nothing about who wrote `==`.
287
287
  def project_defines?(class_name, operator, scope)
288
288
  OPERATOR_OWNER_CHAINS.fetch([class_name, operator]).any? do |name|
289
- !scope.user_def_through_ancestors(name, operator).first.nil? ||
290
- scope.discovered_method_through_ancestors?(name, operator, :instance)
289
+ !Rigor::Reflection.project_method_defined(name, operator, kind: :instance, source: :either,
290
+ scope: scope).no?
291
291
  end
292
292
  end
293
293
 
@@ -358,7 +358,7 @@ module Rigor
358
358
  # signature, a core ancestor, or the project's source) is kept whole. `Object`, `BasicObject` and a module
359
359
  # String or Symbol includes otherwise read `String | Symbol`, as the String they most often are, so a String
360
360
  # method called after the matcher reports nothing. Any other module is UNDECIDED: a passing value may include
361
- # it without being a String. (`approval-queue.d/regexp-match-open-surface-subject.md`, shared with #2256.)
361
+ # it without being a String. (Decision shared with #2256.)
362
362
  def module_match_member(member, scope)
363
363
  return nil unless member.is_a?(Rigor::Type::Nominal) && member.type_args.empty?
364
364
 
@@ -390,8 +390,8 @@ module Rigor
390
390
  # `def`, an included module, or a `define_method`.
391
391
  def project_defines_on_chain?(class_name, method_name, scope)
392
392
  CORE_ANCESTOR_CHAINS.fetch(class_name, [class_name]).any? do |name|
393
- !scope.user_def_through_ancestors(name, method_name).first.nil? ||
394
- scope.discovered_method_through_ancestors?(name, method_name, :instance)
393
+ !Rigor::Reflection.project_method_defined(name, method_name, kind: :instance, source: :either,
394
+ scope: scope).no?
395
395
  end
396
396
  end
397
397
 
@@ -14,7 +14,7 @@ module Rigor
14
14
  # narrows `expect(x).to MATCHER` assertions downstream, and binds `let`/`subject` locals to their
15
15
  # inferred return types inside `it` bodies.
16
16
  #
17
- # Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
17
+ # Tier 3A of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
18
18
  # Ships four checks:
19
19
  #
20
20
  # 1. **Duplicate `let` / `subject` declarations** in the same scope (`warning`). RSpec's runtime lets
@@ -44,7 +44,7 @@ module Rigor
44
44
  # - **No shared-context resolution.** `include_context`, `shared_context`, and `it_behaves_like` are
45
45
  # recognised as scope-opening calls but their declarations are not pulled into the host scope.
46
46
  # - **Constant validation is not done here.** `RSpec.describe SomeClass do` does not validate
47
- # `SomeClass`; the engine's `inference.unresolved-constant` already catches that.
47
+ # `SomeClass`; the engine's constant-resolution diagnostics already catch that.
48
48
  class Rspec < Rigor::Plugin::Base
49
49
  manifest(
50
50
  id: "rspec",
@@ -30,7 +30,7 @@ module Rigor
30
30
  # - `route_to(...)` / `redirect_to(...)` — needs the routes table from `rigor-rails-routes`
31
31
  # (`:helper_table` cross-plugin fact). Future slice.
32
32
  # - `have_enqueued_job(JobClass)` / `have_enqueued_mail(MailerClass)` — class-existence check overlaps
33
- # with engine's `inference.unresolved-constant`; queued behind a decision on which surface owns the
33
+ # with the engine's constant-resolution diagnostics; queued behind a decision on which surface owns the
34
34
  # diagnostic.
35
35
  # - `have_received(:method)` — overlaps with engine's `call.undefined-method`; same coordination
36
36
  # question.
@@ -44,9 +44,8 @@ module Rigor
44
44
  description: "Validates rspec-rails behavioral matchers (have_http_status floor)."
45
45
  )
46
46
 
47
- # ADR-37 — the engine walks each file and hands us every CallNode; the analyzer is now pure per-node
48
- # logic. The diagnostic points at the matcher name (`message_loc`), reproducing the previous
49
- # `message_loc || location` positioning.
47
+ # ADR-37 — the engine walks each file and hands us every CallNode; the analyzer is pure per-node
48
+ # logic. The diagnostic points at the matcher name (`message_loc`).
50
49
  node_rule Prism::CallNode do |node, _scope, path|
51
50
  violation = HaveHttpStatusAnalyzer.violation_for(node)
52
51
  next [] unless violation
@@ -31,7 +31,7 @@ module Rigor
31
31
  # have_db_index(:col)
32
32
  #
33
33
  # All look up `:col` against the model's `columns:` (the published `:model_index` fact's flat
34
- # `Array<String>` per model — see `Analyzer#column_names`). Unknown columns fire `unknown-column`.
34
+ # name list per model — see `Analyzer#column_names`). Unknown columns fire `unknown-column`.
35
35
  #
36
36
  # ### Association matchers
37
37
  #
@@ -107,8 +107,7 @@ module Rigor
107
107
 
108
108
  # A direct matcher invocation is a `CallNode` whose `name` is in `MATCHER_TABLE` and whose first
109
109
  # argument is a `SymbolNode`. The chain shape (`should`, `expect(...).to`, `is_expected.to`) is
110
- # irrelevant — we always recurse to the inner matcher, so a diagnostic fires on the matcher
111
- # regardless of the surrounding chain.
110
+ # irrelevant: the engine visits the inner matcher call, so the diagnostic fires on it regardless.
112
111
  def matcher_invocation?(node)
113
112
  node.is_a?(Prism::CallNode) && MATCHER_TABLE.key?(node.name) && symbol_first_arg?(node)
114
113
  end
@@ -172,11 +171,10 @@ module Rigor
172
171
  end
173
172
 
174
173
  # `entry` is the published fact's per-model Hash: `{ table:, columns:, associations:, ... }`.
175
- # `columns:` is a flat `Array<String>` (`Activerecord#index_to_published_hash`); `associations:` is
176
- # `Array<Hash>` with String `:name`, Symbol `:kind` (`:singular` | `:collection`), `:nullable`,
177
- # `:polymorphic`, and a `:target` that is a String except on polymorphic rows, where it is nil —
178
- # the row shape `ModelDiscoverer#build_association_row` publishes. Only `:name` and `:kind` are
179
- # read here.
174
+ # `columns:` is a flat list of column names (`Activerecord#index_to_published_hash`); `associations:`
175
+ # is a list of rows with `:name`, `:kind` (`:singular` | `:collection`), `:nullable`, `:polymorphic`,
176
+ # and a `:target` that is nil on polymorphic rows — the row shape `ModelDiscoverer#build_association_row`
177
+ # publishes. Only `:name` and `:kind` are read here.
180
178
  def column_names(entry) = entry[:columns] || []
181
179
  def association_rows(entry) = entry[:associations] || []
182
180
  def find_association(entry, name) = association_rows(entry).find { |a| a[:name] == name.to_s }
@@ -98,9 +98,10 @@ module Rigor
98
98
  # Cached separately from `:worker_index` because the two invalidate on different files: editing a
99
99
  # schedule changes which workers are reached without touching `app/workers` at all. The `watch:` roots
100
100
  # the glob at the working directory so that CREATING a schedule file — not just editing one — is seen. Each
101
- # configured path is escaped to a literal name, so `sidekiq[1].yml` is watched as that file.
101
+ # configured path is escaped to a literal name, so `sidekiq[1].yml` is watched as that file, and an
102
+ # absolute path is watched under its own directory (`RootedGlob.split_file`, #2222).
102
103
  producer :scheduled_workers,
103
- watch: -> { [[".", *@schedule_paths.map { |path| Rigor::RootedGlob.literal(path) }]] } do |_params|
104
+ watch: -> { @schedule_paths.map { |path| Rigor::RootedGlob.split_file(path) } } do |_params|
104
105
  ScheduleScan.new(
105
106
  io_boundary: io_boundary,
106
107
  schedule_paths: @schedule_paths
@@ -31,11 +31,10 @@ module Rigor
31
31
  # behaviour: it always raises). This lets the engine's existing flow analysis treat code after
32
32
  # `T.absurd` as unreachable, matching what users of Sorbet expect.
33
33
  # - When the branch is REACHABLE (the discriminant's type isn't `bot`), the recogniser also records
34
- # the call node in a per-plugin set. The plugin's `diagnostics_for_file` later walks the AST for
35
- # `T.absurd` calls and emits a `plugin.sorbet.absurd-reachable` warning at every call_node whose
36
- # object identity matches the recorded set. We rely on the runner only parsing each file once per
37
- # run, so the same Prism node object is seen in both the `dynamic_return` rule and
38
- # `diagnostics_for_file`.
34
+ # the call node in a per-plugin set. The plugin's node rule later emits a
35
+ # `plugin.sorbet.absurd-reachable` warning for each recorded call node, matched by object identity.
36
+ # We rely on the runner only parsing each file once per run, so the same Prism node object is seen
37
+ # in both the `dynamic_return` rule and the node rule.
39
38
  module AbsurdRecognizer
40
39
  # @return true when `call_node` is `T.absurd(x)`.
41
40
  def self.absurd_call?(call_node)
@@ -21,7 +21,7 @@ module Rigor
21
21
  # | `T.unsafe(x)` | return type ← `Dynamic[top]` |
22
22
  # | `T.reveal_type(expr)` | return type ← `inferred(expr)` (passes through) |
23
23
  # | `T.assert_type!(expr, T)` | return type ← translated `T` + static subtype check |
24
- # | `T.bind(self, T)` | return type ← `Constant[nil]` + post_return_fact narrowing self to translated `T` |
24
+ # | `T.bind(self, T)` | return type ← `Constant[nil]` + `self` narrowing fact |
25
25
  #
26
26
  # The Sorbet runtime's `T.let` / `T.cast` actually return the inner expression unchanged at runtime;
27
27
  # their job is purely to *assert* a static type. From Rigor's static perspective the simplest
@@ -36,7 +36,7 @@ module Rigor
36
36
  # `T.reveal_type` is "diagnostic-only" in Sorbet: it passes the value through unchanged at runtime
37
37
  # AND surfaces the inferred static type as a build-time message. The recogniser contributes the
38
38
  # inferred type (so chained call sites still resolve as if the `T.reveal_type` wrapper weren't
39
- # there); the plugin's `diagnostics_for_file` hook surfaces the `plugin.sorbet.reveal-type` `:info`
39
+ # there); the plugin's node rule surfaces the `plugin.sorbet.reveal-type` `:info`
40
40
  # message for human consumption.
41
41
  #
42
42
  # `T.bind(self, T)` is recognised as block-scope self narrowing. The recogniser returns a
@@ -113,9 +113,9 @@ module Rigor
113
113
 
114
114
  # `T.assert_type!(expr, T)` shares the typed-assertion contribution shape with `T.cast` (return is
115
115
  # the asserted type), so the recogniser delegates the return-type half through
116
- # `resolve_typed_assertion`. The static subtype check that distinguishes `assert_type!` from
117
- # `cast` lives in the plugin's `diagnostics_for_file` hook (mirroring the absurd-recognizer
118
- # pattern: record the call here, emit the diagnostic from the per-file walker).
116
+ # `resolve_typed_assertion`. The static subtype check that distinguishes `assert_type!` from `cast`
117
+ # is run by the plugin's `record_assert_type_check`, which records a mismatch for the node rule to
118
+ # emit (the absurd-recognizer pattern).
119
119
  def assert_type_check(call_node, scope)
120
120
  return nil if scope.nil?
121
121
 
@@ -135,8 +135,7 @@ module Rigor
135
135
  # `T.reveal_type(expr)` returns `expr` unchanged at runtime; the Sorbet-side semantics is "make
136
136
  # the inferred static type visible to the user." The contribution mirrors `T.must` minus the
137
137
  # nil-stripping: the call's return type is the inner expression's inferred type. The companion
138
- # diagnostic is emitted by the plugin's `diagnostics_for_file` hook; this method handles the
139
- # contribution only.
138
+ # diagnostic is emitted by a node rule; this method handles the contribution only.
140
139
  def resolve_reveal_type(call_node, scope)
141
140
  inner = nth_argument(call_node, 0)
142
141
  return Rigor::Type::Combinator.untyped if inner.nil? || scope.nil?