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
@@ -3,6 +3,8 @@
3
3
  require "optionparser"
4
4
 
5
5
  require_relative "../configuration"
6
+ require_relative "../effects/config_envelopes"
7
+ require_relative "../effects/registry"
6
8
  require_relative "options"
7
9
  require_relative "../sig_gen"
8
10
  require_relative "command"
@@ -22,21 +24,27 @@ module Rigor
22
24
  # AND the candidate is a `tighter-return`.
23
25
  #
24
26
  # Parameter policy defaults to `untyped`. `--params=observed` (slice 3) opts in to caller-side observation
25
- # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default `spec/` when no flag is given AND a
26
- # `spec/` directory exists), unions per-position arg types, and the generator emits the union per ADR-5 clause 2.
27
+ # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default: the project's test roots,
28
+ # {Configuration#resolved_test_paths}), unions per-position arg types, and the generator emits the union per ADR-5
29
+ # clause 2.
27
30
  # `--params=observed-strict` stays reserved-but-inert until the capability-role catalog ships (rejected with a usage
28
31
  # error so the surface stays stable).
29
- class SigGenCommand < Command
32
+ #
33
+ # `--check` (ADR-112 WD4) is the CI freshness gate: it runs the `--write` merge with every other flag as given,
34
+ # writes nothing, prints what would change, and exits 1 when anything would.
35
+ class SigGenCommand < Command # rubocop:disable Metrics/ClassLength
30
36
  USAGE = "Usage: rigor sig-gen [options] [paths]"
31
37
 
32
- VALID_MODES = %w[print diff write].freeze
38
+ VALID_MODES = %w[print diff write check].freeze
33
39
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
34
40
  VALID_FORMATS = %w[text json].freeze
35
41
 
36
- # The skip reasons {#report_skipped} counts. The two left out each have a detailed report of their own
37
- # ({#report_unrenderable}, {#report_unresolvable_superclasses}), so a method never shows up in two tallies.
42
+ # The skip reasons {#report_skipped} counts. The four left out each have a report of their own
43
+ # ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
44
+ # lines `--write` / `--check` print for `inline_differs`), so a method never shows up in two tallies.
38
45
  SUMMARISED_SKIP_REASONS = (SigGen::Classification::SKIP_DIAGNOSTIC_IDS.keys -
39
- %i[unrenderable_rbs unresolvable_superclass]).freeze
46
+ %i[unrenderable_rbs unresolvable_superclass inline_declared
47
+ inline_differs]).freeze
40
48
  private_constant :SUMMARISED_SKIP_REASONS
41
49
 
42
50
  # @return CLI exit status.
@@ -50,17 +58,22 @@ module Rigor
50
58
  observations = collect_observations(configuration, options)
51
59
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
52
60
  observations: observations,
53
- include_private: options.fetch(:include_private))
61
+ include_private: options.fetch(:include_private),
62
+ effect_annotator: effect_annotator(configuration, paths, options),
63
+ overwrite: options.fetch(:overwrite))
54
64
  candidates = generator.run
55
65
  mode = options.fetch(:mode).to_sym
56
66
 
57
- status = if mode == :write
58
- dispatch_write(candidates, configuration, options)
67
+ status = case mode
68
+ when :write then dispatch_write(candidates, configuration, options)
69
+ when :check then dispatch_check(candidates, configuration, options)
59
70
  else
60
71
  dispatch_print_or_diff(candidates, mode, options)
61
72
  0
62
73
  end
63
74
  report_skipped(candidates, options)
75
+ report_inline_declared(candidates, options)
76
+ report_withheld_annotations(candidates, options)
64
77
  report_unrenderable(generator.unrenderable)
65
78
  report_unresolvable_superclasses(generator.unresolvable_superclasses)
66
79
  status
@@ -68,6 +81,74 @@ module Rigor
68
81
 
69
82
  private
70
83
 
