rigortype 0.3.8 → 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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "classification"
4
+
5
+ module Rigor
6
+ module SigGen
7
+ # Answers for the `sig.skipped.*` identifiers `rigor sig-gen` prints when it declines to emit a
8
+ # signature for a method.
9
+ #
10
+ # ADR-108 WD4 — a skip id is an ANSWER ("Rigor cannot prove a type here"), and the command that exists
11
+ # to answer identifiers must be able to answer it. `rigor explain` resolves against
12
+ # {Analysis::RuleCatalog}, which carries diagnostic rules only, so `rigor explain
13
+ # sig.skipped.untyped-return` used to reply `Unknown rule` for an id Rigor had just printed. This is the
14
+ # second catalogue `explain` consults; it is deliberately separate from the rule catalog because a skip
15
+ # reason has no severity, no profile mapping and no suppression, and forcing it into {RuleCatalog::Entry}
16
+ # would mean inventing four fields that do not apply.
17
+ #
18
+ # Every id in {Classification::SKIP_DIAGNOSTIC_IDS} MUST have an entry here; the gate is
19
+ # `spec/rigor/sig_gen/skip_reason_catalog_spec.rb`.
20
+ module SkipReasonCatalog
21
+ class Entry < Data.define(:id, :summary, :explanation, :next_step)
22
+ # Hash-shaped form for `rigor explain --format=json`. String keys, and `kind` distinguishes a skip
23
+ # reason from a rule entry in a stream a consumer may receive either from.
24
+ def to_h
25
+ {
26
+ "id" => id,
27
+ "kind" => "sig_skip_reason",
28
+ "summary" => summary,
29
+ "explanation" => explanation,
30
+ "next_step" => next_step
31
+ }
32
+ end
33
+ end
34
+
35
+ ENTRIES = {
36
+ "sig.skipped.untyped-return" => Entry.new(
37
+ id: "sig.skipped.untyped-return",
38
+ summary: "The method's return types as Dynamic[top], so no signature was written.",
39
+ explanation: "Rigor inferred the body but the value it returns has no proven type — the last " \
40
+ "expression widened to Dynamic[top] somewhere up its chain. Emitting `untyped` here " \
41
+ "would record noise as a contract: the file would gain a signature that says nothing " \
42
+ "and stops a later, better inference from being noticed. This is the commonest skip " \
43
+ "reason on a project whose dependencies have no RBS.",
44
+ next_step: "Do not write the return type by hand — the skip is the finding. Trace the returned " \
45
+ "expression back to where it became dynamic (`rigor annotate PATH` reads the `#=>` " \
46
+ "column, `rigor trace --format=json --line=N` for one line) and close THAT: install or " \
47
+ "write RBS for the dependency, enable the plugin for the framework, or report an " \
48
+ "engine gap."
49
+ ),
50
+ "sig.skipped.user-authored" => Entry.new(
51
+ id: "sig.skipped.user-authored",
52
+ summary: "An RBS declaration for this method already exists and was left alone.",
53
+ explanation: "Not a gap. The project already states a type for this member, and sig-gen does not " \
54
+ "overwrite a hand-authored contract without being told to. Where `sig/` and an inline " \
55
+ "annotation both declare the member, ADR-112 WD5's consistency rule decides which binds.",
56
+ next_step: "Read the existing declaration. If it disagrees with what Rigor infers, raise the " \
57
+ "disagreement rather than silently retyping it; pass `--overwrite` only once you have " \
58
+ "decided the generated type is the better contract."
59
+ ),
60
+ "sig.skipped.unrenderable-rbs" => Entry.new(
61
+ id: "sig.skipped.unrenderable-rbs",
62
+ summary: "Rigor rendered a line that `rbs` itself rejects, so the method was dropped.",
63
+ explanation: "A defect in Rigor's renderer, not in your code. The method is skipped rather than " \
64
+ "written so one pathological signature cannot make the whole generated file " \
65
+ "unparseable, and the count is reported so the defect is not silent.",
66
+ next_step: "Report it, with the method and the file. `rigor sig-gen --print PATH` shows what was " \
67
+ "generated for the rest of the file and is the useful attachment."
68
+ ),
69
+ "sig.skipped.complex-shape" => Entry.new(
70
+ id: "sig.skipped.complex-shape",
71
+ summary: "Reserved for a shape too complex to render; no code path produces it today.",
72
+ explanation: "The identifier is part of the reserved `sig.skipped.*` family and is carried so the " \
73
+ "family stays stable, but no generator path emits it.",
74
+ next_step: "If you have actually seen this id in output, that is itself worth reporting — it means " \
75
+ "a path exists that this catalogue does not describe."
76
+ ),
77
+ "sig.skipped.unresolvable-superclass" => Entry.new(
78
+ id: "sig.skipped.unresolvable-superclass",
79
+ summary: "The class's superclass chain does not end in a class the RBS environment knows.",
80
+ explanation: "Writing the declaration would make things worse rather than better: RBS would read " \
81
+ "the class as rooted at an unknown parent and collapse it on the next run, losing the " \
82
+ "members it can resolve today.",
83
+ next_step: "Give the environment the missing ancestor — install or write RBS for the gem that " \
84
+ "defines it, or add the project file that declares it to the analysed paths — then " \
85
+ "re-run `rigor sig-gen`."
86
+ ),
87
+ "sig.skipped.inline-declared" => Entry.new(
88
+ id: "sig.skipped.inline-declared",
89
+ summary: "The member is declared inline, and `sig_gen.inline_declared: skip` keeps it out of `sig/`.",
90
+ explanation: "Not a gap. The project asked sig-gen to leave every method its inline `# @rbs` / `#:` " \
91
+ "reader declares out of `sig/`, the setting for a project whose Steep reads the same " \
92
+ "annotations (`inline: true` beside `signature \"sig\"`), where a `sig/` copy would " \
93
+ "declare the method twice (`DuplicatedMethodDefinition`). Rigor still reads the inline " \
94
+ "declaration when it analyses the source; a consumer reading only the shipped `sig/` " \
95
+ "does not see this method.",
96
+ next_step: "Nothing, if the setting is intended. To ship the member in `sig/`, remove " \
97
+ "`sig_gen.inline_declared: skip` from `.rigor.yml` and stop Steep from reading the " \
98
+ "annotations of the files you generate for."
99
+ ),
100
+ "sig.skipped.inline-generic-class" => Entry.new(
101
+ id: "sig.skipped.inline-generic-class",
102
+ summary: "The class is generic by an inline declaration, and sig-gen does not write type parameters.",
103
+ explanation: "The class (or one it is nested in) takes type parameters from `# @rbs generic`, and " \
104
+ "`sig/` either does not declare it yet or declares it with differently named parameters. " \
105
+ "sig-gen would have to open it with a header that has no type parameters, which rbs " \
106
+ "rejects (`GenericParameterMismatchError`) — the class, and every class whose signature " \
107
+ "mentions it, would fail its definition build and read `Dynamic[top]` — or write members " \
108
+ "whose `T` the `sig/` declaration does not bind. The inline declaration still binds when " \
109
+ "the source is analysed.",
110
+ next_step: "Declare the class in `sig/` with the same type parameters as the inline declaration " \
111
+ "(`class Box[T]` ... `end`) and re-run; sig-gen then writes the members into it."
112
+ ),
113
+ "sig.skipped.inline-differs" => Entry.new(
114
+ id: "sig.skipped.inline-differs",
115
+ summary: "The method's inline declaration and its `sig/` declaration disagree; neither was changed.",
116
+ explanation: "The method is declared inline by `# @rbs` / `#:` and in `sig/` (a `def` or an `attr_*`), " \
117
+ "and the two do not state the same types, or `sig/` lacks an annotation written inline. " \
118
+ "Parameter names, union spelling and a `::` that does not change what a name resolves to " \
119
+ "are ignored; overload order is not, because RBS answers a call with the first overload " \
120
+ "that matches. For a parameter-only annotation (`# @rbs name: T`, no `return:`) only the " \
121
+ "parameters are compared, and the return follows the ordinary proposal rules. sig-gen " \
122
+ "does not presume either side right, so it refuses, and `--write` and `--check` exit 1.",
123
+ next_step: "Decide which declaration is right. To keep the inline one, re-run with `--overwrite`, which " \
124
+ "replaces the whole `sig/` member with it (an overload written only in `sig/` is dropped); " \
125
+ "to keep the `sig/` one, edit the annotation to match, or delete it. A parameter-only " \
126
+ "annotation whose parameters differ stays refused under `--overwrite`, because its return " \
127
+ "would be inferred under the `sig/` parameters: delete the `sig/` member and re-run, or add " \
128
+ "`# @rbs return:` so nothing is inferred."
129
+ ),
130
+ "sig.skipped.overridden-by-unsigned-subclass" => Entry.new(
131
+ id: "sig.skipped.overridden-by-unsigned-subclass",
132
+ summary: "A subclass overrides this method and its override was not itself emitted.",
133
+ explanation: "A declaration on the parent is inherited by every subclass that does not redeclare " \
134
+ "the member. Emitting it here would hand the subclass a type describing the parent's " \
135
+ "body, not the override the subclass actually runs.",
136
+ next_step: "Generate signatures for the overriding subclass too — widen the `sig-gen` path set to " \
137
+ "cover it — and the parent's declaration becomes safe to emit."
138
+ )
139
+ }.freeze
140
+
141
+ module_function
142
+
143
+ # @param token — a `sig.skipped.*` identifier.
144
+ # @return the matching entry, or nil when the token is not a known skip reason.
145
+ def resolve(token)
146
+ ENTRIES[token.to_s]
147
+ end
148
+
149
+ def all
150
+ ENTRIES.values
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module SigGen
5
+ # Renders a recorded superclass token into the spelling that goes on an emitted `class` header.
6
+ #
7
+ # Issue [#609](https://github.com/rigortype/rigor/issues/609): every token that survives
8
+ # {Generator#demote_unresolvable_superclasses} names a class the RBS environment or this run declares at an
9
+ # ABSOLUTE position — both guards ask `Reflection.rbs_class_known?` and the emitted-name set, and both are
10
+ # keyed by fully qualified name. The writer then emitted that token bare inside a nested `module` tree, where
11
+ # RBS resolves its first segment relatively against the enclosing namespaces, so `class Show < Help::Show`
12
+ # written under `module Views; module Help` re-points at `::Views::Help::Show` — the class being declared.
13
+ # The self-referential ancestry is what made `AncestorBuilder#singleton_ancestors` recurse until the stack
14
+ # was exhausted on the reporter's project, and what `RBS::RecursiveAncestorError` collapses the class over on
15
+ # a smaller one. Anchoring the token with `::` says the name the guards already checked.
16
+ module SuperclassSpelling
17
+ module_function
18
+
19
+ # @param token — a recorded superclass token, possibly carrying type arguments (`Foo::Bar[untyped]`).
20
+ def absolute(token)
21
+ return token if token.nil? || token.start_with?("::")
22
+
23
+ "::#{token}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -20,9 +20,7 @@ module Rigor
20
20
  # The module is sig-gen-local; the broader question of whether the inference engine itself should always
