rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -13,6 +13,7 @@ require_relative "../inference/scope_indexer"
13
13
  require_relative "../inference/precision_scanner"
14
14
  require_relative "../source/node_children"
15
15
  require_relative "type_of_renderer"
16
+ require_relative "type_of_template_probe"
16
17
  require_relative "command"
17
18
  require_relative "options"
18
19
  require_relative "../project_environment"
@@ -28,10 +29,15 @@ module Rigor
28
29
  # type-of UX (extra flags, watch mode, streaming output) without bloating the CLI shell. Output formatting is
29
30
  # delegated to {TypeOfRenderer}.
30
31
  class TypeOfCommand < Command
32
+ include TypeOfTemplateProbe
33
+
31
34
  USAGE = "Usage: rigor type-of [options] FILE:LINE[:COL] [FILE:LINE[:COL] ...]"
32
35
 
33
- Result = Data.define(:file, :line, :column, :node, :type, :tracer, :enumeration) do
34
- def initialize(enumeration: nil, **rest)
36
+ # `location_mapper` is set for a template unit's result (#1040): the `--trace` fallback locations are
37
+ # positions in the COMPILED Ruby, and the mapper turns one into the `[template_line, column_or_nil]`
38
+ # the user can find in the file they named.
39
+ Result = Data.define(:file, :line, :column, :node, :type, :tracer, :enumeration, :location_mapper) do
40
+ def initialize(enumeration: nil, location_mapper: nil, **rest)
35
41
  super
36
42
  end
37
43
  end
@@ -106,6 +112,14 @@ module Rigor
106
112
  physical = buffer ? buffer.resolve(file) : file
107
113
  return 1 unless file_exists?(buffer ? physical : file)
108
114
 
115
+ unit_probe = template_unit_probe(file, base_scope.environment, buffer)
116
+ return unit_probe if unit_probe.is_a?(Integer)
117
+
118
+ if unit_probe
119
+ context = { configuration: configuration, base_scope: base_scope, options: options }
120
+ return resolve_template(file, physical, unit_probe, indexed_targets, context)
121
+ end
122
+
109
123
  source = File.read(physical)
110
124
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
111
125
  return 1 if parse_errors?(parse_result, file)
@@ -191,11 +205,11 @@ module Rigor
191
205
  end
192
206
  end
193
207
 
194
- def type_result(file, line, column, node, scope_index, options, enumeration: nil)
208
+ # `presentation` is the Result's optional `enumeration:` / `location_mapper:`.
209
+ def type_result(file, line, column, node, scope_index, options, **presentation)
195
210
  tracer = options[:trace] ? Inference::FallbackTracer.new : nil
196
211
  type = scope_index[node].type_of(node, tracer: tracer)
197
- Result.new(file: file, line: line, column: column, node: node, type: type, tracer: tracer,
198
- enumeration: enumeration)
212
+ Result.new(file: file, line: line, column: column, node: node, type: type, tracer: tracer, **presentation)
199
213
  end
200
214
 
201
215
  # Builds the plugin-aware environment relative to the probed file, so the reported type matches what `rigor
@@ -84,7 +84,7 @@ module Rigor
84
84
  @out.puts("fallbacks: none")
85
85
  else
86
86
  @out.puts("fallbacks (#{tracer.size}):")
87
- tracer.each { |event| @out.puts(" - #{format_fallback_text(event, result.file)}") }
87
+ tracer.each { |event| @out.puts(" - #{format_fallback_text(event, result)}") }
88
88
  end
89
89
  end
90
90
 
@@ -118,21 +118,31 @@ module Rigor
118
118
  type: result.type.describe,
119
119
  erased: result.type.erase_to_rbs
120
120
  }
121
- payload[:fallbacks] = result.tracer.map { |event| fallback_to_h(event) } if result.tracer
121
+ payload[:fallbacks] = result.tracer.map { |event| fallback_to_h(event, result) } if result.tracer
122
122
  payload
123
123
  end
124
124
 
125
- def format_fallback_text(event, file)
126
- "#{event.node_class} (#{event.family}) @ #{location_text(event.location, file)}"
125
+ def format_fallback_text(event, result)
126
+ "#{event.node_class} (#{event.family}) @ #{location_text(event.location, result)}"
127
127
  end
128
128
 
129
- def location_text(location, file)
129
+ def location_text(location, result)
130
130
  return "<no location>" unless location.respond_to?(:start_line)
131
131
 
132
- "#{file}:#{location.start_line}:#{location.start_column + 1}"
132
+ line, column = fallback_position(location, result)
133
+ column ? "#{result.file}:#{line}:#{column}" : "#{result.file}:#{line}"
133
134
  end
134
135
 