84
+ # ADR-103 WD9 — the effect table sig-gen writes annotations from, or `nil`.
85
+ #
86
+ # The gate is the project's own `effects:` opt-in and nothing else. An annotation is read back as an
87
+ # enforced envelope, so turning emission on from a sig-gen flag alone would let one command commit a
88
+ # project to a contract `rigor check` was never asked to keep; and a project that has not opted in
89
+ # pays neither the analysis this needs nor a single changed byte of output.
90
+ #
91
+ # `--effect-envelopes` is the second, narrower switch: `%a{pure}` is the ecosystem's existing purity
92
+ # spelling and round-trips through Steep as well as Rigor, while `%a{rigor:v1:effect …}` is Rigor's
93
+ # own and belongs in a project's `sig/` only when the author asked for it by name.
94
+ def effect_annotator(configuration, paths, options)
95
+ unless configuration.effects_enabled?
96
+ if options.fetch(:effect_envelopes)
97
+ @err.puts("rigor sig-gen: --effect-envelopes needs the `effects:` opt-in in .rigor.yml; " \
98
+ "no effect annotation was emitted.")
99
+ end
100
+ return nil
101
+ end
102
+
103
+ require_relative "check_runner_factory"
104
+ # Through the same factory `rigor check` and `rigor doctor` use, so the LRU cap, the worker
105
+ # resolution and the tolerated-effects switch cannot drift from the command whose cache this run
106
+ # shares. `workers: 0` because a collecting run is pinned to the sequential path anyway, and
107
+ # `--no-cache` mirrors `rigor check`'s flag of the same name.
108
+ runner = CheckRunnerFactory.build(
109
+ configuration: configuration,
110
+ options: { no_cache: options.fetch(:no_cache), explain: false, stats: false, workers: 0 },
111
+ buffer: nil, cache_root: configuration.cache_path
112
+ )
113
+ runner.run((configuration.paths + paths).uniq)
114
+ SigGen::EffectAnnotation::Annotator.new(
115
+ table: runner.effect_table, envelopes: options.fetch(:effect_envelopes),
116
+ envelope_index: runner.effect_envelopes, config_envelopes: config_envelopes(configuration)
117
+ )
118
+ end
119
+
120
+ # The project's `effects.envelopes:` entries, for gate 0's `match:` half. Built with a plain
121
+ # registry rather than the run's: gate 0 asks only WHETHER an entry selects this class, never what
122
+ # it bounds, so the vocabulary a plugin would add cannot change the answer.
123
+ def config_envelopes(configuration)
124
+ Effects::ConfigEnvelopes.build(
125
+ entries: configuration.effects_envelopes,
126
+ registry: Effects::Registry.for_configuration(configuration)
127
+ )
128
+ rescue StandardError
129
+ []
130
+ end
131
+
132
+ # The withheld half of the emission, counted the way {#report_skipped} counts a skip: a method that
133
+ # did NOT get `%a{pure}` is the interesting case for a reader who expected one, and silence would
134
+ # read as "sig-gen does not do this" rather than "this method did not earn it".
135
+ def report_withheld_annotations(candidates, options)
136
+ return unless options.fetch(:format) == "text"
137
+
138
+ counts = candidates.each_with_object(Hash.new(0)) do |candidate, acc|
139
+ reason = candidate.effect_reason
140
+ acc[reason] += 1 if reason && reason != :emitted
141
+ end
142
+ return if counts.empty?
143
+
144
+ breakdown = counts.map { |reason, n| "#{SigGen::EffectAnnotation::DIAGNOSTIC_IDS.fetch(reason)}: #{n}" }
145
+ @err.puts(
146
+ "rigor sig-gen: withheld an effect annotation from #{counts.values.sum} method(s) " \
147
+ "(#{breakdown.join(', ')}). An annotation is enforced once written, so it is emitted only " \
148
+ "from an exhaustive, undischarged summary."
149
+ )
150
+ end
151
+
71
152
  # Issue #778 — one stderr line per run saying how many methods the generator declined and why, so a
72
153
  # method missing from the output is never a silent absence. Text mode only: under `--format=json` every
73
154
  # skipped row is already in the payload with its `skip_reason`, and stderr stays clean for the consumer.
@@ -90,6 +171,18 @@ module Rigor
90
171
  )
91
172
  end
92
173
 
