rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../analysis/template_unit_paths"
6
+ require_relative "../analysis/template_unit_positions"
7
+ require_relative "../analysis/template_units"
8
+ require_relative "../inference/precision_scanner"
9
+ require_relative "../inference/scope_indexer"
10
+ require_relative "../source/node_locator"
11
+
12
+ module Rigor
13
+ class CLI
14
+ # #1040 — `rigor type-of` against a **template unit**: a file a loaded plugin compiles into Ruby.
15
+ #
16
+ # Before this the command read the template from disk and parsed its bytes as Ruby, so an ERB view
17
+ # exited 1 on a parse error and an identity-transformed one answered about the wrong node under an
18
+ # unseeded scope. A template path now takes the path `rigor check` takes: the run's
19
+ # {Analysis::TemplateUnits} index is built, the COMPILED source is parsed with the unit's `scopes:`, the
20
+ # file scope is seeded with the unit's `self`, locals and ivars, and the requested template position is
21
+ # resolved through {Analysis::TemplateUnitPositions} — which declines, with a message, whenever the
22
+ # position does not denote exactly one compiled node.
23
+ #
24
+ # Mixed into {TypeOfCommand}; a path no loaded plugin claims never reaches anything here, so a `.rb`
25
+ # probe does not build the index or change a byte of its output.
26
+ module TypeOfTemplateProbe
27
+ # Why a template position has no answer, keyed by {Analysis::TemplateUnitPositions#node_at}'s reasons.
28
+ DECLINE_MESSAGES = {
29
+ no_compiled_line: "the template compiler emitted no Ruby for this line",
30
+ not_verbatim: "the position is in template markup, or in code the template compiler rewrote " \
31
+ "rather than copied, so no compiled expression is made of the bytes there",
32
+ ambiguous: "the bytes there are template markup, or code the compiler copied to more than one " \
33
+ "place, so the position does not name a single compiled expression"
34
+ }.freeze
35
+ private_constant :DECLINE_MESSAGES
36
+ Ractor.make_shareable(DECLINE_MESSAGES)
37
+
38
+ private
39
+
40
+ # The unit `file` compiles to, `nil` for a path that is not a template unit (the caller probes it as
41
+ # Ruby), or an exit status after reporting a template the claiming plugin could not compile.
42
+ def template_unit_probe(file, environment, buffer)
43
+ registry = environment.plugin_registry
44
+ return nil unless template_claimed?(registry, file)
45
+
46
+ units = (@template_units ||= Analysis::TemplateUnits.collect(registry: registry, buffer: buffer))
47
+ entry = units[file]
48
+ return [units, entry] if entry
49
+
50
+ # A template the plugin DECLINED is not a unit, and is probed as the command always probed it —
51
+ # which for a template that is not Ruby is a Prism error about bytes the user did not write, so
52
+ # the decline is stated first rather than left to be inferred from the error.
53
+ failures = template_failures(units, file)
54
+ if failures.empty?
55
+ @err.puts("type-of: #{file}: plugin declined the template; probing its bytes as Ruby")
56
+ return nil
57
+ end
58
+
59
+ failures.each do |failure|
60
+ @err.puts("type-of: #{file}: plugin #{failure.plugin_id} could not compile the template: " \
61
+ "#{failure.message}")
62
+ end
63
+ 1
64
+ end
65
+
66
+ # Only a claimed path builds the index — a `.rb` probe pays no glob expansion and no transform.
67
+ def template_claimed?(registry, file)
68
+ return false if registry.nil? || registry.empty?
69
+
70
+ relative = Analysis::TemplateUnitPaths.relative(file, Dir.pwd)
71
+ registry.plugins.any? do |plugin|
72
+ globs = begin
73
+ plugin.manifest.template_globs
74
+ rescue StandardError
75
+ []
76
+ end
77
+ !globs.empty? && Analysis::TemplateUnitPaths.claims?(globs, relative)
78
+ end
79
+ end
80
+
81
+ def template_failures(units, file)
82
+ relative = Analysis::TemplateUnitPaths.relative(file, Dir.pwd)
83
+ units.failures.select { |failure| failure.path == relative }
84
+ end
85
+
86
+ # Every result for one template: the compiled source parsed and indexed once under the unit's seeded
87
+ # scope, then each requested template position resolved through the inverse map.
88
+ def resolve_template(file, physical, unit_probe, indexed_targets, context)
89
+ units, entry = unit_probe
90
+ parse_result = Prism.parse(entry.source, filepath: file, version: context[:configuration].target_ruby,
91
+ scopes: units.parse_scopes(file))
92
+ return 1 if compiled_parse_errors?(parse_result, file, entry)
93
+
94
+ scope = units.seed(context[:base_scope].with_source_path(file), file)
95
+ scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
96
+ positions = Analysis::TemplateUnitPositions.new(
97
+ entry: entry, template: File.binread(physical), root: parse_result.value,
98
+ skip_node_types: Inference::PrecisionScanner::NON_EXPRESSION_NODE_TYPES
99
+ )
100
+ probe = { file: file, entry: entry, positions: positions, scope_index: scope_index,
101
+ options: context[:options] }
102
+ indexed_targets.map do |target, index|
103
+ resolved = resolve_template_target(probe, target)
104
+ return resolved if resolved.is_a?(Integer)
105
+
106
+ [index, resolved]
107
+ end
108
+ end
109
+
110
+ def resolve_template_target(probe, target)
111
+ file = probe[:file]
112
+ positions = probe[:positions]
113
+ return CLI::EXIT_USAGE unless template_position_in_range?(positions, target)
114
+ return enumerate_template_line(probe, target.line) if target.column.nil?
115
+
116
+ node = positions.node_at(line: target.line, column: target.column)
117
+ if node.is_a?(Symbol)
118
+ @err.puts("type-of: no expression found at #{file}:#{target.line}:#{target.column}: " \
119
+ "#{DECLINE_MESSAGES.fetch(node)}")
120
+ return []
121
+ end
122
+
123
+ [type_result(file, target.line, target.column, node, probe[:scope_index], probe[:options],
124
+ location_mapper: template_location_mapper(probe))]
125
+ end
126
+
127
+ def enumerate_template_line(probe, line)
128
+ unless line.between?(1, probe[:positions].template_line_count)
129
+ @err.puts("type-of: no expression found on #{probe[:file]}:#{line}")
130
+ return []
131
+ end
132
+
133
+ rows, total = probe[:positions].line_nodes(line)
134
+ if rows.empty?
135
+ @err.puts("type-of: no expression found on #{probe[:file]}:#{line}: no compiled expression maps to " \
136
+ "a single column of this line (markup, code the template compiler rewrote, or code " \
137
+ "copied to more than one place)")
138
+ return []
139
+ end
140
+
141
+ enumeration = TypeOfCommand::LineEnumeration.new(total: total, shown: rows.length)
142
+ mapper = template_location_mapper(probe)
143
+ rows.map do |column, node|
144
+ type_result(probe[:file], line, column, node, probe[:scope_index], probe[:options],
145
+ enumeration: enumeration, location_mapper: mapper)
146
+ end
147
+ end
148
+
149
+ # The same range errors, and the same exit status, a `.rb` probe gives — measured against the
150
+ # TEMPLATE's lines, which are the ones the user named. A bare `FILE:LINE` past the end is "no
151
+ # expression found", exactly as it is for Ruby.
152
+ def template_position_in_range?(positions, target)
153
+ return true if target.column.nil?
154
+
155
+ message = if target.line < 1 then "line must be >= 1, got #{target.line}"
156
+ elsif target.column < 1 then "column must be >= 1, got #{target.column}"
157
+ elsif target.line > positions.template_line_count
158
+ "line #{target.line} is past the end of the source buffer"
159
+ end
160
+ return true if message.nil?
161
+
162
+ @err.puts("type-of: #{message}")
163
+ false
164
+ end
165
+
166
+ # A `--trace` fallback carries a COMPILED location; it is reported at the template line, and at a
167
+ # template column only when the location lies in a verbatim run.
168
+ def template_location_mapper(probe)
169
+ entry = probe[:entry]
170
+ positions = probe[:positions]
171
+ lambda do |location|
172
+ [entry.template_line(location.start_line), positions.template_column_for(location)]
173
+ end
174
+ end
175
+
176
+ # A compiled source that does not parse is the plugin's output, not the user's text; its errors are
177
+ # reported at the template lines they map to, as `rigor check` reports every other finding in a unit.
178
+ def compiled_parse_errors?(result, file, entry)
179
+ return false if result.errors.empty?
180
+
181
+ result.errors.each do |error|
182
+ @err.puts("#{file}:#{entry.template_line(error.location.start_line)}: " \
183
+ "compiled template does not parse: #{error.message}")
184
+ end
185
+ true
186
+ end
187
+ end
188
+ end
189
+ end
@@ -131,7 +131,7 @@ module Rigor
131
131
  # ADR-102 WD2), which is exactly why the two corpora are separated rather than both widened.
