rigortype 0.2.6 → 0.2.8

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 (432) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/manual/02-cli-reference.md +2 -1
  4. data/docs/manual/04-diagnostics.md +7 -4
  5. data/docs/manual/06-baseline.md +5 -2
  6. data/docs/manual/08-skills.md +21 -0
  7. data/docs/manual/15-type-protection-coverage.md +6 -4
  8. data/lib/rigor/analysis/baseline.rb +56 -96
  9. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  10. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  11. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  12. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  13. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  14. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  15. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  16. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  17. data/lib/rigor/analysis/check_rules.rb +174 -274
  18. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  19. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  20. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  21. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  22. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  23. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  24. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  25. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  26. data/lib/rigor/analysis/diagnostic.rb +48 -75
  27. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  28. data/lib/rigor/analysis/fact_store.rb +12 -22
  29. data/lib/rigor/analysis/incremental.rb +47 -63
  30. data/lib/rigor/analysis/incremental_session.rb +74 -100
  31. data/lib/rigor/analysis/project_scan.rb +12 -23
  32. data/lib/rigor/analysis/result.rb +3 -5
  33. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  34. data/lib/rigor/analysis/run_stats.rb +38 -61
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  38. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  39. data/lib/rigor/analysis/runner.rb +220 -333
  40. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  41. data/lib/rigor/analysis/worker_session.rb +64 -102
  42. data/lib/rigor/ast/type_node.rb +6 -8
  43. data/lib/rigor/ast.rb +6 -10
  44. data/lib/rigor/bleeding_edge.rb +27 -35
  45. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  46. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  48. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  49. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  50. data/lib/rigor/cache/descriptor.rb +58 -94
  51. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  52. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  53. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  54. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  55. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  56. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  57. data/lib/rigor/cache/rbs_environment.rb +11 -19
  58. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  59. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  60. data/lib/rigor/cache/store.rb +96 -151
  61. data/lib/rigor/cli/annotate_command.rb +51 -88
  62. data/lib/rigor/cli/baseline_command.rb +10 -17
  63. data/lib/rigor/cli/check_command.rb +121 -199
  64. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  65. data/lib/rigor/cli/ci_detector.rb +9 -14
  66. data/lib/rigor/cli/command.rb +8 -14
  67. data/lib/rigor/cli/coverage_command.rb +45 -20
  68. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  69. data/lib/rigor/cli/coverage_report.rb +0 -2
  70. data/lib/rigor/cli/coverage_scan.rb +7 -11
  71. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  72. data/lib/rigor/cli/diff_command.rb +10 -20
  73. data/lib/rigor/cli/docs_command.rb +26 -40
  74. data/lib/rigor/cli/doctor_command.rb +8 -9
  75. data/lib/rigor/cli/explain_command.rb +4 -7
  76. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  77. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  78. data/lib/rigor/cli/lsp_command.rb +11 -19
  79. data/lib/rigor/cli/mcp_command.rb +4 -6
  80. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  81. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  82. data/lib/rigor/cli/options.rb +11 -19
  83. data/lib/rigor/cli/plugin_command.rb +18 -30
  84. data/lib/rigor/cli/plugins_command.rb +29 -51
  85. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  86. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  87. data/lib/rigor/cli/protection_renderer.rb +5 -7
  88. data/lib/rigor/cli/protection_report.rb +27 -17
  89. data/lib/rigor/cli/renderable.rb +4 -6
  90. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  91. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  92. data/lib/rigor/cli/skill_command.rb +70 -36
  93. data/lib/rigor/cli/skill_describe.rb +40 -64
  94. data/lib/rigor/cli/trace_command.rb +9 -16
  95. data/lib/rigor/cli/trace_renderer.rb +35 -51
  96. data/lib/rigor/cli/triage_command.rb +6 -10
  97. data/lib/rigor/cli/triage_renderer.rb +1 -2
  98. data/lib/rigor/cli/type_of_command.rb +14 -23
  99. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_command.rb +6 -10
  101. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  102. data/lib/rigor/cli/type_scan_report.rb +2 -3
  103. data/lib/rigor/cli/upgrade_command.rb +2 -3
  104. data/lib/rigor/cli.rb +16 -28
  105. data/lib/rigor/config_audit.rb +35 -44
  106. data/lib/rigor/configuration/dependencies.rb +41 -70
  107. data/lib/rigor/configuration/severity_profile.rb +28 -42
  108. data/lib/rigor/configuration.rb +150 -241
  109. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  110. data/lib/rigor/environment/class_registry.rb +19 -26
  111. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  112. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  113. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  114. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  115. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  116. data/lib/rigor/environment/rbs_loader.rb +433 -503
  117. data/lib/rigor/environment/reflection.rb +28 -53
  118. data/lib/rigor/environment/reporters.rb +13 -24
  119. data/lib/rigor/environment.rb +176 -291
  120. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  121. data/lib/rigor/flow_contribution/element.rb +7 -12
  122. data/lib/rigor/flow_contribution/fact.rb +33 -57
  123. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  124. data/lib/rigor/flow_contribution/merger.rb +32 -47
  125. data/lib/rigor/flow_contribution.rb +37 -55
  126. data/lib/rigor/inference/acceptance.rb +133 -219
  127. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  128. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  129. data/lib/rigor/inference/budget_trace.rb +43 -63
  130. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  131. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  132. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  133. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  134. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  135. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  136. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  137. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  138. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  139. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  140. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  141. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  142. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  143. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  144. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  145. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  146. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  147. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  148. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  149. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  150. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  151. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  152. data/lib/rigor/inference/def_return_typer.rb +8 -14
  153. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  154. data/lib/rigor/inference/expression_typer.rb +774 -1152
  155. data/lib/rigor/inference/fallback.rb +8 -12
  156. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  157. data/lib/rigor/inference/flow_tracer.rb +25 -36
  158. data/lib/rigor/inference/hkt_body.rb +45 -68
  159. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  160. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  161. data/lib/rigor/inference/hkt_registry.rb +50 -76
  162. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  163. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  164. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  165. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  166. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  167. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  168. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  169. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  170. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  171. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  172. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  173. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  174. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  175. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  176. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  177. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  178. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  179. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  180. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  181. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  182. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  183. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  184. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  185. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  186. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  187. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  188. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  189. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  190. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  191. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  192. data/lib/rigor/inference/mutation_widening.rb +115 -158
  193. data/lib/rigor/inference/narrowing.rb +509 -727
  194. data/lib/rigor/inference/origin_lookup.rb +38 -0
  195. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  196. data/lib/rigor/inference/precision_scanner.rb +26 -35
  197. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  198. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  199. data/lib/rigor/inference/protection_scanner.rb +16 -18
  200. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  201. data/lib/rigor/inference/scope_indexer.rb +473 -817
  202. data/lib/rigor/inference/statement_evaluator.rb +706 -1115
  203. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  204. data/lib/rigor/inference/synthetic_method.rb +9 -16
  205. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  206. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  207. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  208. data/lib/rigor/language_server/buffer_table.rb +11 -18
  209. data/lib/rigor/language_server/completion_provider.rb +69 -116
  210. data/lib/rigor/language_server/debouncer.rb +16 -25
  211. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  212. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  213. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  214. data/lib/rigor/language_server/hover_provider.rb +8 -11
  215. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  216. data/lib/rigor/language_server/loop.rb +14 -19
  217. data/lib/rigor/language_server/project_context.rb +46 -74
  218. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  219. data/lib/rigor/language_server/server.rb +55 -83
  220. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  221. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  222. data/lib/rigor/language_server/uri.rb +8 -13
  223. data/lib/rigor/language_server.rb +4 -6
  224. data/lib/rigor/mcp/loop.rb +2 -3
  225. data/lib/rigor/mcp/server.rb +4 -7
  226. data/lib/rigor/mcp.rb +3 -6
  227. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  228. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  229. data/lib/rigor/plugin/base.rb +335 -518
  230. data/lib/rigor/plugin/blueprint.rb +14 -23
  231. data/lib/rigor/plugin/box.rb +18 -29
  232. data/lib/rigor/plugin/fact_store.rb +16 -26
  233. data/lib/rigor/plugin/inflector.rb +37 -53
  234. data/lib/rigor/plugin/io_boundary.rb +33 -56
  235. data/lib/rigor/plugin/isolation.rb +42 -55
  236. data/lib/rigor/plugin/load_error.rb +10 -15
  237. data/lib/rigor/plugin/loader.rb +30 -49
  238. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  239. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  240. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  241. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  242. data/lib/rigor/plugin/macro.rb +10 -15
  243. data/lib/rigor/plugin/manifest.rb +85 -144
  244. data/lib/rigor/plugin/node_context.rb +14 -22
  245. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  246. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  247. data/lib/rigor/plugin/registry.rb +132 -205
  248. data/lib/rigor/plugin/services.rb +21 -33
  249. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  250. data/lib/rigor/plugin/trust_policy.rb +24 -38
  251. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  252. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  253. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  254. data/lib/rigor/protection/mutator.rb +50 -70
  255. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  256. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  257. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  258. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  259. data/lib/rigor/rbs_extended.rb +107 -197
  260. data/lib/rigor/reflection.rb +68 -86
  261. data/lib/rigor/scope/discovery_index.rb +14 -19
  262. data/lib/rigor/scope.rb +255 -310
  263. data/lib/rigor/sig_gen/classification.rb +6 -10
  264. data/lib/rigor/sig_gen/generator.rb +208 -309
  265. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  266. data/lib/rigor/sig_gen/method_candidate.rb +15 -15
  267. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  268. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  269. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  270. data/lib/rigor/sig_gen/renderer.rb +7 -13
  271. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  272. data/lib/rigor/sig_gen/write_result.rb +8 -16
  273. data/lib/rigor/sig_gen/writer.rb +141 -173
  274. data/lib/rigor/sig_gen.rb +3 -6
  275. data/lib/rigor/signature_path_audit.rb +24 -30
  276. data/lib/rigor/source/constant_path.rb +10 -14
  277. data/lib/rigor/source/literals.rb +31 -45
  278. data/lib/rigor/source/node_locator.rb +9 -11
  279. data/lib/rigor/source/node_walker.rb +9 -13
  280. data/lib/rigor/source.rb +3 -4
  281. data/lib/rigor/testing.rb +16 -20
  282. data/lib/rigor/triage/catalogue.rb +38 -62
  283. data/lib/rigor/triage.rb +31 -52
  284. data/lib/rigor/trinary.rb +9 -13
  285. data/lib/rigor/type/acceptance_router.rb +4 -6
  286. data/lib/rigor/type/accepts_result.rb +10 -14
  287. data/lib/rigor/type/app.rb +19 -27
  288. data/lib/rigor/type/bot.rb +4 -6
  289. data/lib/rigor/type/bound_method.rb +10 -15
  290. data/lib/rigor/type/combinator.rb +165 -257
  291. data/lib/rigor/type/constant.rb +23 -34
  292. data/lib/rigor/type/data_class.rb +10 -15
  293. data/lib/rigor/type/data_instance.rb +14 -20
  294. data/lib/rigor/type/difference.rb +21 -32
  295. data/lib/rigor/type/dynamic.rb +3 -5
  296. data/lib/rigor/type/hash_shape.rb +32 -18
  297. data/lib/rigor/type/integer_range.rb +11 -16
  298. data/lib/rigor/type/intersection.rb +27 -42
  299. data/lib/rigor/type/nominal.rb +10 -15
  300. data/lib/rigor/type/plain_lattice.rb +9 -13
  301. data/lib/rigor/type/refined.rb +67 -114
  302. data/lib/rigor/type/singleton.rb +4 -6
  303. data/lib/rigor/type/struct_class.rb +11 -16
  304. data/lib/rigor/type/struct_instance.rb +15 -21
  305. data/lib/rigor/type/tuple.rb +14 -19
  306. data/lib/rigor/type/union.rb +30 -42
  307. data/lib/rigor/type_node/generic.rb +14 -26
  308. data/lib/rigor/type_node/identifier.rb +12 -19
  309. data/lib/rigor/type_node.rb +3 -12
  310. data/lib/rigor/value_semantics.rb +16 -21
  311. data/lib/rigor/version.rb +1 -1
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  314. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  315. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  318. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  319. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  322. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  323. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +107 -83
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  326. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  327. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  332. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  333. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -236
  334. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  337. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  338. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  339. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  340. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  341. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  342. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  343. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  344. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  345. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  346. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  347. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  348. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  349. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  350. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  351. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  352. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  353. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  354. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  357. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  358. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  359. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  360. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  361. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  362. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +20 -36
  363. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +23 -30
  364. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  365. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  366. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  369. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  370. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  371. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  374. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  384. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  389. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  390. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  392. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  393. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  394. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  395. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  398. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  399. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  400. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  401. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  409. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  410. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +182 -328
  411. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  412. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  413. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  414. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  415. data/sig/rigor/scope.rbs +6 -0
  416. data/skills/rigor-ask/SKILL.md +21 -1
  417. data/skills/rigor-baseline-reduce/SKILL.md +16 -0
  418. data/skills/rigor-ci-setup/SKILL.md +96 -249
  419. data/skills/rigor-doctor/SKILL.md +39 -49
  420. data/skills/rigor-doctor/references/01-checks.md +52 -0
  421. data/skills/rigor-editor-setup/SKILL.md +14 -0
  422. data/skills/rigor-mcp-setup/SKILL.md +14 -0
  423. data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
  424. data/skills/rigor-plugin-author/SKILL.md +16 -0
  425. data/skills/rigor-plugin-review/SKILL.md +174 -0
  426. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
  427. data/skills/rigor-plugin-tune/SKILL.md +21 -2
  428. data/skills/rigor-project-init/SKILL.md +16 -0
  429. data/skills/rigor-protection-uplift/SKILL.md +15 -0
  430. data/skills/rigor-rbs-setup/SKILL.md +15 -0
  431. data/skills/rigor-upgrade/SKILL.md +16 -0
  432. metadata +8 -4