135
- def fallback_to_h(event)
136
+ # A template unit's result (#1040) maps the compiled location back; a column that names nothing in the
137
+ # template is left out rather than printed as a column of bytes the user never wrote.
138
+ def fallback_position(location, result)
139
+ mapper = result.location_mapper
140
+ return [location.start_line, location.start_column + 1] if mapper.nil?
141
+
142
+ mapper.call(location)
143
+ end
144
+
145
+ def fallback_to_h(event, result)
136
146
  hash = {
137
147
  node_class: event.node_class.name,
138
148
  family: event.family,
@@ -140,8 +150,9 @@ module Rigor
140
150
  }
141
151
  location = event.location
142
152
  if location.respond_to?(:start_line)
143
- hash[:line] = location.start_line
144
- hash[:column] = location.start_column + 1
153
+ line, column = fallback_position(location, result)
154
+ hash[:line] = line
155
+ hash[:column] = column if column
145
156
  end
146
157
  hash
147
158
  end
@@ -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
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.expand_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
 
@@ -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
 
@@ -49,6 +49,9 @@ module Rigor
49
49
  **/node_modules/**
50
50
  ].freeze
51
51
 
52
+ # The conventional test roots `test_paths: nil` auto-detects, in this order.
53
+ CONVENTIONAL_TEST_PATHS = %w[spec test].freeze
54
+
52
55
  DEFAULTS = {
53
56
  "target_ruby" => "4.0",
54
57
  "paths" => ["lib"],
@@ -57,6 +60,20 @@ module Rigor
57
60
  "disable" => [],
58
61
  "libraries" => [],
59
62
  "signature_paths" => nil,
63
+ # The project's test roots: the directories (or files) holding its tests. `nil` (the default) auto-detects
64
+ # whichever of `spec/` and `test/` exist, the same way `signature_paths: nil` auto-detects `sig/`; `[]`
65
+ # declares that the project has none. `rigor sig-gen --params=observed` harvests call-site observations
66
+ # from them when no `--observe=PATH` is given. See {#resolved_test_paths}.
67
+ "test_paths" => nil,
68
+ # ADR-112 WD4 — `rigor sig-gen` settings. `inline_declared:` decides what happens to a method the inline
69
+ # reader (`rigor-rbs-inline`) already declares from `# @rbs` / `#:`. `write` (the default) copies the
70
+ # inline declaration into `sig/`, so the generated signature is the complete contract a gem ships;
71
+ # `skip` leaves every member that reader declares out of `sig/`, for a project whose Steep reads the
72
+ # same annotations (`inline: true` beside `signature "sig"`), where the copy would be a second
73
+ # declaration of one method (`DuplicatedMethodDefinition`). See {#sig_gen_inline_declared}.
74
+ "sig_gen" => {
75
+ "inline_declared" => "write"
76
+ },
60
77
  # ADR-17 — project-side monkey-patch pre-evaluation. Empty by default; users opt in by listing explicit
61
78
  # files that the analyzer walks before per-file inference so patched-method declarations are visible
62
79
  # across the project (e.g. `lib/core_ext/string_extensions.rb`). Slice 1 plumbing only — listed files
@@ -202,11 +219,12 @@ module Rigor
202
219
  "auto_detect" => true
203
220
  }
204
221
  }.freeze
222
+ Ractor.make_shareable(DEFAULTS)
205
223
 
206
224
  # Top-level keys whose values are file/directory paths that MUST be resolved relative to the config
207
225
  # file's directory. `exclude:` is intentionally NOT in this list — its entries are glob patterns
208
226
  # (`**/vendor/**`), not paths.
209
- PATH_KEYS = %w[paths signature_paths pre_eval].freeze
227
+ PATH_KEYS = %w[paths signature_paths test_paths pre_eval].freeze
210
228
  private_constant :PATH_KEYS
211
229
 
212
230
  # Top-level keys this implementation DECLARES but never reads — see `docs/internal-spec/config.md`
@@ -251,7 +269,7 @@ module Rigor
251
269
 
252
270
  attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
253
271
  :cache_validation, :disabled_rules,
254
- :libraries, :signature_paths, :fold_platform_specific_paths, :parameter_inference,
272
+ :libraries, :signature_paths, :test_paths, :fold_platform_specific_paths, :parameter_inference,
255
273
  :plugins_isolation,
256
274
  :plugins_io_network, :plugins_io_allowed_paths,
257
275
  :plugins_io_allowed_url_hosts,
@@ -260,7 +278,7 @@ module Rigor
260
278
  :dependencies, :parallel_workers,
261
279
  :bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
262
280
  :rbs_collection_lockfile, :rbs_collection_auto_detect,
263
- :pre_eval, :baseline_path, :effects,
281
+ :pre_eval, :baseline_path, :effects, :sig_gen_inline_declared,
264
282
  :effects_snapshot_path, :effects_snapshot_reach, :effects_snapshot_gate, :effects_tolerated,
265
283
  :effects_labels, :effects_attribution, :effects_envelopes
266
284
 
@@ -282,6 +300,16 @@ module Rigor
282
300
  @effects_check
283
301
  end
284
302
 
303
+ # The test roots a run uses: the declared `test_paths:` as written (already resolved against the
304
+ # config file's directory by {.load}), or — when the key is unset — whichever of
305
+ # {CONVENTIONAL_TEST_PATHS} is a directory under `root`, as absolute paths. `root` is the directory
306
+ # the run treats as the project root (the CLI's CWD), as for `signature_paths:` auto-detection.
307
+ def resolved_test_paths(root: Dir.pwd)
308
+ return test_paths unless test_paths.nil?
309
+
310
+ CONVENTIONAL_TEST_PATHS.map { |dir| File.expand_path(dir, root) }.select { |dir| File.directory?(dir) }
311
+ end
312
+
285
313
  # Loads a configuration file.
286
314
  #
287
315
  # `path == nil` triggers auto-discovery against {DISCOVERY_ORDER}. The first present file in that list is
@@ -509,6 +537,9 @@ module Rigor
509
537
  @libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
510
538
  sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
511
539
  @signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
540
+ test_paths = data.fetch("test_paths", DEFAULTS.fetch("test_paths"))
541
+ @test_paths = test_paths.nil? ? nil : Array(test_paths).map(&:to_s).freeze
542
+ @sig_gen_inline_declared = coerce_sig_gen_inline_declared(data.fetch("sig_gen", nil))
512
543
  @pre_eval = expand_pre_eval_entries(
513
544
  Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
514
545
  )
@@ -593,6 +624,9 @@ module Rigor
593
624
  "disable" => disabled_rules,
594
625
  "libraries" => libraries,
595
626
  "signature_paths" => signature_paths,
627
+ # `test_paths:` is deliberately absent, as `effects:` is: `to_h` feeds the run cache key and the
628
+ # incremental-snapshot fingerprint, and the test roots change no diagnostic, so editing them must not
629
+ # invalidate either. `sig_gen:` is absent for the same reason: only `rigor sig-gen` reads it.
596
630
  "pre_eval" => pre_eval,
597
631
  "fold_platform_specific_paths" => fold_platform_specific_paths,
598
632
  "parameter_inference" => parameter_inference,
@@ -980,6 +1014,35 @@ module Rigor
980
1014
  VALID_CACHE_VALIDATIONS = %w[auto stat digest].freeze
981
1015
  private_constant :VALID_CACHE_VALIDATIONS
982
1016
 
1017
+ # The `sig_gen.inline_declared:` values, as the Symbols {#sig_gen_inline_declared} answers.
1018
+ VALID_SIG_GEN_INLINE_DECLARED = %w[write skip].freeze
1019
+ private_constant :VALID_SIG_GEN_INLINE_DECLARED
1020
+
1021
+ # Rejects rather than fails soft: an unrecognised value read as `write` would put a second declaration of
1022
+ # every inline-annotated method into `sig/` for exactly the project that asked for none. A `sig_gen:`
1023
+ # block that omits the key, or is written empty, keeps the default.
1024
+ def coerce_sig_gen_inline_declared(section)
1025
+ section = {} if section.nil?
1026
+ raise ConfigurationError, "sig_gen must be a mapping, got #{section.inspect}" unless section.is_a?(Hash)
1027
+
1028
+ # The schema closes this object (`additionalProperties: false`); a misspelled key here would otherwise
1029
+ # load as the default and write exactly what `inline_declare: skip` was meant to prevent.
1030
+ unknown = section.keys.map(&:to_s) - DEFAULTS.fetch("sig_gen").keys
1031
+ unless unknown.empty?
1032
+ raise ConfigurationError,
1033
+ "sig_gen has unknown key(s) #{unknown.inspect}; known keys: #{DEFAULTS.fetch('sig_gen').keys.inspect}"
1034
+ end
1035
+
1036
+ value = section.fetch("inline_declared", DEFAULTS.dig("sig_gen", "inline_declared")).to_s
1037
+ unless VALID_SIG_GEN_INLINE_DECLARED.include?(value)
1038
+ raise ConfigurationError,
1039
+ "sig_gen.inline_declared must be one of #{VALID_SIG_GEN_INLINE_DECLARED.inspect}, " \
1040
+ "got #{section['inline_declared'].inspect}"
1041
+ end
1042
+
1043
+ value.to_sym
1044
+ end
1045
+
983
1046
  def coerce_cache_validation(value)
984
1047
  str = value.to_s
985
1048
  VALID_CACHE_VALIDATIONS.include?(str) ? str : "auto"