132
132
  def reference_files(_paths, configuration)
133
133
  relative = Analysis::Reachability::ProjectFiles.own(Dir.glob(REFERENCE_GLOB, base: Dir.pwd), Dir.pwd)
134
- absolute = relative.map { |rel| File.expand_path(rel) }
134
+ absolute = relative.map { |rel| File.absolute_path(rel) }
135
135
  Analysis::PathExpansion.reject_excluded(absolute, configuration.exclude_patterns).to_set
136
136
  end
137
137
 
@@ -177,7 +177,7 @@ module Rigor
177
177
  return [] if names.empty?
178
178
 
179
179
  Analysis::Reachability::ProjectFiles.own(Dir.glob(TEMPLATE_GLOB, base: Dir.pwd), Dir.pwd).flat_map do |rel|
180
- absolute = File.expand_path(rel)
180
+ absolute = File.absolute_path(rel)
181
181
  haystack = cache.serve(:runs, absolute) { constant_bearing_text(File.read(absolute).scrub) }
182
182
  names.filter_map do |fqn|
183
183
  next unless haystack.include?(fqn)
@@ -208,7 +208,7 @@ module Rigor
208
208
  return [] if globs.empty?
209
209
 
210
210
  matcher = Analysis::Reachability::ProjectFiles.method(:entry_point_match?)