21
21
  # construct generics with explicit type_args is queued as an ADR-14 follow-up.
22
22
  module TypeElaborator
23
- # @param type [Rigor::Type]
24
- # @param environment [Rigor::Environment]
25
- # @return [Rigor::Type] same shape with bare generic
23
+ # @return same shape with bare generic
26
24
  # nominals filled in.
27
25
  def self.elaborate(type, environment:)
28
26
  arity_cache = {}
@@ -13,23 +13,32 @@ module Rigor
13
13
  # - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root` /
14
14
  # `:skipped_invalid_rbs` / `:skipped_invalid_encoding`.
15
15
  # - `applied` — the {MethodCandidate}s that actually landed on disk.
16
+ # - `replaced` — the subset of `applied` that replaced an existing declaration rather than adding one
17
+ # (`--overwrite`'s tighter returns, and inline updates).
16
18
  # - `skipped` — the {MethodCandidate}s the writer declined (e.g. tighter-return without `--overwrite`). Each
17
19
  # entry pairs the candidate with a skip reason keyword (`:user_authored`).
20
+ # - `left_unreadable` — the {MethodCandidate}s whose effect annotation was NOT written because the
21
+ # target declaration already carries annotations, so its bytes were left alone
22
+ # (`sig.effect.left-unreadable`; see {Writer#splice_annotations}). The signature line itself was
23
+ # still updated.
18
24
  # - `error` — the refusal cause, when `action` is a refusal: for `:skipped_invalid_rbs` the file the writer
