rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -77,7 +77,9 @@ a diagnostic another layer has already suppressed.
77
77
  | `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
78
78
  | `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
79
79
 
80
- `generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`.
80
+ `generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`. They analyse the configured `paths:` only: a positional path argument is rejected with a usage error (exit `64`), since a baseline over a subset of files would silently drop the entries of every other file. The other three subcommands reject a positional path the same way.
81
+
82
+ The summary counts only the diagnostics the written buckets cover. A diagnostic with no rule — a **parse error** in a file Rigor could not parse, a missing `paths:` entry, or an internal error — or no path cannot be keyed, so it never enters the baseline. The command prints a second line saying how many, why, and which files. The exit status stays 0, but the next `rigor check` with `baseline:` wired still reports them and exits non-zero until you fix those files, exclude them, or remove a bad `paths:` entry.
81
83
 
82
84
  `generate` and `regenerate` take `--match-mode=rule` (the
83
85
  default — one bucket per file × rule) or `--match-mode=message`
@@ -103,11 +105,11 @@ snapshot in the repository and compares against it explicitly:
103
105
 
104
106
  ```sh
105
107
  # Once: capture the current diagnostic surface.
106
- rigor check --format=json > rigor.baseline.json
107
- git add rigor.baseline.json
108
+ rigor check --format=json > rigor-check.json
109
+ git add rigor-check.json
108
110
 
109
111
  # Per PR: compare against the committed snapshot.
110
- rigor diff rigor.baseline.json
112
+ rigor diff rigor-check.json
111
113
  ```
112
114
 
113
115
  [`rigor diff`](02-cli-reference.md#rigor-diff) prints a `+ NEW`
@@ -25,6 +25,7 @@ design + capability matrix lives in
25
25
  | `textDocument/documentSymbol` | Outline tree from Prism AST: `class` / `module` / `def` with nesting. |
26
26
  | `workspace/didChangeWatchedFiles` | Invalidates the per-session `Environment` + `Cache::Store` cache so saved files repropagate. |
27
27
  | `workspace/didChangeConfiguration` | Same — re-reads `.rigor.yml` / `Gemfile.lock` etc. |
28
+ | `window/showMessage` | One warning per [configuration warning](03-configuration.md) — the lines `rigor check` prints on STDERR, such as a key that has no effect or a `disable:` token that names no rule. Sent on `initialized`, and again after a save only when re-reading `.rigor.yml` changed the set of warnings. |
28
29
 
29
30
  ## Prerequisites
30
31
 
@@ -51,8 +52,10 @@ rigor lsp [--transport=stdio] [--log=PATH] [--config=PATH]
51
52
 
52
53
  - `--transport=stdio` — default; only value accepted in v1. TCP /
53
54
  Unix-socket transports are queued.
54
- - `--log=PATH` — wire log + server debug to a file. Without it,
55
- server-side logs go to stderr.
55
+ - `--log=PATH` — append the wire log (each message in and out, one
56
+ line each) and the server's debug output from the start of the loop to
57
+ a file. Output before the loop starts still goes to stderr. Without
58
+ it, no wire log is written and server-side logs go to stderr.
56
59
  - `--config=PATH` — mirrors `rigor check --config=PATH`. Without
57
60
  it, `Configuration.discover` walks `.rigor.yml` / `.rigor.dist.yml`
58
61
  from the project root.
@@ -194,6 +197,14 @@ For a legacy bundler-based install, swap the connection list to
194
197
  there, the LSP should too. If `rigor check` fails, fix that
195
198
  first.
196
199
 
200
+ **A rule still fires although `.rigor.yml` turns it off.**
201
+
202
+ - Look for a `rigor:` warning the server showed when it started (or
203
+ after the save that changed `.rigor.yml`): a misspelt key such as
204
+ `undefined-method: "off"` at the top level, or a `disable:` token
205
+ that names no rule, has no effect, and the server says so once.
206
+ The same warnings appear on STDERR from `rigor check`.
207
+
197
208
  **Completion popup is empty.**
198
209
 
199
210
  - Completion only fires on a node with a known type. Receivers
@@ -193,6 +193,28 @@ All messages except `notifications/*` require a response.
193
193
 
194
194
  ## Tool reference
195
195
 
196
+ Every tool that takes a `config` argument reports the configuration's
197
+ warnings — the ones `rigor check` prints on STDERR, such as a key that
198
+ has no effect or a `disable:` token that names no rule. `rigor_check`
199
+ carries them in its own document; every other such tool appends a
200
+ second `text` content block holding one JSON object, after the tool's
201
+ own output, which is left unchanged:
202
+
203
+ ```json
204
+ {"config_warnings": [{"kind": "unknown_key", "message": "`undefined-method` is not a recognized configuration key; it has no effect.", "key": "undefined-method", "suggestion": null}]}
205
+ ```
206
+
207
+ Each entry has the shape of `rigor check --format json`'s
208
+ `config_warnings` entries. There is no such block when the
209
+ configuration has no warnings, or when it fails to load (the tool
210
+ reports that error itself).
211
+
212
+ `rigor_type_of` and `rigor_explain` print JSON whose top-level shape changes
213
+ in the next minor release (`{ "schema_version": 1, "results" | "rules" |
214
+ "skip_reasons": [...] }`). Until then, a successful call carries a final
215
+ `text` content block, after any `config_warnings` block, with the one-line
216
+ notice the CLI prints on STDERR. The tool's own document is left unchanged.
217
+
196
218
  ### rigor_check
197
219
 
198
220
  Analyze one or more Ruby files or directories for type errors,
@@ -222,9 +244,16 @@ undefined methods, arity mismatches, and nil-receiver risks.
222
244
  }