174
+ # ADR-112 WD4 — `sig_gen.inline_declared: skip` leaving methods out is the project's own choice, not a
175
+ # method sig-gen "could not type or would not overwrite", so it is counted on a line of its own.
176
+ def report_inline_declared(candidates, options)
177
+ return unless options.fetch(:format) == "text"
178
+
179
+ count = candidates.count { |c| c.skip_reason == :inline_declared }
180
+ return if count.zero?
181
+
182
+ @err.puts("rigor sig-gen: left #{count} method(s) declared inline out of sig/, as " \
183
+ "`sig_gen.inline_declared: skip` asks (sig.skipped.inline-declared).")
184
+ end
185
+
93
186
  # A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
94
187
  # code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
95
188
  # staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
@@ -137,30 +230,99 @@ module Rigor
137
230
 
138
231
  # @return exit status — non-zero when a file the user asked to write could not be written.
139
232
  def dispatch_write(candidates, configuration, options)
140
- layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
141
- path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
142
- writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
143
-
144
- results = writer.write_all(candidates)
233
+ results = build_writer(configuration, options).write_all(candidates)
145
234
 
146
- SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
147
- # A refused write (an assembled file that does not parse, or an existing target that is not valid
148
- # UTF-8) means the user asked for a write and did not get one, so the command must not report
149
- # success — a green `sig-gen --write` in CI would otherwise mean nothing.
235
+ refused = refused_candidates(candidates)
236
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
237
+ SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
238
+ # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
239
+ # or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
240
+ # write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
241
+ # would otherwise mean nothing.
150
242
  refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
151
- results.any? { |result| refusals.include?(result.action) } ? 1 : 0
243
+ results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
244
+ end
245
+
246
+ # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
247
+ # pass `--overwrite`. Unlike every other skip, `sig/` contradicts the source while one stands.
248
+ def refused_candidates(candidates)
249
+ candidates.select { |candidate| candidate.skip_reason == :inline_differs }
250
+ end
251
+
252
+ # ADR-112 WD4 — the same writer as {#dispatch_write}, flags included, in dry-run mode. Defined by what
253
+ # `--write` would do rather than by whether `--diff` prints anything: a tighter return against an existing
254
+ # declaration is a proposal `--write` declines without `--overwrite`, and a gate that counted it could
255
+ # never pass on a project that reviewed it and said no. With `--overwrite` it counts, because `--write
256
+ # --overwrite` would apply it.
257
+ #
258
+ # @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
259
+ def dispatch_check(candidates, configuration, options)
260
+ results = build_writer(configuration, options, dry_run: true).write_all(candidates)
261
+ refused = refused_candidates(candidates)
262
+ SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
263
+ refused: refused)
264
+ stale = SigGen::Renderer.out_of_date(results)
265
+ return 0 if stale.empty? && refused.empty?
266
+
267
+ report_check_failure(stale, refused) if options.fetch(:format) == "text"
268
+ 1
269
+ end
270
+
271
+ def report_check_failure(stale, refused)
272
+ unless stale.empty?
273
+ @err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
274
+ "run `rigor sig-gen --write` with the same options to update them.")
275
+ end
276
+ return if refused.empty?
277
+
278
+ @err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
279
+ "disagree; make them agree, or pass --overwrite to replace the sig/ member.")
280
+ end
281
+
282
+ def build_writer(configuration, options, dry_run: false)
283
+ layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
284
+ path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
285
+ SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite), dry_run: dry_run)
152
286
  end
153
287
 
154
288
  # Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
155
- # specified, default to `spec/` (skipped silently when the directory is absent).
289
+ # specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
290
+ # root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
291
+ # naming the reason.
156
292
  def collect_observations(configuration, options)
157
293
  return {} if options.fetch(:params) != "observed"
158
294
 
159
295
  observe_paths = options.fetch(:observe)
160
- observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
296
+ observe_paths = configuration.resolved_test_paths if observe_paths.empty?
297
+ if observe_paths.empty?
298
+ warn_no_test_roots(configuration)
299
+ else
300
+ warn_missing_test_roots(observe_paths)
301
+ end
161
302
  SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
162
303
  end
163
304
 