19
25
  # assembled does not parse, so it was NOT written (writing it would poison the project's sig tree — the
20
26
  # consumer quarantines an unparseable `.rbs`, taking every other type in that file down with it); for
21
27
  # `:skipped_invalid_encoding` the EXISTING target file is not valid UTF-8, so the writer refuses to merge
22
28
  # into content it cannot read faithfully.
23
29
  class WriteResult
24
- attr_reader :source_path, :target_path, :action, :applied, :skipped, :error
30
+ attr_reader :source_path, :target_path, :action, :applied, :skipped, :error, :left_unreadable, :replaced
25
31
 
26
- def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil)
32
+ def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil, # rubocop:disable Metrics/ParameterLists
33
+ left_unreadable: [], replaced: [])
27
34
  @source_path = source_path
28
35
  @target_path = target_path
29
36
  @action = action
30
37
  @applied = applied.freeze
31
38
  @skipped = skipped.freeze
32
39
  @error = error
40
+ @left_unreadable = left_unreadable.freeze
41
+ @replaced = replaced.freeze
33
42
  freeze
34
43
  end
35
44
 
@@ -40,7 +49,14 @@ module Rigor
40
49
  action: action.to_s,
41
50
  applied: applied.map(&:to_h),
42
51
  skipped: skipped.map { |c, reason| c.to_h.merge(write_skip_reason: reason.to_s) }