211
- cwd = "#{File.expand_path(Dir.pwd)}/"
211
+ cwd = "#{File.absolute_path(Dir.pwd)}/"
212
212
  declarations.filter_map do |d|
213
213
  relative = d.path.delete_prefix(cwd)
214
214
  d.fqn if globs.any? { |g| matcher.call(g, d.path) || matcher.call(g, relative) }
data/lib/rigor/cli.rb CHANGED
@@ -183,7 +183,7 @@ module Rigor
183
183
  return 1
184
184
  end
185
185
 
186
- File.write(path, init_template)
186
+ File.write(path, init_template(path))
187
187
  @out.puts("Created #{path}")
188
188
  print_init_next_steps(path)
189
189
  0
@@ -230,7 +230,7 @@ module Rigor
230
230
  # Renders the starter `.rigor.yml` body. The template serialises `Configuration::DEFAULTS` (so the on-disk file
231
231
  # round-trips through `Configuration.load`) and prepends a short header that points the user at the keys they are
232
232
  # most likely to want to edit.
233
- def init_template
233
+ def init_template(path = ".rigor.dist.yml")
234
234
  <<~YAML
235
235
  # yaml-language-server: $schema=#{CONFIG_SCHEMA_URL}
236
236
  # Rigor configuration. The full key reference is