@@ -8,61 +8,47 @@ require_relative "write_result"
8
8
 
9
9
  module Rigor
10
10
  module SigGen
11
- # Applies a per-source-file group of {MethodCandidate}s to
12
- # the target `.rbs` file under the project signature tree.
11
+ # Applies a per-source-file group of {MethodCandidate}s to the target `.rbs` file under the project
12
+ # signature tree.
13
13
  #
14
- # ADR-14 slice 2: the writer parses the target with
15
- # `RBS::Parser` to find the matching class declaration and
16
- # inserts new method declarations just before the class's
17
- # closing `end` keyword. Existing declarations are NEVER
18
- # touched unless `--overwrite` is set AND the candidate's
19
- # classification is `tighter-return`.
14
+ # ADR-14 slice 2: the writer parses the target with `RBS::Parser` to find the matching class declaration and
15
+ # inserts new method declarations just before the class's closing `end` keyword. Existing declarations are
16
+ # NEVER touched unless `--overwrite` is set AND the candidate's classification is `tighter-return`.
20
17
  #
21
- # The slice does NOT re-render the whole file through
22
- # `RBS::Writer`. That would lose comments / blank-line
23
- # formatting per upstream design; the byte-range insertion
24
- # approach taken here preserves untouched declarations
25
- # verbatim. Mixed hand-written + generated output inside
26
- # the *same* class declaration may still lose trailing
27
- # blank lines on the touched ranges; the `--diff` review
28
- # surface from slice 1 is the user's escape hatch.
18
+ # The slice does NOT re-render the whole file through `RBS::Writer`. That would lose comments / blank-line
19
+ # formatting per upstream design; the byte-range insertion approach taken here preserves untouched
20
+ # declarations verbatim. Mixed hand-written + generated output inside the *same* class declaration may still
21
+ # lose trailing blank lines on the touched ranges; the `--diff` review surface from slice 1 is the user's
22
+ # escape hatch.
29
23
  #