43
- }.tap { |h| h[:error] = error if error }
52
+ }.tap do |h|
53
+ h[:error] = error if error
54
+ # Absent when nothing was replaced, so a run that only adds lines keeps its pre-#1076 payload.
55
+ h[:replaced] = replaced.map(&:to_h) unless replaced.empty?
56
+ # Absent rather than empty when nothing was left: an effects-off payload stays byte-identical
57
+ # to a pre-#391 one.
58
+ h[:effect_left_unreadable] = left_unreadable.map(&:to_h) unless left_unreadable.empty?
59
+ end
44
60
  end
45
61
  end
46
62
  end
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "rbs"
5
5
 
6
6
  require_relative "classification"
7
+ require_relative "superclass_spelling"
7
8
  require_relative "write_result"
8
9
 
9
10
  module Rigor
@@ -30,12 +31,16 @@ module Rigor
30
31
 
31
32
  # Per-`update_existing` accumulator. The merge_class helper mutates `source` / `decls` / `applied` /
32
33
  # `skipped` in place as each class is processed so the next class sees the latest byte positions.
33
- MergeState = Struct.new(:source, :decls, :applied, :skipped, keyword_init: true)
34
+ MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, keyword_init: true)
34
35
  private_constant :MergeState
35
36
 
36
- def initialize(path_mapper:, overwrite: false)
37
+ # @param dry_run — assemble and validate every target exactly as a write would, and report it, without
38
+ # creating or changing a file. `sig-gen --check` (ADR-112 WD4) is this, so it can only ever disagree
39
+ # with `--write` about the disk, never about the content.
40
+ def initialize(path_mapper:, overwrite: false, dry_run: false)
37
41
  @path_mapper = path_mapper
38
42
  @overwrite = overwrite
43
+ @dry_run = dry_run
39
44
  # Run-level (cross-file) namespace-kind view, populated per `#write_all` from every candidate's per-file
40
45
  # map. Empty until then so the single-target `#write` path falls back to per-candidate kinds only.
41
46
  @global_namespace_kinds = {}
@@ -50,8 +55,7 @@ module Rigor
50
55
  # ADR-14 follow-up: this is the consolidated-layout entry point. The legacy `write(source_path,
51
56
  # candidates)` below assumes all candidates share a target and remains for spec convenience.
52
57
  #
53
- # @param candidates [Array<MethodCandidate>]
54
- # @return [Array<WriteResult>] one per target sig file.
58
+ # @return one per target sig file.
55
59
  def write_all(candidates)
56
60
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
57
61
  return [] if emittable.empty?
@@ -62,12 +66,10 @@ module Rigor
62
66
  .map { |target, group| write_target(target, group) }
63
67
  end
64
68
 
65
- # @param source_path [String]
66
- # @param candidates [Array<MethodCandidate>] only
69
+ # @param candidates — only
67
70
  # emittable classifications (new-method /
68
71
  # tighter-return) are honoured; the caller is
69
72
  # responsible for filtering.
70
- # @return [WriteResult]
71
73
  def write(source_path, candidates)
72
74
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
73
75
  return WriteResult.new(source_path: source_path, target_path: nil, action: :noop) if emittable.empty?
@@ -76,6 +78,15 @@ module Rigor
76
78
  write_target(target, emittable, source_path: source_path)
77
79
  end
78
80
 
81
+ # Whether `proposed` has STRICTLY FEWER bare `untyped` tokens than `existing`. A count, not a slot-by-slot
82
+ # comparison: it reads as "at least one more position is concrete", and does not prove that no concrete
83
+ # position became `untyped` elsewhere. Word-boundary matching counts `untyped` only as a type token, not
84
+ # as a substring inside identifiers. Public because the generator asks the same question before it lists a
85
+ # declared `initialize` at all (#1436).
86
+ def self.fewer_untyped?(proposed, existing)
87
+ proposed.scan(/\buntyped\b/).size < existing.scan(/\buntyped\b/).size
88
+ end
89
+
79
90
  private
80
91
 
81
92
  # Shared per-target write path used by both `#write` and `#write_all`. Picks a representative
@@ -93,7 +104,7 @@ module Rigor
93
104
  # Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
94
105
  # `:new_file` row (the generator does not classify one), and admitting it here would let a
95
106
  # classification with no writer behind it decide that a target file must be created.
96
- EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN].freeze
107
+ EMITTABLE = [Classification::NEW_METHOD, Classification::TIGHTER_RETURN, Classification::INLINE_OVERWRITE].freeze
97
108
  private_constant :EMITTABLE