223
245
  ```
224
246
 
247
+ When `.rigor.yml` has [configuration warnings](03-configuration.md)
248
+ (a key or a `disable:` token that has no effect), the document carries
249
+ them under `config_warnings`, as `rigor check --format json` does.
250
+
225
251
  `isError` is `false` when the run completes normally, even if
226
- diagnostics were found. `isError: true` signals a configuration or
227
- runtime failure (bad path, unreadable config, etc.).
252
+ diagnostics were found. `isError: true` signals a usage or runtime
253
+ failure: an unreadable `config`, or a `rigor_coverage` call whose
254
+ `paths` leave nothing to measure. A missing path given to `rigor_check`
255
+ is not one of these: it is reported in the diagnostics, and the call
256
+ returns `isError: false`.
228
257
 
229
258
  **Typical agent use:** call `rigor_check` on the files an agent is
230
259
  about to edit, record the baseline, apply the edit, call again, diff
@@ -484,6 +513,13 @@ as `Constant` / `Nominal` / shaped / refined (precise) versus
484
513
 
485
514
  **Returns:** JSON — the same as `rigor coverage --format json`.
486
515
 
516
+ A `paths` entry that is missing, or is not a Ruby file, is reported as
517
+ `rigor coverage` reports it. Beside files that are measured it is skipped. The result
518
+ then carries a second text block with the CLI's stderr, unfiltered: path
519
+ warnings such as `missing.rb:1:1: warning: no such file or directory (skipped)`,
520
+ and any other warnings. When no entry holds a Ruby file, the call is
521
+ `isError: true` and the block says so.
522
+
487
523
  ```json