30
- # Safety boundary: the writer ASSERTS the target lives
31
- # inside the configured signature tree before touching the
32
- # disk. Files outside that tree route through
33
- # `WriteResult(action: :skipped_outside_sig_root)`; the
24
+ # Safety boundary: the writer ASSERTS the target lives inside the configured signature tree before touching
25
+ # the disk. Files outside that tree route through `WriteResult(action: :skipped_outside_sig_root)`; the
34
26
  # caller decides whether to warn or fail.
35
27
  class Writer # rubocop:disable Metrics/ClassLength
36
28
  INDENT = " "
37
29
  private_constant :INDENT
38
30
 
39
- # Per-`update_existing` accumulator. The merge_class
40
- # helper mutates `source` / `decls` / `applied` /
41
- # `skipped` in place as each class is processed so the
42
- # next class sees the latest byte positions.
31
+ # Per-`update_existing` accumulator. The merge_class helper mutates `source` / `decls` / `applied` /
32
+ # `skipped` in place as each class is processed so the next class sees the latest byte positions.
43
33
  MergeState = Struct.new(:source, :decls, :applied, :skipped, keyword_init: true)
44
34
  private_constant :MergeState
45
35
 
46
36
  def initialize(path_mapper:, overwrite: false)
47
37
  @path_mapper = path_mapper
48
38
  @overwrite = overwrite
49
- # Run-level (cross-file) namespace-kind view, populated
50
- # per `#write_all` from every candidate's per-file map.
51
- # Empty until then so the single-target `#write` path
52
- # falls back to per-candidate kinds only.
39
+ # Run-level (cross-file) namespace-kind view, populated per `#write_all` from every candidate's per-file
40
+ # map. Empty until then so the single-target `#write` path falls back to per-candidate kinds only.
53
41
  @global_namespace_kinds = {}
42
+ # Run-level qualified-class-name => superclass-token view, same lifecycle as `@global_namespace_kinds`.
43
+ @global_superclasses = {}
54
44
  end
55
45
 
56
- # Process the full candidate list by resolving each
57
- # candidate's target sig file via the path mapper (which
58
- # may route consolidated-layout classes to existing
59
- # files) and grouping candidates that share a target
46
+ # Process the full candidate list by resolving each candidate's target sig file via the path mapper (which
47
+ # may route consolidated-layout classes to existing files) and grouping candidates that share a target
60
48
  # before writing.