98
109
 
99
110
  def inside_sig_root?(target)
@@ -118,8 +129,10 @@ module Rigor
118
129
  action: :skipped_invalid_rbs, error: error)
119
130
  end
120
131
 
121
- FileUtils.mkdir_p(target.dirname)
122
- target.write(content)
132
+ unless @dry_run
133
+ FileUtils.mkdir_p(target.dirname)
134
+ target.write(content)
135
+ end
123
136
  WriteResult.new(source_path: source_path, target_path: target,
124
137
  action: :created, applied: candidates)
125
138
  end
@@ -247,7 +260,7 @@ module Rigor
247
260
 
248
261
  def render_tree_node_body(node, kinds, supers, depth, prefix)
249
262
  inner_indent = INDENT * (depth + 1)
250
- method_lines = node[:methods].map { |c| "#{inner_indent}#{c.rbs}\n" }.join
263
+ method_lines = node[:methods].flat_map { |c| c.rbs_lines.map { |line| "#{inner_indent}#{line}\n" } }.join
251
264
  child_blocks = node[:children].values.map do |child|
252
265
  render_tree_node(child, kinds, supers, depth + 1, prefix + [node[:name]])
253
266
  end.join
@@ -261,7 +274,7 @@ module Rigor
261
274
  return "" unless keyword == :class
262
275
 
263
276
  superclass = supers[qualified]
264
- superclass ? " < #{superclass}" : ""
277
+ superclass ? " < #{SuperclassSpelling.absolute(superclass)}" : ""
265
278
  end
266
279
 
267
280
  # Per ADR-14 gap-#3 (a) the keyword for a segment comes from `namespace_kinds` when known. The default for
@@ -293,13 +306,15 @@ module Rigor
293
306
  decls = parse_signature(source)
294
307
  return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
295
308
 
296
- state = MergeState.new(source: source, decls: decls, applied: [], skipped: [])
309
+ state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
310
+ replaced: [])
297
311
  merge_candidates(state, candidates)
298
312
 
299
313
  action = state.applied.empty? ? :noop : :updated
300
314
  unless action == :updated
301
315
  return WriteResult.new(source_path: source_path, target_path: target, action: action,
302
- applied: state.applied, skipped: state.skipped)
316
+ applied: state.applied, skipped: state.skipped,
317
+ left_unreadable: state.left_unreadable)
303
318
  end
304
319
 
305
320
  # The merge splices text into an existing file by byte offset, so a bug here can produce a file neither
@@ -312,9 +327,10 @@ module Rigor
312
327
  action: :skipped_invalid_rbs, error: error)
313
328
  end
314
329
 
315
- target.write(state.source)
330
+ target.write(state.source) unless @dry_run
316
331
  WriteResult.new(source_path: source_path, target_path: target,
317
- action: action, applied: state.applied, skipped: state.skipped)
332
+ action: action, applied: state.applied, skipped: state.skipped,
333
+ left_unreadable: state.left_unreadable, replaced: state.replaced)
318
334
  end
319
335
 
320
336
  # Applies every class group, then every requested shell, then normalises the layout the three steps
@@ -546,7 +562,7 @@ module Rigor
546
562
  state.applied.concat(methods)
547
563
  insert_namespace_chain(state, class_name, kinds, supers, methods)
548
564
  else
549
- state.source = merge_into_existing_class(state.source, decl, methods, state.applied, state.skipped)
565
+ state.source = merge_into_existing_class(state.source, decl, methods, state)
550
566
  state.decls = parse_signature(state.source) || state.decls
551
567
  end
552
568
  end
@@ -577,24 +593,29 @@ module Rigor
577
593
  source.end_with?("\n")
578
594
  end
579
595
 
580
- def merge_into_existing_class(source, decl, methods, applied, skipped)
596
+ def merge_into_existing_class(source, decl, methods, state)
581
597
  existing_pairs = collect_member_pairs(decl)
582
598
  new_methods, conflicting = partition_against_existing(methods, existing_pairs)
583
599
 
584
600
  source = insert_into_class(source, decl, new_methods)
585
- applied.concat(new_methods)
601
+ state.applied.concat(new_methods)
586
602
 
587
603
  if @overwrite
588
- source, replaced = replace_eligible_conflicts(source, decl, conflicting)
589
- applied.concat(replaced)
590
- skipped.concat(conflicting.reject { |c| replaced.include?(c) }.map { |c| [c, :user_authored] })
604
+ source, replaced = replace_eligible_conflicts(source, decl, conflicting, state)
605
+ state.applied.concat(replaced)
606
+ state.replaced.concat(replaced)
607
+ state.skipped.concat(conflicting.reject { |c| replaced.include?(c) }.map { |c| [c, :user_authored] })
591
608
  else