@@ -266,6 +266,12 @@ module Rigor
266
266
  # Leave unset (or `null`) to auto-detect
267
267
  # `<root>/sig`. Use `[]` to disable
268
268
  # project-RBS loading entirely.
269
+ # - test_paths: the directories holding your tests.
270
+ # `rigor sig-gen --params=observed` reads
271
+ # their call sites to type parameters.
272
+ # `rigor init` writes the spec/ and test/
273
+ # it finds; unset auto-detects them, and
274
+ # `[]` declares the project has none.
269
275
  # - cache.path: where Rigor will eventually persist
270
276
  # analysis results across runs.
271
277
  #
@@ -275,10 +281,22 @@ module Rigor
275
281
  # tempfile, uri, logger, date, prism, rbs). Adding a
276
282
  # `sig/<gem>.rbs` file under `sig/` is the simplest way
277
283
  # to extend type coverage today.
278
- #{YAML.dump(Configuration::DEFAULTS).sub(/\A---\n/, '')}
284
+ #{YAML.dump(init_settings(path)).sub(/\A---\n/, '')}
279
285
  YAML
280
286
  end
281
287
 
288
+ # The settings `rigor init` writes: the defaults, with `test_paths:` spelled out as the test roots found now, so
289
+ # the project's config says where its tests are rather than leaving it to auto-detection. The roots are written
290
+ # relative to the config file's directory, which is what {Configuration.load} resolves them against.
291
+ def init_settings(path)
292
+ test_paths = Configuration.new.resolved_test_paths
293
+ return Configuration::DEFAULTS if test_paths.empty?
294
+
295
+ config_dir = Pathname(File.absolute_path(File.dirname(path)))
296
+ relative = test_paths.map { |root| Pathname(root).relative_path_from(config_dir).to_s }
297
+ Configuration::DEFAULTS.merge("test_paths" => relative)
298
+ end
299
+
282
300
  def run_annotate
283
301
  require_relative "cli/annotate_command"
284
302
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "did_you_mean"
4
4
 
5
+ require_relative "config_path"
5
6
  require_relative "signature_path_audit"
6
7
  # ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
7
8
  # `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
@@ -199,14 +200,14 @@ module Rigor
199
200
  end
200
201
 
201
202
  def self.add_missing_dir(warnings, path, project_root, kind, key)
202
- return if path.nil? || File.directory?(File.expand_path(path, project_root))
203
+ return if path.nil? || File.directory?(ConfigPath.absolute(path, project_root))
203
204
 
204
205
  warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} is not a directory",
205
206
  fields: { "path" => path })
206
207
  end
207
208
 
208
209
  def self.add_missing_file(warnings, path, project_root, kind, key)
209
- return if path.nil? || File.file?(File.expand_path(path, project_root))
210
+ return if path.nil? || File.file?(ConfigPath.absolute(path, project_root))
210
211
 
211
212
  warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} does not exist", fields: { "path" => path })
212
213
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Resolves a path-valued setting that a person wrote into `.rigor.yml`, or that Bundler wrote into its
5
+ # own config (`BUNDLE_PATH: "~/gems"`), to an absolute path.
6
+ #
7
+ # A leading `~/` and a bare `~` expand to the invoking user's home directory — exactly what
8
+ # `File.expand_path` does for those two spellings, and what Bundler itself does for `BUNDLE_PATH`. Every
9
+ # other spelling goes through `File.absolute_path`, so `~drafts/x` stays a directory named `~drafts`
10
+ # instead of `File.expand_path`'s home-of-user-`drafts` reading, which raises `ArgumentError` for a user
11
+ # that does not exist (#1510). It never raises on a `~`.
12
+ #
13
+ # Paths the analysis is *given to analyse* (CLI path arguments, template units, the runner's file sets)
14
+ # do not use this: the shell has already expanded a `~` there, so a surviving one is a directory name.
15
+ module ConfigPath
16
+ HOME_PREFIX = %r{\A~(?=/|\z)}
17
+
18
+ # `base_dir` anchors a relative result and defaults to the working directory.
19
+ def self.absolute(path, base_dir = nil)
20
+ text = path.to_s
21
+ return File.absolute_path(text, base_dir) unless text.match?(HOME_PREFIX)
22
+
23
+ home = Dir.home
24
+ File.absolute_path(text.sub(HOME_PREFIX, home), base_dir)
25
+ rescue ArgumentError
26
+ File.absolute_path(text, base_dir)
27
+ end
28
+ end
29
+ end
@@ -54,6 +54,11 @@ module Rigor
54
54
  "def.override-return-widened" => :off,