61
49
  #
62
- # ADR-14 follow-up: this is the consolidated-layout
63
- # entry point. The legacy `write(source_path, candidates)`
64
- # below assumes all candidates share a target and
65
- # remains for spec convenience.
50
+ # ADR-14 follow-up: this is the consolidated-layout entry point. The legacy `write(source_path,
51
+ # candidates)` below assumes all candidates share a target and remains for spec convenience.
66
52
  #
67
53
  # @param candidates [Array<MethodCandidate>]
68
54
  # @return [Array<WriteResult>] one per target sig file.
@@ -71,6 +57,7 @@ module Rigor
71
57
  return [] if emittable.empty?
72
58
 
73
59
  @global_namespace_kinds = build_namespace_kinds(candidates)
60
+ @global_superclasses = build_superclasses(candidates)
74
61
  emittable.group_by { |c| @path_mapper.target_for(c.path, class_name: c.class_name) }
75
62
  .map { |target, group| write_target(target, group) }
76
63
  end
@@ -91,10 +78,8 @@ module Rigor
91
78
 
92
79
  private
93
80
 
94
- # Shared per-target write path used by both `#write` and
95
- # `#write_all`. Picks a representative `source_path` for
96
- # the {WriteResult} when multiple candidates merge into
97
- # one target.
81
+ # Shared per-target write path used by both `#write` and `#write_all`. Picks a representative
82
+ # `source_path` for the {WriteResult} when multiple candidates merge into one target.
98
83
  def write_target(target, candidates, source_path: nil)
99
84
  source_path ||= candidates.first&.path
100
85
  unless inside_sig_root?(target)
@@ -112,8 +97,7 @@ module Rigor
112
97
  root = @path_mapper.sig_root_dir.realpath
113
98
  target.expand_path.ascend.any? { |ancestor| realpath_or_nil(ancestor) == root }
114
99
  rescue Errno::ENOENT
115
- # The sig root doesn't exist yet; we'll create it
116
- # alongside the target file. Allow this case.
100
+ # The sig root doesn't exist yet; we'll create it alongside the target file. Allow this case.
117
101
  target.expand_path.to_s.start_with?(@path_mapper.sig_root_dir.expand_path.to_s)
118
102
  end
119
103
 
@@ -130,35 +114,26 @@ module Rigor
130
114
  action: :created, applied: candidates)
131
115
  end
132
116
 
133
- # ADR-14 gap-#3 follow-up (c): when one candidate's
134
- # `class_name` is a strict prefix of another's, emit a
135
- # single nested tree instead of two flat sibling
136
- # blocks. The third-round self-dogfood surfaced
137
- # `Analysis::DependencySourceInference::GemResolver`
138
- # containing `class Resolved < Data.define(...)` — the
139
- # earlier `group_by(&:class_name)` flattened that into
140
- # two top-level wraps (one for GemResolver's own
141
- # methods, one for GemResolver::Resolved's), which
142
- # Steep accepted but is not the canonical layout in
143
- # this project's `sig/`.
117
+ # ADR-14 gap-#3 follow-up (c): when one candidate's `class_name` is a strict prefix of another's, emit a
118
+ # single nested tree instead of two flat sibling blocks. The third-round self-dogfood surfaced
119
+ # `Analysis::DependencySourceInference::GemResolver` containing `class Resolved < Data.define(...)` the
120
+ # earlier `group_by(&:class_name)` flattened that into two top-level wraps (one for GemResolver's own
121
+ # methods, one for GemResolver::Resolved's), which Steep accepted but is not the canonical layout in this
122
+ # project's `sig/`.
144
123
  #
145
- # The writer now builds a tree keyed by qualified-name
146
- # segments. Each tree node carries (qualified_name,
147
- # methods, shells, children); rendering walks the tree
148
- # so every nested class appears inside its parent's
149
- # block. Empty class shells (gap-#3 follow-up (e), e.g.
150
- # `Unresolvable = Data.define(...)`) participate as
151
- # zero-method tree nodes — they emit `class Foo\nend`
152
- # at their position.
124
+ # The writer now builds a tree keyed by qualified-name segments. Each tree node carries (qualified_name,
125
+ # methods, shells, children); rendering walks the tree so every nested class appears inside its parent's
126
+ # block. Empty class shells (gap-#3 follow-up (e), e.g. `Unresolvable = Data.define(...)`) participate as
127
+ # zero-method tree nodes — they emit `class Foo\nend` at their position.
153
128
  def render_new_file(candidates)
154
129
  shells = collect_class_shells(candidates)
155
130
  tree = build_namespace_tree(candidates, shells)
156
131
  kinds = merged_namespace_kinds(candidates)
157
- render_tree_nodes(tree, kinds, 0)
132
+ supers = merged_superclasses(candidates)
133
+ render_tree_nodes(tree, kinds, supers, 0)
158
134
  end
159
135
 
160
- # Drains `class_shells` from every candidate; the
161
- # generator's walker populates the same set on every
136
+ # Drains `class_shells` from every candidate; the generator's walker populates the same set on every
162
137
  # candidate produced from a given file (gap-#3 (e)).
163
138
  def collect_class_shells(candidates)
164
139
  shells = Set.new
@@ -174,30 +149,44 @@ module Rigor
174
149
  merged
175
150
  end
176
151
 
177
- # Folds every candidate's per-file namespace-kind map
178
- # into one run-level view so a `class Foo` recorded
179
- # while scanning `foo.rb` governs the wrapper keyword
180
- # emitted for `Foo` in a *sibling* file's target e.g.
181
- # `foo/bar.rb` declaring `class Foo::Bar`, whose compact
182
- # constant path never names `Foo`, so the walker records
183
- # no kind for it. Without this view that sibling target
184
- # wraps the nested class in `module Foo` while `foo.rbs`
185
- # declares `class Foo`; loading both raises
186
- # `RBS::DuplicatedDeclarationError`, aborting the whole
187
- # RBS env build.
152
+ # Folds every candidate's per-file namespace-kind map into one run-level view so a `class Foo` recorded
153
+ # while scanning `foo.rb` governs the wrapper keyword emitted for `Foo` in a *sibling* file's target — e.g.
154
+ # `foo/bar.rb` declaring `class Foo::Bar`, whose compact constant path never names `Foo`, so the walker
155
+ # records no kind for it. Without this view that sibling target wraps the nested class in `module Foo`
156
+ # while `foo.rbs` declares `class Foo`; loading both raises `RBS::DuplicatedDeclarationError`, aborting the
157
+ # whole RBS env build.
188
158
  def build_namespace_kinds(candidates)