305
+ # A declared root that does not exist is read as empty, so name it: `sig-gen` is the only command that reads
306
+ # the test roots, and without this the run looks exactly like one whose tests pass no typed arguments.
307
+ def warn_missing_test_roots(observe_paths)
308
+ missing = observe_paths.reject { |path| File.exist?(path) }
309
+ return if missing.empty?
310
+
311
+ consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
312
+ @err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
313
+ "which does not exist#{consequence}. Check `test_paths:` or --observe=PATH.")
314
+ end
315
+
316
+ def warn_no_test_roots(configuration)
317
+ reason = if configuration.test_paths.nil?
318
+ "no spec/ or test/ directory was found and `test_paths:` is unset"
319
+ else
320
+ "`test_paths:` is empty"
321
+ end
322
+ @err.puts("rigor sig-gen: --params=observed has no test roots to observe (#{reason}); every parameter " \
323
+ "stays untyped. Declare `test_paths:` in the configuration or pass --observe=PATH.")
324
+ end
325
+
164
326
  def parse_options
165
327
  options = {
166
328
  mode: "print",
@@ -170,6 +332,8 @@ module Rigor
170
332
  overwrite: false,
171
333
  observe: [],
172
334
  include_private: false,
335
+ effect_envelopes: false,
336
+ no_cache: false,
173
337
  config: nil
174
338
  }
175
339
  build_option_parser(options).parse!(@argv)
@@ -181,23 +345,33 @@ module Rigor
181
345
  nil
182
346
  end
183
347
 
184
- def build_option_parser(options) # rubocop:disable Metrics/AbcSize
348
+ def build_option_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
185
349
  OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
186
350
  opts.banner = USAGE
187
- opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
188
- opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
189
- opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
190
- opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
351
+ opts.on("--print", "Write RBS skeletons to stdout (default)") { select_mode(options, "print") }
352
+ opts.on("--diff", "Write a unified diff against existing RBS") { select_mode(options, "diff") }
353
+ opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { select_mode(options, "write") }
354
+ opts.on("--check", "Exit 1 when --write would change sig/; write nothing") { select_mode(options, "check") }
355
+ opts.on("--overwrite", "Allow tighter-return updates, and inline declarations that disagree with sig/, " \
356
+ "to replace user-authored RBS") do
191
357
  options[:overwrite] = true
192
358
  end
193
359
  opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
194
360
  options[:include_private] = true
195
361
  end
362
+ opts.on("--effect-envelopes", "Also emit %a{rigor:v1:effect ...} for effectful methods " \
363
+ "(requires the effects: opt-in)") do
364
+ options[:effect_envelopes] = true
365
+ end
366
+ opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
367
+ options[:no_cache] = true
368
+ end
196
369
  opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
197
370
  opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
198
371
  options[:params] = value
199
372
  end
200
- opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
373
+ opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable; " \
374
+ "default: the configured test_paths)") do |value|
201
375
  options[:observe] << value
202
376
  end
203
377
  opts.on("--new-files", "Emit only new-file classifications") do
@@ -213,12 +387,19 @@ module Rigor
213
387
  end
214
388
  end
215
389
 
390
+ # A second, different mode flag is a usage error rather than last-one-wins: `--check --write` must not
391
+ # quietly write in a CI job that meant to gate, nor `--write --check` quietly not.
392
+ def select_mode(options, mode)
393
+ options[:mode] = options[:mode_given] && options[:mode] != mode ? "conflict" : mode
394
+ options[:mode_given] = true
395
+ end
396
+
216
397
  def validation_error(options)
217
398
  mode = options.fetch(:mode)
218
399
  format = options.fetch(:format)
219
400
  params = options.fetch(:params)
220
401
 
221
- return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
402
+ return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
222
403
  return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
223
404
  return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
224
405
  if params == "observed-strict"
@@ -74,7 +74,7 @@ module Rigor
74
74
 
75
75
  # The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
76
76
  # up.
77
- SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
77
+ SKILLS_ROOT = File.expand_path("../../../skills", __dir__).freeze
78
78
 
79
79
  # @return CLI exit status.
80
80
  def run
@@ -148,7 +148,6 @@ module Rigor
148
148
  @deep = deep
149
149
  end
150
150
 
151
- # @return the full describe report.
152
151
  def render
153
152
  catalog = catalog_skills
154
153
  state = ProjectStateProbe.new(@root).to_h
@@ -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