55
55
  "def.override-param-narrowed" => :off,
56
56
  "def.ivar-write-mismatch" => :warning,
57
+ # Issue #1367 — both report a write its setter rejects, so it raises on every run. The type check rests on
58
+ # an inferred value type, so it follows `call.argument-type-mismatch` (warning under lenient); the
59
+ # read-only write needs no type at all, so it is an error in every profile.
60
+ "global.write-type-mismatch" => :warning,
61
+ "global.readonly-write" => :error,
57
62
  # ADR-8 companion (PHPStan IgnoreParseErrorRule-modelled): a broken suppression comment is
58
63
  # equally bad in every profile — it silently fails to do what the author believes it does — so
59
64
  # both rules stay :warning across all three profiles (including strict).
@@ -80,7 +85,12 @@ module Rigor
80
85
  # Opt-in author assertion: you only see it if you wrote a
81
86
  # `conforms-to` directive, so it stays a :warning even in
82
87
  # lenient — it is never unsolicited noise.
83
- "rbs_extended.unsatisfied-conformance" => :warning
88
+ "rbs_extended.unsatisfied-conformance" => :warning,
89
+ # ADR-112 WD5 / #1075 — an error in every profile, lenient included. The rule guards its
90
+ # false-positive rate through its narrow definition of a contradiction, and ADR-112 WD5 rules
91
+ # out lowering the severity instead; only a proven disagreement between two declarations the
92
+ # project wrote itself can fire.
93
+ "rbs.contradicting-signature" => :error
84
94
  }.freeze,
85
95
  balanced: {
86
96
  "call.undefined-method" => :error,
@@ -110,6 +120,8 @@ module Rigor
110
120
  "def.override-return-widened" => :warning,
111
121
  "def.override-param-narrowed" => :warning,
112
122
  "def.ivar-write-mismatch" => :warning,
123
+ "global.write-type-mismatch" => :error,
124
+ "global.readonly-write" => :error,
113
125
  "suppression.unknown-rule" => :warning,
114
126
  "suppression.empty" => :warning,
115
127
  "suppression.unknown-marker" => :warning,
@@ -118,7 +130,8 @@ module Rigor
118
130
  "effect.liskov-widened" => :warning,
119
131
  "effect.unknown-label" => :info,
120
132
  "effect.annotations-unchecked" => :info,
121
- "rbs_extended.unsatisfied-conformance" => :warning
133
+ "rbs_extended.unsatisfied-conformance" => :warning,
134
+ "rbs.contradicting-signature" => :error
122
135
  }.freeze,
123
136
  strict: {
124
137
  "call.undefined-method" => :error,
@@ -147,6 +160,8 @@ module Rigor
147
160
  "def.override-return-widened" => :error,
148
161
  "def.override-param-narrowed" => :error,
149
162
  "def.ivar-write-mismatch" => :error,
163
+ "global.write-type-mismatch" => :error,
164
+ "global.readonly-write" => :error,
150
165
  "suppression.unknown-rule" => :warning,
151
166
  "suppression.empty" => :warning,
152
167
  "suppression.unknown-marker" => :warning,
@@ -158,7 +173,8 @@ module Rigor
158
173
  # `:info` even under strict: a residual that failed a build would punish the project for
159
174
  # carrying an annotation it has not opted into checking, which is the opposite of the point.
160
175
  "effect.annotations-unchecked" => :info,
161
- "rbs_extended.unsatisfied-conformance" => :error
176
+ "rbs_extended.unsatisfied-conformance" => :error,
177
+ "rbs.contradicting-signature" => :error
162
178
  }.freeze