592
- skipped.concat(conflicting.map { |c| [c, :user_authored] })
609
+ state.skipped.concat(conflicting.map { |c| [c, :user_authored] })
593
610
  end
594
611
 
595
612
  source
596
613
  end
597
614
 
615
+ def inline_overwrite?(candidate)
616
+ candidate.classification == Classification::INLINE_OVERWRITE
617
+ end
618
+
598
619
  # Returns a list of `[method_name (Symbol), kind (Symbol)]` pairs for every method-like member in the
599
620
  # declaration. ADR-14 slice 4 recognises `MethodDefinition`'s `:instance` / `:singleton` kind plus the
600
621
  # three `attr_*` declaration kinds so a source-side `attr_reader :name` and an RBS-side
@@ -630,7 +651,8 @@ module Rigor
630
651
  return source if new_methods.empty?
631
652
 
632
653
  indent = INDENT * member_indent_depth(decl)
633
- insert_before_end(source, decl, new_methods.map { |c| "#{indent}#{c.rbs}\n" }.join)
654
+ lines = new_methods.flat_map { |c| c.rbs_lines.map { |line| "#{indent}#{line}\n" } }.join
655
+ insert_before_end(source, decl, lines)
634
656
  end
635
657
 
636
658
  # Walks the class's existing method declarations; for each replaceable candidate that matches a member
@@ -645,21 +667,34 @@ module Rigor
645
667
  # canonical case is `initialize_stub_candidate`, which bypasses the existing-RBS comparison and always
646
668
  # classifies as `NEW_METHOD` — when sig-gen's `--params=observed` upgrades a `(path: untyped) -> void`
647
669
  # declaration to `(path: String) -> void` we want `--overwrite` to apply it.
648
- def replace_eligible_conflicts(source, decl, candidates)
670
+ def replace_eligible_conflicts(source, decl, candidates, state)
649
671
  eligible = candidates.select { |c| eligible_for_replacement?(c, decl, source) }
650
672
  return [source, []] if eligible.empty?
651
673
 
652
674
  replaced = []
675
+ done = {}
653
676
  # Apply replacements from highest byte position downward so earlier byte offsets remain valid as the
654
677
  # source grows or shrinks.
655
- sorted = eligible.sort_by { |c| -member_position(decl, c.method_name, c.kind) }
678
+ sorted = eligible.sort_by { |c| -member_position(decl, c) }
656
679
  sorted.each do |candidate|
657
- source = apply_replacement(source, decl, candidate) and replaced << candidate
680
+ member = replaceable_member(decl, candidate)
681
+ # An `attr_accessor` answers for two methods; its one replacement covers both candidates.
682
+ next replaced << candidate if member && done[member.location.start_pos]
683
+
684
+ result = apply_replacement(source, decl, candidate, state)
685
+ next if result.nil?
686
+
687
+ done[member.location.start_pos] = true
688
+ source = result
689
+ replaced << candidate
658
690
  end
659
691
  [source, replaced]
660
692
  end
661
693
 
662
694
  def eligible_for_replacement?(candidate, decl, source)
695
+ return true if inline_overwrite?(candidate)
696
+ return false if replaces_declared_void?(candidate, decl)
697
+
663
698
  case candidate.classification
664
699
  when Classification::TIGHTER_RETURN then true
665
700
  when Classification::NEW_METHOD then tightens_untyped?(candidate, decl, source)
@@ -667,27 +702,56 @@ module Rigor
667
702
  end
668
703
  end
669
704
 
670
- # Compares the existing member's source-side RBS text against the candidate's proposed RBS text. Returns
671
- # true when the new spelling has STRICTLY FEWER bare `untyped` tokens than the existing one — i.e. at
672
- # least one `untyped` slot becomes a concrete type AND no concrete slot becomes `untyped`. Word-boundary
673
- # matching ensures we count `untyped` only as a type token, not as a substring inside identifiers.
674
- def tightens_untyped?(candidate, decl, source)
705
+ # Issue #836 — `--overwrite` never turns a declared `-> void` into a value return. The generator no
706
+ # longer proposes one (`Generator#declares_void?` classifies the method `equivalent`), but the writer
707
+ # reaches the same member through case 2 above whenever the RBS environment did not resolve the `def` to
708
+ # this declaration: an observed-parameter upgrade of `(untyped m) -> void` to `(Module m) -> Registry`
709
+ # counts one `untyped` fewer and would be applied, erasing the author's statement that the return is not
710
+ # part of the contract. A candidate that keeps `void` is still eligible, which is what case 2 is for —
711
+ # the `initialize` stub is always spelled `-> void`.
712
+ def replaces_declared_void?(candidate, decl)
675
713
  member = find_method_member(decl, candidate.method_name, candidate.kind)