488
524
  {
489
525
  "summary": {
@@ -190,9 +190,10 @@ unchanged tree, so it cannot see an edit that fails to re-check a
190
190
  file it affects. `--verify-incremental-edits=N` also edits `N`
191
191
  files one at a time, in memory and not on disk, and compares each
192
192
  warm recheck with a full analysis of the edited tree. Each file is
193
- edited twice: once emptied, and once reduced to its `class` /
194
- `module` declarations with every body dropped. It replays only
195
- those two removals, never an addition or any other edit.
193
+ edited up to four times: once emptied, once reduced to its
194
+ `class` / `module` declarations with every body dropped, and up
195
+ to twice with one statement of a `class` / `module` body removed
196
+ and the rest kept. It replays only removals, never an addition.
196
197
 
197
198
  Rigor is also precise about *what* an edit changed. Re-checking a
198
199
  file that many others depend on — a base class, a widely-used
@@ -397,21 +397,29 @@ diff — not a policy scoped to `app/views/**/*`. See
397
397
  knows what `rigor-rails-routes` published, and `permit`
398
398
  validation only what `rigor-activerecord` published — enabling
399
399
  those producers widens what this plugin can check.
400
- - **`params[:key]` stays untyped.** Inside a controller, `params`
401
- types as `ActionController::Parameters`, and so does the result
402
- of every builder method that always returns one — `require`,
400
+ - **`params[:key]` is a gradual carrier.** Inside a controller,
401
+ `params` types as `ActionController::Parameters`, and so does the
402
+ result of every builder method that always returns one — `require`,
403
403
  `permit`, `permit!`, `expect`, `slice`, `slice!`, `except`,
404
404
  `without`, `extract!`, `merge`, `merge!`, `reverse_merge`,
405
405
  `reverse_merge!`, `with_defaults`, `with_defaults!`, `compact`,
406
406
  `compact_blank`, `deep_dup` — so a chain built from them keeps a
407
- concrete receiver throughout. A subscript read is deliberately
408
- left untyped: `params[:missing]` is `nil` at runtime, and a type
409
- that says otherwise would let the flow rules fold live
410
- conditions (`if params[:q]`, `url.nil?`) to a constant and report
411
- working code. Methods whose result depends on the call — `dig`,
412
- `fetch`, `compact!`, and the block-less `select` / `reject` /
413
- `transform_keys` / `transform_values` — are untyped for the same
414
- reason.
407
+ concrete receiver throughout. A subscript read answers
408
+ `Dynamic[ActionController::Parameters | nil]` (#2430): the static
409
+ facet is the nilable `Parameters`, so `params[:user][:name]` and
410
+ `params[:user].permit(:name)` keep dispatching on that facet, and
411
+ the `Dynamic` wrapper lets the value cross a declared boundary such
412
+ as a `String` parameter, as a scalar param does at runtime. The
413
+ facet is nilable rather than the non-nil nominal on purpose:
414
+ `params[:missing]` is `nil` at runtime, and a non-nil type would
415
+ let the flow rules fold live conditions (`if params[:q]`,
416
+ `url.nil?`) to a constant and report working code. Methods on the
417
+ carrier outside the chain (`params[:q].to_s`) answer `Dynamic[top]`,
418
+ because the engine dispatches a `Dynamic` receiver without projecting
419
+ its facet: a loss of precision, not a false positive. Methods whose
420
+ result depends on the call — `dig`, `fetch`, `compact!`, and the
421
+ block-less `select` / `reject` / `transform_keys` /
422
+ `transform_values` — are untyped for the same reason.
415
423
  - **`flash[:key]` and `session[:key]` stay untyped too**, for that
416
424
  same reason and measured the same way. Both are leaf reads that
417
425
  return whatever was stored — or `nil` for a key that is not set.
@@ -32,6 +32,17 @@ path, no vendoring, no `signature_paths:` wiring.
32
32
  > surface. When it is loaded, the auto overlay stands down so the two
33
33
  > never double-declare.
34
34
 
35
+ If the project also runs `rbs collection install` for `activesupport`,
36
+ some methods are declared twice — `Object#acts_like?`,
37
+ `Object#html_safe?`, `Time#to_fs` among them. Rigor merges the two:
38
+ the plugin's signatures are tried first and the collection's still
39
+ apply, so a call either one accepts type-checks. When your own `sig/`
40
+ or inline RBS declares one of the plugin's methods, yours is used
41
+ instead and the plugin's stands down; the run reports one
42
+ `rbs.coverage.plugin-signature-stood-down` info row for the plugin's
43
+ file, naming those methods and the file whose declaration is used for
44
+ each.
45
+
35
46
  ## What it covers
36
47
 
37
48
  Roughly the top ~40 selectors plus their close neighbours, across:
@@ -55,7 +66,15 @@ Roughly the top ~40 selectors plus their close neighbours, across:
55
66
  - **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang) and
56
67
  their `#to_options` alias, `#deep_merge`, `#reverse_merge` /
57
68
  `#with_defaults` (+ bang), `#with_indifferent_access`, `#except!`,
58
- `#extract!`.
69
+ `#extract!`. `#symbolize_keys` / `#to_options` map the key type per
70
+ member, as ActiveSupport does (`key.to_sym rescue key`): a `String` or
71
+ `Symbol` key reads as `Symbol`, and every other key (an `Integer`, a
72
+ `Float`, a project class) reads as `untyped`, because a mixin or a
73
+ project file can give any class a `to_sym`. So
74
+ `Hash[String, Integer]` gives `Hash[Symbol, Integer]` and
75
+ `Hash[Integer, Integer]` gives `Hash[untyped, Integer]`. Without the
76
+ plugin, on the activesupport overlay alone, the key reads `untyped`.
77
+ The bang forms answer the receiver's own type.
59
78
  - **Range** — `#overlaps?`, `#to_fs` / `#to_formatted_s`.
60
79
  - **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
61
80
 
@@ -97,6 +97,13 @@ module Rigor
97
97
  new(buckets, project_root: project_root)
98
98
  end
99
99
 
100
+ # The diagnostics a baseline cannot address: no rule (a Prism parse error, a path or internal error) or no path.
101
+ # `#filter` passes these through and `from_diagnostics` leaves them out of every bucket; this is the one
102
+ # definition of both.
103
+ def unkeyable(diagnostics)
104
+ diagnostics.reject { |d| d.qualified_rule && d.path }
105
+ end
106
+
100
107
  private
101
108
 
102
109
  def parse_loaded(raw, path:, project_root:)
@@ -138,17 +145,12 @@ module Rigor
138
145
  raise LoadError, "#{path}: ignored[#{index}] `message:` is not a valid Regexp: #{e.message}"
139
146
  end
140
147
 
141
- # Returns Hash{[file, rule, regex_or_nil] => Array<Diagnostic>}.
142
- # In message mode, each unique message gets its own bucket;
143
- # in rule mode, every diagnostic for a (file, rule) pair
144
- # contributes to a single bucket regardless of message.
145
- # File paths are normalised to relative before keying.
148
+ # Groups diagnostics by [file, rule, regex_or_nil]. In message mode, each unique message gets its own
149
+ # bucket; in rule mode, every diagnostic for a (file, rule) pair contributes to a single bucket regardless
150
+ # of message. File paths are normalised to relative before keying.
146
151
  def group_for_baseline(diagnostics, match_mode, project_root)
147
152
  root = Pathname.new(project_root)
148
- diagnostics.each_with_object({}) do |diag, into|
149
- next if diag.qualified_rule.nil?
150
- next if diag.path.nil?
151
-
153
+ (diagnostics - unkeyable(diagnostics)).each_with_object({}) do |diag, into|
152
154
  rel = relative_path(diag.path, root)
153
155
  key = case match_mode
154
156
  when :rule
@@ -160,18 +162,18 @@ module Rigor
160
162
  end
161
163
  end
162
164
 
165
+ def relative_path(path, root_pathname)
166
+ Pathname.new(path).relative_path_from(root_pathname).to_s
167
+ rescue ArgumentError
168
+ path # different drive letter on Windows or non-child path
169
+ end
170
+
163
171
  # Generates a Regexp source string for the baseline row. The string is `Regexp.escape`d so the YAML
164
172
  # round-trip produces a regex that matches the literal message. Users hand-editing the row can replace
165
173
  # the escaped form with a pattern.
166
174
  def message_pattern_for(message)
167
175
  Regexp.new(Regexp.escape(message.to_s))
168
176
  end
169
-
170
- def relative_path(path, root_pathname)
171
- Pathname.new(path).relative_path_from(root_pathname).to_s
172
- rescue ArgumentError
173
- path # different drive letter on Windows or non-child path
174
- end
175
177
  end
176
178
 
177
179
  class LoadError < StandardError; end
@@ -181,11 +183,8 @@ module Rigor
181
183
  def initialize(buckets, project_root: Dir.pwd)
182
184
  @project_root = Pathname.new(project_root)
183
185
  @buckets = buckets.freeze
184
- # For each (file, qualified_rule) pair, two arrays:
185
- # - rule-ID rows (message_regex == nil)
186
- # - message-pattern rows (message_regex != nil)
187
- # The matcher walks message-pattern rows first (tighter match takes precedence); diagnostics that
188
- # don't match any message row fall through to the rule-ID row if one exists.
186
+ # Buckets grouped by (file, rule). `claim_bucket_for` splits each group into message-pattern rows, tried
187
+ # first as the tighter match, and the rule-ID row (message_regex == nil) as the fallback.
189
188
  @by_pair = buckets.group_by { |b| [b.file, b.rule] }.freeze
190
189
  freeze
191
190
  end
@@ -216,7 +215,7 @@ module Rigor
216
215
 
217
216
  # Diagnostics that lacked a rule or a path bypass the baseline entirely (the baseline can't address
218
217
  # them).
219
- unkeyable = diagnostics.reject { |d| d.qualified_rule && d.path }
218
+ unkeyable = self.class.unkeyable(diagnostics)
220
219
  [surfaced + unkeyable, silenced_count]
221
220
  end
222
221
 
@@ -225,7 +224,7 @@ module Rigor
225
224
  # - `:within` — `actual <= count` (silenced by the filter).
226
225
  # - `:over` — `actual > count` (over threshold; surfaced in the regular `rigor check` output).
227
226
  # - `:cleared` — `actual == 0` (the bucket can be pruned).
228
- # - `:reducible` — `0 < actual < count` (the bucket's count can be tightened; future `regenerate` slice 5
227
+ # - `:reducible` — `0 < actual < count` (the bucket's count can be tightened; `rigor baseline regenerate`
229
228
  # handles this).
230
229
  DriftRow = Struct.new(:bucket, :actual_count, :status, keyword_init: true) do
231
230
  def delta
@@ -254,8 +253,8 @@ module Rigor
254
253
  end
255
254
  end
256
255
 
257
- # Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune` (slice 2) to
258
- # remove cleared buckets (`actual == 0`) from the on-disk file.
256
+ # Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune` to remove cleared
257
+ # buckets (`actual == 0`) from the on-disk file.
259
258
  def without(buckets_to_drop)
260
259
  dropset = buckets_to_drop.to_set
261
260
  self.class.new(buckets.reject { |b| dropset.include?(b) }, project_root: @project_root)
@@ -311,9 +310,7 @@ module Rigor
311
310
  # over rule-ID rows because they're more specific. A diagnostic that matches no row goes into a
312
311
  # synthetic "no-bucket" bin keyed by (file, rule).
313
312
  bins = {}
314
- diagnostics.each do |diag|
315
- next if diag.qualified_rule.nil? || diag.path.nil?
316
-
313
+ (diagnostics - self.class.unkeyable(diagnostics)).each do |diag|
317
314
  bucket = claim_bucket_for(diag)
318
315
  key = if bucket
319
316
  [bucket.file, bucket.rule,
@@ -23,10 +23,11 @@ module Rigor
23
23
  # `BlockNode` ancestors. Mutation tracking through loop bodies is incomplete (`shift = 0; loop {
24
24
  # shift += 7 }` keeps `shift` at `Constant<7>` per the analyzer), so a `Constant<bool>` predicate
25
25
  # inside the loop body is suspect.
26
- # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?`. These
27
- # read like the user explicitly checking for a runtime condition that the type system already proves
28
- # can't happen — Rigor's strict-on-returns RBS often disagrees with the user's defensive check (e.g.
29
- # `Module#name` returns `String` per RBS but anonymous classes really do return `nil`). Skipping
26
+ # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?` /
27
+ # `.respond_to?`. These read like the user explicitly checking for a runtime condition that the type
28
+ # system already proves can't happen — Rigor's strict-on-returns RBS often disagrees with the user's
29
+ # defensive check (e.g. `Module#name` returns `String` per RBS but anonymous classes really do return
30
+ # `nil`). Skipping
30
31
  # these forms keeps the rule useful for genuine logic errors without faulting defensive code.
31
32
  #
32
33
  # Also skipped (so the literal-only `unreachable-branch` rule doesn't double-fire alongside this one):
@@ -53,8 +54,6 @@ module Rigor
53
54
  NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
54
55
  RULE_WALK_GATES = [:loop_or_block].freeze
55
56
 
56
- # @return one entry per qualifying predicate. Empty when the tree carries no firing
57
- # predicates.
58
57
  def initialize(scope_index)
59
58
  @scope_index = scope_index
60
59
  @results = []
@@ -35,8 +35,8 @@ module Rigor
35
35
  # exactly like the legacy walk.
36
36
  NODE_CLASSES = [Prism::DefNode].freeze
37
37
 
38
- # Returns `Array<{def_class:, def_name:, write_node:}>` — one entry per dead-assignment write. Empty
39
- # when the tree has no qualifying writes.
38
+ # One entry per dead-assignment write, as `{def_node:, write_node:}`. Empty when the tree has no
39
+ # qualifying writes.
40
40
  def initialize(scope_index)
41
41
  @scope_index = scope_index
42
42
  @results = []
@@ -24,10 +24,10 @@ module Rigor
24
24
  # currently declaration-sourced ivar*, so a second hop (`c = @count; d = c`) and an `||=` rewrite land on
25
25
  # the plain `with_local` path and carry no mark.
26
26
  #
27
- # A branch join is NOT one of those cases, contrary to what this comment claimed when it landed: both
28
- # arms of `if c then r = @count else r = @count end` stamp `(:local, :r)`, and `join_declaration_sourced`
29
- # INTERSECTS, so the mark survives. Only an *asymmetric* join drops it, and by that intersection rather
30
- # than by reaching `with_local`. The normative statement — establishment, drop, join and the full list of
27
+ # A branch join keeps the mark when both arms stamp it: `if c then r = @count else r = @count end` stamps
28
+ # `(:local, :r)` on each arm, and `join_declaration_sourced` INTERSECTS, so the mark survives. Only an
29
+ # *asymmetric* join drops it, by that intersection rather than by reaching `with_local`. The normative
30
+ # statement — establishment, drop, join and the full list of
31
31
  # unmarked shapes, each verified against the implementation — is
32
32
  # `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
33
33
  # comment subordinate to it.
@@ -36,8 +36,8 @@ module Rigor
36
36
  # a Hash literal, loops and blocks included.
37
37
  NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
38
38
 
39
- # Returns `Array<{hash_node:, key_node:, first_key_node:, key_label:}>` — one entry per LATER
40
- # occurrence of a repeated literal key (`key_node` is the repeat the diagnostic points at;
39
+ # One entry per LATER occurrence of a repeated literal key, carrying `hash_node`, `key_node`,
40
+ # `first_key_node` and `key_label` (`key_node` is the repeat the diagnostic points at;
41
41
  # `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
42
42
  # qualifying duplicates.
43
43
  def initialize(scope_index)
@@ -29,6 +29,13 @@ module Rigor
29
29
  # under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
30
30
  # separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
31
31
  # ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
32
+ # - `Array(param)` (issue #2280): the conversion passes the parameter's elements through, so
33
+ # `Array(param).first.upcase` reads the lower bound back.
34
+ # - an `if` / `unless` / ternary whose value is the parameter (issue #2280): `(c ? param : param).upcase`.
35
+ # Unlike `&&` / `||`, a branch that does not hold the parameter keeps its diagnostic: the form is rooted
36
+ # when every branch that can run is. A predicate the engine folds to one arm (the same certainty
37
+ # `StatementEvaluator#eval_if` elides the dead arm on) leaves only that arm to decide, so
38
+ # `(c ? param : 1).upcase` declines when `c` folds truthy and reports `Integer` when it folds falsey.
32
39
  #
33
40
  # Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
34
41
  # is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
@@ -40,40 +47,105 @@ module Rigor
40
47
  # length, which is tiny in practice).
41
48
  MAX_DEPTH = 64
42
49
 
43
- def rooted?(node, scope, depth = 0)
50
+ # `fold` is whether `scope` is still the scope the walked node is evaluated in. The callers pass the scope
51
+ # at the start of the expression; once the walk passes something evaluated earlier inside it (a leading
52
+ # statement, the left operand of `&&`, an earlier literal element, a conditional's predicate), that
53
+ # earlier code may have rebound what a nested predicate reads (`(c = false; c ? param : 1)`), so a
54
+ # conditional below no longer folds its predicate in `scope`.
55
+ def rooted?(node, scope, depth = 0, fold: true)
44
56
  return false if node.nil? || depth > MAX_DEPTH
45
57
 
46
58
  case node
47
59
  when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
48
- when Prism::CallNode then rooted?(node.receiver, scope, depth + 1)
60
+ when Prism::CallNode then call_rooted?(node, scope, depth + 1, fold)
49
61
  when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
50
- literal_rooted?(node, scope, depth + 1)
51
- else combining_rooted?(node, scope, depth + 1)
62
+ literal_rooted?(node, scope, depth + 1, fold)
63
+ else combining_rooted?(node, scope, depth + 1, fold)
64
+ end
65
+ end
66
+
67
+ def call_rooted?(node, scope, depth, fold)
68
+ return rooted?(node.receiver, scope, depth, fold: fold) unless array_conversion?(node, scope)
69
+
70
+ rooted?(node.arguments.arguments.first, scope, depth, fold: fold)
71
+ end
72
+
73
+ # A receiverless `Array(x)` reaching Kernel#Array: exactly one plain argument, no block, and no project
74
+ # `Array` on `self`'s class or its project ancestors to bind ahead of Kernel's.
75
+ def array_conversion?(node, scope)
76
+ node.receiver.nil? && node.name == :Array && node.block.nil? &&
77
+ node.arguments&.arguments&.size == 1 && !project_array_method?(scope)
78
+ end
79
+
80
+ def project_array_method?(scope)
81
+ self_type = scope.self_type
82
+ case self_type
83
+ when Type::Nominal then scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :instance)
84
+ when Type::Singleton
85
+ scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :singleton)
86
+ else false
52
87
  end
53
88
  end
54
89
 
55
90
  # An array or hash literal is rooted when any element is: the engine reads the element back precisely.
56
- def literal_rooted?(node, scope, depth)
91
+ def literal_rooted?(node, scope, depth, fold)
57
92
  parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
58
- parts.any? { |part| rooted?(part, scope, depth) }
93
+ parts.each_with_index.any? { |part, index| rooted?(part, scope, depth, fold: fold && index.zero?) }
59
94
  end
60
95
 
61
96
  # Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
62
97
  # operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
63
98
  # on the whole expression is FP-safe by the same lower-bound argument.
64
- def combining_rooted?(node, scope, depth)
99
+ def combining_rooted?(node, scope, depth, fold)
65
100
  case node
66
101
  when Prism::RescueModifierNode
67
- rooted?(node.expression, scope, depth) || rooted?(node.rescue_expression, scope, depth)
68
- when Prism::AndNode, Prism::OrNode then rooted?(node.left, scope, depth) || rooted?(node.right, scope, depth)
69
- when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth)
70
- when Prism::StatementsNode then rooted?(node.body.last, scope, depth)
102
+ rooted?(node.expression, scope, depth, fold: fold) ||
103
+ rooted?(node.rescue_expression, scope, depth, fold: false)
104
+ when Prism::AndNode, Prism::OrNode
105
+ rooted?(node.left, scope, depth, fold: fold) || rooted?(node.right, scope, depth, fold: false)
106
+ when Prism::ParenthesesNode then body_rooted?(node.body, scope, depth, fold)
107
+ when Prism::StatementsNode then body_rooted?(node, scope, depth, fold)
108
+ when Prism::IfNode, Prism::UnlessNode then conditional_rooted?(node, scope, depth, fold)
109
+ when Prism::ElseNode then rooted?(node.statements, scope, depth, fold: fold)
110
+ else false
111
+ end
112
+ end
113
+
114
+ # A body's value is its last statement; any statement before it runs first.
115
+ def body_rooted?(body, scope, depth, fold)
116
+ return rooted?(body, scope, depth, fold: fold) unless body.is_a?(Prism::StatementsNode)
117
+
118
+ rooted?(body.body.last, scope, depth, fold: fold && body.body.size == 1)
119
+ end
120
+
121
+ # Every branch that can run must be rooted; an absent branch is `nil`, never rooted. The predicate is
122
+ # typed only when the branches disagree, so a conditional that never mentions a parameter costs nothing.
123
+ # An arm runs after the predicate, so a conditional nested in one never folds.
124
+ def conditional_rooted?(node, scope, depth, fold)
125
+ truthy_rooted, falsey_rooted = arms(node).map { |arm| rooted?(arm, scope, depth, fold: false) }
126
+ return truthy_rooted if truthy_rooted == falsey_rooted
127
+ return false unless fold
128
+
129
+ case predicate_certainty(node.predicate, scope)
130
+ when :truthy then truthy_rooted
131
+ when :falsey then falsey_rooted
71
132
  else false