189
159
  candidates.each_with_object({}) do |candidate, acc|
190
160
  (candidate.namespace_kinds || {}).each { |name, kind| apply_namespace_kind(acc, name, kind) }
191
161
  end
192
162
  end
193
163
 
194
- # A `class` declaration is authoritative and MUST win
195
- # over the `:module` wrapper default: a compact
196
- # `class Foo::Bar` never names `Foo`, so the only signal
197
- # for `Foo`'s kind is an actual `class Foo` (or
198
- # `Const = Data.define(...)` shell) seen elsewhere. This
199
- # guarantees the generated tree never mixes `class` /
200
- # `module` for the same constant.
164
+ # Superclass twins of {#merged_namespace_kinds} / {#build_namespace_kinds}: fold every candidate's
165
+ # per-file `class_superclasses` map into one view keyed by qualified class name. Only plain-constant
166
+ # superclasses are ever recorded (the generator skips computed ones), so a missing key means "emit no
167
+ # superclass", never "unknown".
168
+ def merged_superclasses(candidates)
169
+ merged = @global_superclasses.dup
170
+ candidates.each do |c|
171
+ next unless c.respond_to?(:class_superclasses)
172
+
173
+ (c.class_superclasses || {}).each { |name, sup| merged[name] = sup }
174
+ end
175
+ merged
176
+ end
177
+
178
+ def build_superclasses(candidates)
179
+ candidates.each_with_object({}) do |candidate, acc|
180
+ next unless candidate.respond_to?(:class_superclasses)
181
+
182
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
183
+ end
184
+ end
185
+
186
+ # A `class` declaration is authoritative and MUST win over the `:module` wrapper default: a compact
187
+ # `class Foo::Bar` never names `Foo`, so the only signal for `Foo`'s kind is an actual `class Foo` (or
188
+ # `Const = Data.define(...)` shell) seen elsewhere. This guarantees the generated tree never mixes
189
+ # `class` / `module` for the same constant.
201
190
  def apply_namespace_kind(map, key, kind)
202
191
  if kind == :class
203
192
  map[key] = :class
@@ -206,12 +195,9 @@ module Rigor
206
195
  end
207
196
  end
208
197
 
209
- # Tree node: { name:, children: Hash{String => node},
210
- # methods: Array<MethodCandidate>, shell: Boolean }.
211
- # `shell` flags nodes that came in via `class_shells`
212
- # only (no methods of their own); rendering uses it to
213
- # default the keyword to `:class` for the `class Const`
214
- # = `Data.define(...)` case.
198
+ # Tree node: { name:, children: Hash{String => node}, methods: Array<MethodCandidate>, shell: Boolean }.
199
+ # `shell` flags nodes that came in via `class_shells` only (no methods of their own); rendering uses it to
200
+ # default the keyword to `:class` for the `class Const` = `Data.define(...)` case.
215
201
  def build_namespace_tree(candidates, shells)
216
202
  root = { name: nil, children: {}, methods: [], shell: false }
217
203
  candidates.group_by(&:class_name).each do |class_name, methods|
@@ -234,35 +220,44 @@ module Rigor
234
220
  end
235
221
  end
236
222
 
237
- def render_tree_nodes(node, kinds, depth)
238
- node[:children].values.map { |child| render_tree_node(child, kinds, depth, [node[:name]].compact) }.join("\n")
223
+ def render_tree_nodes(node, kinds, supers, depth)
224
+ node[:children].values.map do |child|
225
+ render_tree_node(child, kinds, supers, depth, [node[:name]].compact)
226
+ end.join("\n")
239
227
  end
240
228
 
241
- def render_tree_node(node, kinds, depth, prefix)
229
+ def render_tree_node(node, kinds, supers, depth, prefix)
242
230
  indent = INDENT * depth
243
231
  qualified = (prefix + [node[:name]]).join("::")
244
232
  keyword = node_keyword(node, kinds, qualified)
245
- body = render_tree_node_body(node, kinds, depth, prefix)
246
- "#{indent}#{keyword} #{node[:name]}\n#{body}#{indent}end\n"
233
+ header = "#{keyword} #{node[:name]}#{superclass_suffix(keyword, supers, qualified)}"
234
+ body = render_tree_node_body(node, kinds, supers, depth, prefix)
235
+ "#{indent}#{header}\n#{body}#{indent}end\n"
247
236
  end
248
237
 
249
- def render_tree_node_body(node, kinds, depth, prefix)
238
+ def render_tree_node_body(node, kinds, supers, depth, prefix)
250
239
  inner_indent = INDENT * (depth + 1)
251
240
  method_lines = node[:methods].map { |c| "#{inner_indent}#{c.rbs}\n" }.join
252
241
  child_blocks = node[:children].values.map do |child|
253
- render_tree_node(child, kinds, depth + 1, prefix + [node[:name]])
242
+ render_tree_node(child, kinds, supers, depth + 1, prefix + [node[:name]])
254
243
  end.join
255
244
  method_lines + child_blocks
256
245
  end
257
246
 