676
714
  return false if member.nil?
677
715
 
678
- existing_rbs = source[member.location.start_pos...member.location.end_pos]
679
- count_untyped(candidate.rbs) < count_untyped(existing_rbs)
716
+ declared_void = member.overloads.any? do |overload|
717
+ overload.method_type.type.return_type.is_a?(RBS::Types::Bases::Void)
718
+ end
719
+ declared_void && !candidate.rbs.to_s.end_with?(" -> void")
680
720
  end
681
721
 
682
- def count_untyped(rbs)
683
- rbs.scan(/\buntyped\b/).size
722
+ # Compares the existing member's source-side RBS text against the candidate's proposed RBS text
723
+ # ({.fewer_untyped?}).
724
+ def tightens_untyped?(candidate, decl, source)
725
+ member = find_method_member(decl, candidate.method_name, candidate.kind)
726
+ return false if member.nil?
727
+
728
+ self.class.fewer_untyped?(candidate.rbs, source[member.location.start_pos...member.location.end_pos])
684
729
  end
685
730
 
686
- def member_position(decl, method_name, kind)
687
- member = find_method_member(decl, method_name, kind)
731
+ def member_position(decl, candidate)
732
+ member = replaceable_member(decl, candidate)
688
733
  member ? member.location.start_pos : -1
689
734
  end
690
735
 
736
+ # The member a replacement overwrites. An inline overwrite may also land on an `attr_*` declaration of
737
+ # the same method (ADR-112 WD4), which it replaces in the inline attribute's own spelling; an
738
+ # `overloading?` member (`| ...`) is never its target.
739
+ def replaceable_member(decl, candidate)
740
+ member = find_method_member(decl, candidate.method_name, candidate.kind)
741
+ return member unless candidate.classification == Classification::INLINE_OVERWRITE
742
+
743
+ member = nil if member.respond_to?(:overloading?) && member.overloading?
744
+ member || find_attr_member(decl, candidate.method_name, candidate.kind)
745
+ end
746
+
747
+ def find_attr_member(decl, method_name, kind)
748
+ decl.members.find do |m|
749
+ pairs = []
750
+ collect_pairs_for_member(m, pairs) unless m.is_a?(RBS::AST::Members::MethodDefinition)
751
+ m.respond_to?(:kind) && m.kind == kind && pairs.any? { |name, _| name == method_name }
752
+ end
753
+ end
754
+
691
755
  def find_method_member(decl, method_name, kind)
692
756
  decl.members.find do |m|
693
757
  m.is_a?(RBS::AST::Members::MethodDefinition) && m.name == method_name && m.kind == kind
@@ -697,12 +761,96 @@ module Rigor
697
761
  # Splices the new RBS one-liner over the existing declaration's byte range. `RBS::Parser`'s location
698
762
  # starts at the `def` keyword, NOT at the column zero of the line, so the leading whitespace stays inside
699
763
  # `source[0...start_pos]` and we do not re-emit it.
700
- def apply_replacement(source, decl, candidate)
701
- member = find_method_member(decl, candidate.method_name, candidate.kind)
764
+ def apply_replacement(source, decl, candidate, state)
765
+ member = replaceable_member(decl, candidate)
702
766
  return nil if member.nil?
703
767
 
704
768
  loc = member.location