72
133
  end
73
134
  end
74
135
 
75
- def last_statement(body)
76
- body.is_a?(Prism::StatementsNode) ? body.body.last : body
136
+ # `[runs when the predicate is truthy, runs when it is falsey]`.
137
+ def arms(node)
138
+ node.is_a?(Prism::IfNode) ? [node.statements, node.subsequent] : [node.else_clause, node.statements]
139
+ end
140
+
141
+ # The arm certainty `StatementEvaluator#branch_certainty` elides on: a decidable version guard, else the
142
+ # predicate's folded truthiness unless an optimistic carrier props it up.
143
+ def predicate_certainty(predicate, scope)
144
+ guard = Inference::VersionGuard.verdict(predicate)
145
+ return guard if guard
146
+ return nil unless Inference::OptimisticOrigin.resolve(predicate, scope).nil?
147
+
148
+ Inference::Narrowing.predicate_certainty(scope.type_of(predicate))
77
149
  end
78
150
  end
79
151
  end
@@ -44,8 +44,9 @@ module Rigor
44
44
  NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
45
45
  RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
46
46
 
47
- # Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:,
48
- # method_name:, visibility:, per_invocation:, action:}>]]`. Empty when the tree has no qualifying writes.
47
+ # Groups the writes by class qualified name, then by ivar name. Each write carries `node`, `type`,
48
+ # `declared`, `method_name`, `visibility`, `per_invocation` and `action`. Empty when the tree has no
49
+ # qualifying writes.
49
50
  # `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