163
179
  }.freeze
164
180
 
@@ -194,7 +210,12 @@ module Rigor
194
210
  profile_table.fetch(rule, authored_severity)
195
211
  end
196
212
 
213
+ # A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
214
+ # diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
215
+ # so the split below is skipped for them rather than allocating a name that nothing can match.
197
216
  def family_override(rule, overrides)
217
+ return nil unless overrides.any? { |key, _| !key.to_s.include?(".") }
218
+
198
219
  family = rule.split(".").first
199
220
  return nil if family.nil?
200
221
 
@@ -6,6 +6,7 @@ require_relative "bleeding_edge"
6
6
  require_relative "ci_detector"
7
7
  require_relative "configuration/dependencies"
8
8
  require_relative "configuration/severity_profile"
9
+ require_relative "config_path"
9
10
  require_relative "configuration_error"
10
11
  require_relative "effects/entry_points"
11
12
  require_relative "effects/label"
@@ -49,6 +50,9 @@ module Rigor
49
50
  **/node_modules/**
50
51
  ].freeze
51
52
 
53
+ # The conventional test roots `test_paths: nil` auto-detects, in this order.
54
+ CONVENTIONAL_TEST_PATHS = %w[spec test].freeze
55
+
52
56
  DEFAULTS = {
53
57
  "target_ruby" => "4.0",
54
58
  "paths" => ["lib"],
@@ -57,6 +61,20 @@ module Rigor
57
61
  "disable" => [],
58
62
  "libraries" => [],
59
63
  "signature_paths" => nil,
64
+ # The project's test roots: the directories (or files) holding its tests. `nil` (the default) auto-detects
65
+ # whichever of `spec/` and `test/` exist, the same way `signature_paths: nil` auto-detects `sig/`; `[]`
66
+ # declares that the project has none. `rigor sig-gen --params=observed` harvests call-site observations
67
+ # from them when no `--observe=PATH` is given. See {#resolved_test_paths}.
68
+ "test_paths" => nil,
69
+ # ADR-112 WD4 — `rigor sig-gen` settings. `inline_declared:` decides what happens to a method the inline
70
+ # reader (`rigor-rbs-inline`) already declares from `# @rbs` / `#:`. `write` (the default) copies the
71
+ # inline declaration into `sig/`, so the generated signature is the complete contract a gem ships;
72
+ # `skip` leaves every member that reader declares out of `sig/`, for a project whose Steep reads the
73
+ # same annotations (`inline: true` beside `signature "sig"`), where the copy would be a second
74
+ # declaration of one method (`DuplicatedMethodDefinition`). See {#sig_gen_inline_declared}.
75
+ "sig_gen" => {
76
+ "inline_declared" => "write"
77
+ },
60
78
  # ADR-17 — project-side monkey-patch pre-evaluation. Empty by default; users opt in by listing explicit
61
79
  # files that the analyzer walks before per-file inference so patched-method declarations are visible
62
80
  # across the project (e.g. `lib/core_ext/string_extensions.rb`). Slice 1 plumbing only — listed files
@@ -202,11 +220,12 @@ module Rigor
202
220
  "auto_detect" => true
203
221
  }
204
222
  }.freeze
223
+ Ractor.make_shareable(DEFAULTS)
205
224
 
206
225
  # Top-level keys whose values are file/directory paths that MUST be resolved relative to the config
207
226
  # file's directory. `exclude:` is intentionally NOT in this list — its entries are glob patterns
208
227
  # (`**/vendor/**`), not paths.
209
- PATH_KEYS = %w[paths signature_paths pre_eval].freeze
228
+ PATH_KEYS = %w[paths signature_paths test_paths pre_eval].freeze
210
229
  private_constant :PATH_KEYS
211
230
 
212
231
  # Top-level keys this implementation DECLARES but never reads — see `docs/internal-spec/config.md`
@@ -251,7 +270,7 @@ module Rigor
251
270
 
252
271
  attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
253
272
  :cache_validation, :disabled_rules,
254
- :libraries, :signature_paths, :fold_platform_specific_paths, :parameter_inference,
273
+ :libraries, :signature_paths, :test_paths, :fold_platform_specific_paths, :parameter_inference,
255
274
  :plugins_isolation,
256
275
  :plugins_io_network, :plugins_io_allowed_paths,
257
276
  :plugins_io_allowed_url_hosts,
@@ -260,7 +279,7 @@ module Rigor
260
279
  :dependencies, :parallel_workers,
261
280
  :bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
262
281
  :rbs_collection_lockfile, :rbs_collection_auto_detect,
263
- :pre_eval, :baseline_path, :effects,
282
+ :pre_eval, :baseline_path, :effects, :sig_gen_inline_declared,
264
283
  :effects_snapshot_path, :effects_snapshot_reach, :effects_snapshot_gate, :effects_tolerated,
265
284
  :effects_labels, :effects_attribution, :effects_envelopes
266
285
 
@@ -282,6 +301,16 @@ module Rigor
282
301
  @effects_check
283
302
  end
284
303
 
304
+ # The test roots a run uses: the declared `test_paths:` as written (already resolved against the
305
+ # config file's directory by {.load}), or — when the key is unset — whichever of
306
+ # {CONVENTIONAL_TEST_PATHS} is a directory under `root`, as absolute paths. `root` is the directory
307
+ # the run treats as the project root (the CLI's CWD), as for `signature_paths:` auto-detection.
308
+ def resolved_test_paths(root: Dir.pwd)
309
+ return test_paths unless test_paths.nil?
310
+
311
+ CONVENTIONAL_TEST_PATHS.map { |dir| File.absolute_path(dir, root) }.select { |dir| File.directory?(dir) }
312
+ end
313
+
285
314
  # Loads a configuration file.
286
315
  #
287
316
  # `path == nil` triggers auto-discovery against {DISCOVERY_ORDER}. The first present file in that list is
@@ -376,7 +405,7 @@ module Rigor
376
405
  # the include-aware load before applying `--treat-all-as-inline-rbs`'s plugin injection (see
377
406
  # {CLI::CheckCommand#load_check_configuration}).
378
407
  def self.load_with_includes(path, visited: Set.new)
379
- absolute = File.expand_path(path)
408
+ absolute = File.absolute_path(path)
380
409
  raise ConfigurationError, "circular include: #{absolute}" if visited.include?(absolute)
381
410
 
382
411
  raw = read_yaml(absolute)
@@ -405,7 +434,7 @@ module Rigor
405
434
 
406
435
  accumulated = {}
407
436
  includes.each do |inc|
408
- inc_path = File.expand_path(inc.to_s, base_dir)
437
+ inc_path = ConfigPath.absolute(inc.to_s, base_dir)
409
438
  unless File.exist?(inc_path)
410
439
  raise ConfigurationError, "include not found: #{inc.inspect} (referenced from #{base_dir})"
411
440
  end
@@ -431,7 +460,7 @@ module Rigor
431
460
  def self.resolve_path_key!(out, key, base_dir)
432
461
  return unless out.key?(key) && !out[key].nil?
433
462
 
434
- out[key] = Array(out[key]).map { |p| File.expand_path(p.to_s, base_dir) }
463
+ out[key] = Array(out[key]).map { |p| ConfigPath.absolute(p.to_s, base_dir) }
435
464
  end
436
465
 
437
466
  def self.resolve_plugins_io_paths!(out, base_dir)
@@ -439,7 +468,7 @@ module Rigor
439
468
  return unless plugins_io.is_a?(Hash) && plugins_io["allowed_paths"]
440
469
 
441
470
  duped = plugins_io.dup
442
- duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| File.expand_path(p.to_s, base_dir) }
471
+ duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| ConfigPath.absolute(p.to_s, base_dir) }
443
472
  out["plugins_io"] = duped
444
473
  end
445
474
 
@@ -509,6 +538,9 @@ module Rigor
509
538
  @libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
510
539
  sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
511
540
  @signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
541
+ test_paths = data.fetch("test_paths", DEFAULTS.fetch("test_paths"))
542
+ @test_paths = test_paths.nil? ? nil : Array(test_paths).map(&:to_s).freeze
543
+ @sig_gen_inline_declared = coerce_sig_gen_inline_declared(data.fetch("sig_gen", nil))
512
544
  @pre_eval = expand_pre_eval_entries(
513
545
  Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
514
546
  )
@@ -593,6 +625,9 @@ module Rigor
593
625
  "disable" => disabled_rules,
594
626
  "libraries" => libraries,
595
627
  "signature_paths" => signature_paths,
628
+ # `test_paths:` is deliberately absent, as `effects:` is: `to_h` feeds the run cache key and the
629
+ # incremental-snapshot fingerprint, and the test roots change no diagnostic, so editing them must not
630
+ # invalidate either. `sig_gen:` is absent for the same reason: only `rigor sig-gen` reads it.
596
631
  "pre_eval" => pre_eval,
597
632
  "fold_platform_specific_paths" => fold_platform_specific_paths,
598
633
  "parameter_inference" => parameter_inference,
@@ -980,6 +1015,35 @@ module Rigor
980
1015
  VALID_CACHE_VALIDATIONS = %w[auto stat digest].freeze
981
1016
  private_constant :VALID_CACHE_VALIDATIONS
982
1017
 
1018
+ # The `sig_gen.inline_declared:` values, as the Symbols {#sig_gen_inline_declared} answers.
1019
+ VALID_SIG_GEN_INLINE_DECLARED = %w[write skip].freeze
1020
+ private_constant :VALID_SIG_GEN_INLINE_DECLARED
1021
+
1022
+ # Rejects rather than fails soft: an unrecognised value read as `write` would put a second declaration of
1023
+ # every inline-annotated method into `sig/` for exactly the project that asked for none. A `sig_gen:`
1024
+ # block that omits the key, or is written empty, keeps the default.
1025
+ def coerce_sig_gen_inline_declared(section)
1026
+ section = {} if section.nil?
1027
+ raise ConfigurationError, "sig_gen must be a mapping, got #{section.inspect}" unless section.is_a?(Hash)
1028
+
1029
+ # The schema closes this object (`additionalProperties: false`); a misspelled key here would otherwise
1030
+ # load as the default and write exactly what `inline_declare: skip` was meant to prevent.
1031
+ unknown = section.keys.map(&:to_s) - DEFAULTS.fetch("sig_gen").keys
1032
+ unless unknown.empty?
1033
+ raise ConfigurationError,
1034
+ "sig_gen has unknown key(s) #{unknown.inspect}; known keys: #{DEFAULTS.fetch('sig_gen').keys.inspect}"
1035
+ end
1036
+
1037
+ value = section.fetch("inline_declared", DEFAULTS.dig("sig_gen", "inline_declared")).to_s
1038
+ unless VALID_SIG_GEN_INLINE_DECLARED.include?(value)
1039
+ raise ConfigurationError,
1040
+ "sig_gen.inline_declared must be one of #{VALID_SIG_GEN_INLINE_DECLARED.inspect}, " \
1041
+ "got #{section['inline_declared'].inspect}"
1042
+ end
1043
+
1044
+ value.to_sym
1045
+ end
1046
+
983
1047
  def coerce_cache_validation(value)
984
1048
  str = value.to_s
985
1049
  VALID_CACHE_VALIDATIONS.include?(str) ? str : "auto"