258
- # Per ADR-14 gap-#3 (a) the keyword for a segment comes
259
- # from `namespace_kinds` when known. The default for an
260
- # explicit class shell (gap-#3 (e), `Const =
261
- # Data.define(...)`) is `:class`; otherwise default to
262
- # `:class` for a method-bearing leaf node and `:module`
263
- # for an intermediate (children-only) segment. Defaulting
264
- # intermediates to `:module` matches RBS's "multiple
265
- # `module Foo` declarations merge" rule.
247
+ # ` < Super` for a `class` node whose qualified name has a recorded superclass, else the empty string. A
248
+ # `module` never takes a superclass (RBS forbids it), so the keyword gates emission — a name coincidentally
249
+ # recorded as both (impossible from one source class, but cheap to guard) stays a bare module.
250
+ def superclass_suffix(keyword, supers, qualified)
251
+ return "" unless keyword == :class
252
+
253
+ superclass = supers[qualified]
254
+ superclass ? " < #{superclass}" : ""
255
+ end
256
+
257
+ # Per ADR-14 gap-#3 (a) the keyword for a segment comes from `namespace_kinds` when known. The default for
258
+ # an explicit class shell (gap-#3 (e), `Const = Data.define(...)`) is `:class`; otherwise default to
259
+ # `:class` for a method-bearing leaf node and `:module` for an intermediate (children-only) segment.
260
+ # Defaulting intermediates to `:module` matches RBS's "multiple `module Foo` declarations merge" rule.
266
261
  def node_keyword(node, kinds, qualified)
267
262
  return kinds.fetch(qualified) if kinds.key?(qualified)
268
263
  return :class if node[:shell]
@@ -277,7 +272,8 @@ module Rigor
277
272
  return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
278
273
 
279
274
  state = MergeState.new(source: source, decls: decls, applied: [], skipped: [])
280
- candidates.group_by(&:class_name).each { |class_name, methods| merge_class(state, class_name, methods) }
275
+ supers = merged_superclasses(candidates)
276
+ candidates.group_by(&:class_name).each { |class_name, methods| merge_class(state, class_name, methods, supers) }
281
277
  merge_class_shells(state, collect_class_shells(candidates), merged_namespace_kinds(candidates))
282
278
 
283
279
  action = state.applied.empty? ? :noop : :updated
@@ -286,15 +282,11 @@ module Rigor
286
282
  action: action, applied: state.applied, skipped: state.skipped)
287
283
  end
288
284
 
289
- # ADR-14 gap-#3 (e): for every requested class shell
290
- # that isn't already declared in the target file,
291
- # insert an empty `class Const\nend` block inside the
292
- # nearest existing ancestor. Shells already covered by
293
- # an existing declaration are silently a no-op. The
294
- # `applied` accumulator does NOT grow — shells are
295
- # structural declarations, not methods, so the
296
- # action-count surface (`updated +N`) keeps
297
- # reflecting method changes only.
285
+ # ADR-14 gap-#3 (e): for every requested class shell that isn't already declared in the target file,
286
+ # insert an empty `class Const\nend` block inside the nearest existing ancestor. Shells already covered by
287
+ # an existing declaration are silently a no-op. The `applied` accumulator does NOT grow — shells are
288
+ # structural declarations, not methods, so the action-count surface (`updated +N`) keeps reflecting method
289
+ # changes only.
298
290
  def merge_class_shells(state, shells, kinds)
299
291
  shells.each do |qualified|
300
292
  next if find_class_decl(state.decls, qualified)
@@ -325,11 +317,9 @@ module Rigor
325
317
  [[], segments]
326
318
  end
327
319
 
328
- # Pulls the indent depth (in `INDENT` units) one level
329
- # deeper than the anchor decl's own column. Pre-
330
- # existing members might be missing (an empty
331
- # `class Foo; end`) so the keyword column is the
332
- # robust signal.
320
+ # Pulls the indent depth (in `INDENT` units) one level deeper than the anchor decl's own column.
321
+ # Pre-existing members might be missing (an empty `class Foo; end`) so the keyword column is the robust
322
+ # signal.
333
323
  def anchor_decl_indent_depth(decl)
334
324
  decl_column = decl.location[:keyword].start_column
335
325
  (decl_column / INDENT.size) + 1
@@ -367,22 +357,19 @@ module Rigor
367
357
  nil
368
358
  end
369
359
 
370
- def merge_class(state, class_name, methods)
360
+ def merge_class(state, class_name, methods, supers = {})
371
361
  decl = find_class_decl(state.decls, class_name)
372
362
  state.source = if decl.nil?
373
- append_new_class(state.source, class_name, methods, state.applied)
363
+ append_new_class(state.source, class_name, methods, state.applied, supers[class_name])
374
364
  else
375
365
  merge_into_existing_class(state.source, decl, methods, state.applied, state.skipped)
376
366
  end
377
367
  state.decls = parse_signature(state.source) || state.decls
378
368
  end
379
369
 
380
- # Walks the parsed decl tree recursively, tracking the
381
- # enclosing module/class prefix, and returns the
382
- # declaration whose fully-qualified name matches
383
- # `qualified_name`. Recursing into modules lets us
384
- # match `Rigor::Type::Nominal` against the
385
- # `class Nominal` declaration nested inside
370
+ # Walks the parsed decl tree recursively, tracking the enclosing module/class prefix, and returns the
371
+ # declaration whose fully-qualified name matches `qualified_name`. Recursing into modules lets us match
372
+ # `Rigor::Type::Nominal` against the `class Nominal` declaration nested inside
386
373
  # `module Rigor; module Type; … end; end`.
387
374
  def find_class_decl(decls, qualified_name)
388
375
  find_class_decl_in(decls, [], qualified_name)
@@ -402,12 +389,12 @@ module Rigor
402
389
  nil
403
390
  end
404
391
 
405
- # Appends an entirely new `class Foo … end` block at the
406
- # end of the file (with a leading blank line as
392
+ # Appends an entirely new `class Foo … end` block at the end of the file (with a leading blank line as
407
393
  # separator).
408
- def append_new_class(source, class_name, methods, applied)
394
+ def append_new_class(source, class_name, methods, applied, superclass = nil)
409
395
  body = methods.map { |c| "#{INDENT}#{c.rbs}" }.join("\n")
410
- snippet = "\nclass #{class_name}\n#{body}\nend\n"
396
+ header = superclass ? "class #{class_name} < #{superclass}" : "class #{class_name}"
397
+ snippet = "\n#{header}\n#{body}\nend\n"
411
398
  applied.concat(methods)
412
399
  ends_with_newline?(source) ? source + snippet : "#{source}\n#{snippet}"
413
400
  end
@@ -434,13 +421,10 @@ module Rigor
434
421
  source
435
422
  end
436
423
 