50
51
  # the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
51
52
  #
@@ -152,7 +152,6 @@ module Rigor
152
152
  end
153
153
  end
154
154
 
155
- # `o.singleton_class`.
156
155
  def singleton_class_of_local?(node)
157
156
  node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? &&
158
157
  node.receiver.is_a?(Prism::LocalVariableReadNode)
@@ -12,16 +12,13 @@ module Rigor
12
12
  # decides, with NO loop / block suppression and NO threaded context — so the collector declares no
13
13
  # `RULE_WALK_GATES` and ignores the walk's `context`.
14
14
  #
15
- # The per-node dispatch itself stays in `CheckRules` (the verbatim `case` from `diagnose`'s former inline
16
- # walk — only the traversal moved, ADR-53 WD4) and is handed in as a callable built in the `CheckRules`
17
- # module context, so its calls to the private diagnostic builders remain implicit-self. The collector
18
- # only owns the traversal hook and accumulation. Unlike the fact collectors its `#results` are the
19
- # accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
20
- # shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
15
+ # The per-node dispatch stays in `CheckRules` (ADR-53 WD4) and is handed in as a callable built in the
16
+ # `CheckRules` module context, so its calls to the private diagnostic builders remain implicit-self. The
17
+ # collector only owns the traversal hook and accumulation; `#results` are the accumulated {Diagnostic}s in
18
+ # the walk's emission order.
21
19
  class MainPassCollector