705
- source[0...loc.start_pos] + candidate.rbs + source[loc.end_pos..]
769
+ replaced = source[0...loc.start_pos] + kept_comments(source, loc) + candidate.rbs + source[loc.end_pos..]
770
+ # Both splices insert above the member, the effect annotation at its own line and the declared ones at
771
+ # its first annotation's, so running the lower insertion first keeps the positions the parse gave valid.
772
+ replaced = splice_annotations(replaced, member, candidate, state)
773
+ splice_declared_annotations(replaced, member, candidate)
774
+ end
775
+
776
+ # A member written across lines (`def f: () -> A` / ` # why` / ` | (B) -> C`) can hold comments inside its
777
+ # own location, which the splice above would delete with the text it replaces. Each such comment line is
778
+ # kept, moved above the new line at the member's indentation. Only whole comment lines are recognised: RBS
779
+ # has no string type that spans a line, so a line whose first non-blank character is `#` is a comment.
780
+ def kept_comments(source, loc)
781
+ text = source[loc.start_pos...loc.end_pos].to_s
782
+ return "" unless text.include?("\n")
783
+
784
+ indent = source[line_start_index(source, loc.start_pos)...loc.start_pos].to_s
785
+ indent = "" unless indent.match?(/\A[ \t]*\z/)
786
+ text.lines.drop(1).filter_map { |line| line.strip if line.match?(/\A\s*#/) }
787
+ .map { |comment| "#{comment}\n#{indent}" }.join
788
+ end
789
+
790
+ # ADR-112 WD4 — the annotations an inline declaration carries that its `sig/` copy lacks, added above the
791
+ # member. Additive only: an annotation already there stays, whoever wrote it, so a copy never loses
792
+ # anything to regeneration (an annotation later deleted inline is therefore not deleted here).
793
+ def splice_declared_annotations(source, member, candidate)
794
+ missing = missing_declared_annotations(member, candidate)
795
+ return source if missing.empty?
796
+
797
+ line_start = line_start_index(source, member.location.start_pos)
798
+ indent = source[line_start...member.location.start_pos].to_s
799
+ return source unless indent.match?(/\A[ \t]*\z/)
800
+
801
+ anchor = annotation_block_start(source, member, line_start)
802
+ source[0...anchor] + missing.map { |line| "#{indent}#{line}\n" }.join + source[anchor..]
803
+ end
804
+
805
+ # A declared annotation line is `%a` plus a one-character delimiter pair around its content.
806
+ def missing_declared_annotations(member, candidate)
807
+ present = member.annotations.map { |annotation| annotation.string.to_s.strip }
808
+ candidate.declared_annotations.reject { |line| present.include?(line[3...-1].to_s.strip) }
809
+ end
810
+
811
+ # The start of the line the member's first annotation sits on, or of the member's own line.
812
+ def annotation_block_start(source, member, line_start)
813
+ starts = member.annotations.filter_map { |annotation| annotation.location&.start_pos }
814
+ ([line_start] + starts.map { |pos| line_start_index(source, pos) }).min
815
+ end
816
+
817
+ # ADR-103 WD9 — the annotation half of a replacement, and the one place this slice is allowed to
818
+ # write bytes ABOVE an existing declaration.
819
+ #
820
+ # `RBS::Parser` puts a member's annotations OUTSIDE its `location`, so {#apply_replacement} above
821
+ # already leaves every annotation on the declaration byte-untouched — the `%a{deprecated}` and the
822
+ # `%a{implicitly-returns-nil}` a user wrote survive an `--overwrite` unchanged. What is left is the
823
+ # question of adding ours, and the answer is: only onto a declaration that carries none.
824
+ #
825
+ # A declaration that already carries an annotation is left exactly as it is, and the candidate is
826
+ # reported as `sig.effect.left-unreadable`. The writer cannot tell an annotation it authored from
827
+ # one the user wrote, and it has no grammar for merging two — so rewriting the region could silently
828
+ # replace an author's own `%a{pure}` with a labelled envelope, or strip a directive Rigor does not
829
+ # read at all. Leaving it and saying so is the only move that cannot destroy an authored fact.
830
+ #
831
+ # An annotation on one of the member's overloads (`def m: %a{pure} (Integer) -> String`) counts too:
832
+ # it sits inside the location, and a tighter return keeps it (#1436), so ours would be a second one.
833
+ def splice_annotations(source, member, candidate, state)
834
+ return source if candidate.annotations.empty?
835
+
836
+ if annotated?(member)
837
+ # Rebuilt so the reported row carries the reason it was actually given. The generator stamped
838
+ # `:emitted` when it decided the annotation; only the writer knows the declaration refused it.
839
+ state.left_unreadable << candidate.with_effect_annotation(candidate.annotations, :left_unreadable)
840
+ return source
841
+ end
842
+
843
+ line_start = line_start_index(source, member.location.start_pos)
844
+ indent = source[line_start...member.location.start_pos].to_s
845
+ return source unless indent.match?(/\A[ \t]*\z/)
846
+
847
+ source[0...line_start] + candidate.annotations.map { |line| "#{indent}#{line}\n" }.join + source[line_start..]
848
+ end
849
+
850
+ def annotated?(member)
851
+ return true unless member.annotations.empty?
852
+
853
+ member.respond_to?(:overloads) && member.overloads.any? { |overload| overload.annotations.any? }
706
854
  end
707
855
  end
708
856
  end
data/lib/rigor/sig_gen.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sig_gen/alias_index"
3
4
  require_relative "sig_gen/classification"
5
+ require_relative "sig_gen/effect_annotation"
6
+ require_relative "sig_gen/inline_declarations"
4
7
  require_relative "sig_gen/method_candidate"
5
8
  require_relative "sig_gen/observed_call"
6
9
  require_relative "sig_gen/meta_class_shape"