437
- # Returns a list of `[method_name (Symbol), kind (Symbol)]`
438
- # pairs for every method-like member in the declaration.
439
- # ADR-14 slice 4 recognises `MethodDefinition`'s
440
- # `:instance` / `:singleton` kind plus the three
441
- # `attr_*` declaration kinds so a source-side
442
- # `attr_reader :name` and an RBS-side `attr_reader name: T`
443
- # are treated as the same member (i.e. user-authored).
424
+ # Returns a list of `[method_name (Symbol), kind (Symbol)]` pairs for every method-like member in the
425
+ # declaration. ADR-14 slice 4 recognises `MethodDefinition`'s `:instance` / `:singleton` kind plus the
426
+ # three `attr_*` declaration kinds so a source-side `attr_reader :name` and an RBS-side
427
+ # `attr_reader name: T` are treated as the same member (i.e. user-authored).
444
428
  def collect_member_pairs(decl)
445
429
  pairs = []
446
430
  decl.members.each { |m| collect_pairs_for_member(m, pairs) }
@@ -465,10 +449,9 @@ module Rigor
465
449
  methods.partition { |c| !existing_pairs.include?([c.method_name, c.kind]) }
466
450
  end
467
451
 
468
- # Inserts each new method line one column before the
469
- # class declaration's `end` keyword. The insertion text
470
- # carries its own leading indent + trailing newline so
471
- # the surrounding source's whitespace stays intact.
452
+ # Inserts each new method line one column before the class declaration's `end` keyword. The insertion
453
+ # text carries its own leading indent + trailing newline so the surrounding source's whitespace stays
454
+ # intact.
472
455
  def insert_into_class(source, decl, new_methods)
473
456
  return source if new_methods.empty?
474
457
 
@@ -477,34 +460,25 @@ module Rigor
477
460
  source[0...end_pos] + addition + source[end_pos..]
478
461
  end
479
462
 
480
- # Walks the class's existing method declarations; for
481
- # each replaceable candidate that matches a member
482
- # name, slices out the old declaration's source range
483
- # and substitutes the new RBS one-liner. Members that
463
+ # Walks the class's existing method declarations; for each replaceable candidate that matches a member
464
+ # name, slices out the old declaration's source range and substitutes the new RBS one-liner. Members that
484
465
  # are not `MethodDefinition`s are left alone.
485
466
  #
486
- # Two candidate classifications are eligible for
487
- # replacement under `--overwrite`:
467
+ # Two candidate classifications are eligible for replacement under `--overwrite`:
488
468
  #
489
- # 1. `TIGHTER_RETURN` — the classifier already proved the
490
- # new return type is a strict subtype of the declared
491
- # one (with lenience guards passed).
492
- # 2. `NEW_METHOD` whose new RBS strictly tightens an
493
- # `untyped` position in the existing declaration. The
494
- # canonical case is `initialize_stub_candidate`, which
495
- # bypasses the existing-RBS comparison and always
496
- # classifies as `NEW_METHOD` — when sig-gen's
497
- # `--params=observed` upgrades a `(path: untyped) -> void`
498
- # declaration to `(path: String) -> void` we want
499
- # `--overwrite` to apply it.
469
+ # 1. `TIGHTER_RETURN` — the classifier already proved the new return type is a strict subtype of the
470
+ # declared one (with lenience guards passed).
471
+ # 2. `NEW_METHOD` whose new RBS strictly tightens an `untyped` position in the existing declaration. The
472
+ # canonical case is `initialize_stub_candidate`, which bypasses the existing-RBS comparison and always
473
+ # classifies as `NEW_METHOD` when sig-gen's `--params=observed` upgrades a `(path: untyped) -> void`
474
+ # declaration to `(path: String) -> void` we want `--overwrite` to apply it.
500
475
  def replace_eligible_conflicts(source, decl, candidates)
501
476
  eligible = candidates.select { |c| eligible_for_replacement?(c, decl, source) }
502
477
  return [source, []] if eligible.empty?
503
478
 
504
479
  replaced = []
505
- # Apply replacements from highest byte position downward
506
- # so earlier byte offsets remain valid as the source
507
- # grows or shrinks.
480
+ # Apply replacements from highest byte position downward so earlier byte offsets remain valid as the
481
+ # source grows or shrinks.
508
482
  sorted = eligible.sort_by { |c| -member_position(decl, c.method_name, c.kind) }
509
483
  sorted.each do |candidate|
510
484
  source = apply_replacement(source, decl, candidate) and replaced << candidate
@@ -520,14 +494,10 @@ module Rigor
520
494
  end
521
495
  end
522
496
 
523
- # Compares the existing member's source-side RBS text
524
- # against the candidate's proposed RBS text. Returns
525
- # true when the new spelling has STRICTLY FEWER bare
526
- # `untyped` tokens than the existing one i.e. at
527
- # least one `untyped` slot becomes a concrete type AND
528
- # no concrete slot becomes `untyped`. Word-boundary
529
- # matching ensures we count `untyped` only as a type
530
- # token, not as a substring inside identifiers.
497
+ # Compares the existing member's source-side RBS text against the candidate's proposed RBS text. Returns
498
+ # true when the new spelling has STRICTLY FEWER bare `untyped` tokens than the existing one — i.e. at
499
+ # least one `untyped` slot becomes a concrete type AND no concrete slot becomes `untyped`. Word-boundary
500
+ # matching ensures we count `untyped` only as a type token, not as a substring inside identifiers.
531
501
  def tightens_untyped?(candidate, decl, source)
532
502
  member = find_method_member(decl, candidate.method_name, candidate.kind)
533
503
  return false if member.nil?
@@ -551,10 +521,8 @@ module Rigor
551
521
  end
552
522
  end
553
523
 
554
- # Splices the new RBS one-liner over the existing
555
- # declaration's byte range. `RBS::Parser`'s location
556
- # starts at the `def` keyword, NOT at the column zero of
557
- # the line, so the leading whitespace stays inside
524
+ # Splices the new RBS one-liner over the existing declaration's byte range. `RBS::Parser`'s location
525
+ # starts at the `def` keyword, NOT at the column zero of the line, so the leading whitespace stays inside
558
526
  # `source[0...start_pos]` and we do not re-emit it.