22
20
  # The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
23
- # too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same). The three global-write
24
- # classes carry issue #1367's `global.*` rules.
21
+ # too. The global-write classes and `MultiWriteNode` carry issue #1367's `global.*` rules.
25
22
  NODE_CLASSES = [
26
23
  Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
27
24
  Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
@@ -19,7 +19,7 @@ module Rigor
19
19
  # branched on, whose declaration the reader's author never opened. A configuration constant read in ten
20
20
  # files would put a warning in all ten.
21
21
  #
22
- # This is [ADR-58](../../../../docs/adr/58-declaration-sourced-ivar-typing.md)'s discipline applied to a
22
+ # This is [ADR-58](../../../../docs/adr/58-ivar-field-typing.md)'s discipline applied to a
23
23
  # different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
24
24
  # can lose precision but can never manufacture a false positive"
25
25
  # (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
@@ -39,8 +39,8 @@ module Rigor
39
39
  SEND_CALLS = %i[send __send__ public_send].freeze
40
40
  # Reading the block through these runs it with its own `self`, or does not run it.
41
41
  PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
42
- # Calls, on any receiver, that can reach a local by name: a `Binding`, a string `eval`, and the string forms of
43
- # the `*_eval` family (with an argument), whose code runs in the caller's scope.
42
+ # Calls, on any receiver, that can reach a local by name: `binding`, `eval`, `local_variable_get`. The string
43
+ # forms of the `*_eval` family (`STRING_EVAL_CALLS`, with an argument) run in the caller's scope too.
44
44
  BINDING_CALLS = %i[binding eval local_variable_get].freeze
45
45
  STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
46
46
  private_constant :STRING_EVAL_CALLS
@@ -96,7 +96,7 @@ module Rigor
96
96
  found
97
97
  end
98
98
 
99
- # `{name => {[owner, side] => rows}}` over the project's named `def` rows, built once per file.
99
+ # The project's named `def` row counts, keyed by method name and then by owner and side; built once per file.
100
100
  def def_rows(scope)
101
101
  @def_rows ||= RebindingBlockCallees.def_row_counts(scope)
102
102
  end
@@ -55,8 +55,7 @@ module Rigor
55
55
  collect_ensure_returns(begin_node)
56
56
  end
57
57
 
58
- # Returns `Array<{return_node:}>` — one entry per offending `return`, frozen the same way
59
- # `#collect` returns it.
58
+ # One `{return_node:}` entry per offending `return`, frozen the same way `#collect` returns them.
60
59
  def results
61
60
  @results.freeze
62
61
  end