559
527
  def apply_replacement(source, decl, candidate)
560
528
  member = find_method_member(decl, candidate.method_name, candidate.kind)
data/lib/rigor/sig_gen.rb CHANGED
@@ -13,13 +13,10 @@ require_relative "sig_gen/write_result"
13
13
  require_relative "sig_gen/writer"
14
14
 
15
15
  module Rigor
16
- # Namespace for the RBS signature generator that powers
17
- # `rigor sig-gen` (ADR-14).
16
+ # Namespace for the RBS signature generator that powers `rigor sig-gen` (ADR-14).
18
17
  #
19
- # The generator emits RBS from Rigor's inference results so
20
- # users close RBS coverage gaps without freehand authorship.
21
- # See `docs/adr/14-rbs-sig-generation.md` for the design
22
- # rationale and the slicing plan.
18
+ # The generator emits RBS from Rigor's inference results so users close RBS coverage gaps without freehand
19
+ # authorship. See `docs/adr/14-rbs-sig-generation.md` for the design rationale and the slicing plan.
23
20
  module SigGen
24
21
  end
25
22
  end
@@ -1,33 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- # Classifies each configured `signature_paths:` entry by what it
5
- # actually contributes to the RBS environment, so a caller can warn
6
- # when a configured path resolves to nothing.
4
+ # Classifies each configured `signature_paths:` entry by what it actually contributes to
5
+ # the RBS environment, so a caller can warn when a configured path resolves to nothing.
7
6
  #
8
- # The failure this guards against is silent. {Environment::RbsLoader}
9
- # `add`s a `signature_paths:` entry only when `path.directory?`, and
10
- # only the `.rbs` files under it carry signatures — so a typo'd or
11
- # moved path (or a directory holding no `.rbs`) loads zero signatures
12
- # with no trace on stderr or in the run summary. The downstream symptom
13
- # is the most authoritative diagnostics: every call into the extensions
14
- # the missing RBS was meant to describe fires `call.undefined-method` at
15
- # `evidence_tier: high`. A one-character path typo can manufacture
16
- # hundreds of plausible-looking false positives; surfacing the empty
17
- # entry makes the real cause visible.
7
+ # The failure this guards against is silent. {Environment::RbsLoader} `add`s a
8
+ # `signature_paths:` entry only when `path.directory?`, and only the `.rbs` files under it
9
+ # carry signatures — so a typo'd or moved path (or a directory holding no `.rbs`) loads
10
+ # zero signatures with no trace on stderr or in the run summary. The downstream symptom is
11
+ # the most authoritative diagnostics: every call into the extensions the missing RBS was
12
+ # meant to describe fires `call.undefined-method` at `evidence_tier: high`. A one-character
13
+ # path typo can manufacture hundreds of plausible-looking false positives; surfacing the
14
+ # empty entry makes the real cause visible.
18
15
  #
19
- # The audit deliberately mirrors the loader's own acceptance test
20
- # (`path.directory?` + a recursive `**/*.rbs` glob) so a `:ok` verdict
21
- # means the loader did load from it and a warning means it did not.
16
+ # The audit deliberately mirrors the loader's own acceptance test (`path.directory?` + a
17
+ # recursive `**/*.rbs` glob) so a `:ok` verdict means the loader did load from it and a
18
+ # warning means it did not.
22
19
  module SignaturePathAudit
23
20
  # One configured `signature_paths:` entry's resolution status.
24
21
  #
25
22
  # `status` is one of:
26
23
  # - `:ok` — a directory containing at least one `.rbs`.
27
24
  # - `:missing` — the path does not exist.
28
- # - `:not_directory` — the path exists but is not a directory (the
29
- # loader only `add`s directories, so a `.rbs` file passed directly
30
- # is silently ignored).
25
+ # - `:not_directory` — the path exists but is not a directory (the loader only `add`s
26
+ # directories, so a `.rbs` file passed directly is silently ignored).
31
27
  # - `:empty` — a directory with no `.rbs` file (recursive).
32
28
  Entry = Data.define(:path, :status, :rbs_file_count) do
33
29
  def ok?
@@ -38,9 +34,9 @@ module Rigor
38
34
  !ok?
39
35
  end
40
36
 
41
- # One-line, human-facing reason. The wording matches the loader's
42
- # actual behaviour ("loaded nothing from it") rather than the
43
- # filesystem error, so the message points at the consequence.
37
+ # One-line, human-facing reason. The wording matches the loader's actual behaviour
38
+ # ("loaded nothing from it") rather than the filesystem error, so the message points
39
+ # at the consequence.
44
40
  def message
45
41
  case status
46
42
  when :missing
@@ -59,12 +55,11 @@ module Rigor
59
55
  end
60
56
  end
61
57
 
62
- # Audits each configured entry. `signature_paths` is the
63
- # {Configuration#signature_paths} array (absolute paths, already
64
- # resolved against the config file's directory). Pass `nil`the
65
- # unset default, where Rigor auto-detects `<root>/sig` to get an
66
- # empty result: an absent auto-detected `sig/` is a normal setup, not
67
- # a misconfiguration, so it is never audited.
58
+ # Audits each configured entry. `signature_paths` is the {Configuration#signature_paths}
59
+ # array (absolute paths, already resolved against the config file's directory). Pass
60
+ # `nil` the unset default, where Rigor auto-detects `<root>/sig` — to get an empty
61
+ # result: an absent auto-detected `sig/` is a normal setup, not a misconfiguration, so it
62
+ # is never audited.
68
63
  #
69
64
  # @param signature_paths [Array<String, Pathname>, nil]
70
65
  # @return [Array<Entry>]
@@ -72,8 +67,7 @@ module Rigor
72
67
  Array(signature_paths).map { |path| classify(path.to_s) }
73
68
  end
74
69
 
75
- # The subset of {audit} that resolved to nothing — the entries worth
76
- # warning about.
70
+ # The subset of {audit} that resolved to nothing — the entries worth warning about.
77
71
  #
78
72
  # @param signature_paths [Array<String, Pathname>, nil]
79
73
  # @return [Array<Entry>]