rigortype 0.2.7 → 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 (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -14,46 +14,33 @@ require_relative "sorbet/sigil_detector"
14
14
 
15
15
  module Rigor
16
16
  module Plugin
17
- # rigor-sorbet — ingests Sorbet `sig { ... }` blocks as
18
- # method-signature contributions to Rigor's analyzer.
17
+ # rigor-sorbet — ingests Sorbet `sig { ... }` blocks as method-signature contributions to Rigor's
18
+ # analyzer.
19
19
  #
20
20
  # ADR-11 slice 1 — first deliverable. Recognises:
21
21
  #
22
- # - `sig { params(x: Integer).returns(String) }` above a
23
- # `def foo(x)` definition, contributing the parsed return
24
- # type at every call site.
25
- # - The `void` terminus and the `abstract` / `override` /
26
- # `overridable` / `final` modifiers (recorded on the
27
- # {MethodSignature} for slice ≥2).
28
- # - `class Foo` / `module Foo::Bar` / `class << self`
29
- # nesting; `def self.foo` is recognised as a singleton
30
- # method.
22
+ # - `sig { params(x: Integer).returns(String) }` above a `def foo(x)` definition, contributing the
23
+ # parsed return type at every call site.
24
+ # - The `void` terminus and the `abstract` / `override` / `overridable` / `final` modifiers (recorded
25
+ # on the {MethodSignature} for slice ≥2).
26
+ # - `class Foo` / `module Foo::Bar` / `class << self` nesting; `def self.foo` is recognised as a
27
+ # singleton method.
31
28
  #
32
- # The {TypeTranslator} table documents coverage. Most of
33
- # Sorbet's vocabulary translates; remaining gaps (`T.proc`,
34
- # `T::Struct` subclasses, `T.attached_class`, etc.) degrade
35
- # silently to `Dynamic[top]`.
29
+ # The {TypeTranslator} table documents coverage. Most of Sorbet's vocabulary translates; remaining
30
+ # gaps (`T.proc`, `T::Struct` subclasses, `T.attached_class`, etc.) degrade silently to `Dynamic[top]`.
36
31
  #
37
- # Architecture: per-run `Catalog` is built lazily on first
38
- # access by walking every configured `paths:` entry's `.rb`
39
- # files plus every `rbi_paths:` entry's `.rbi` files (slice
40
- # 4) via the plugin's `IoBoundary`. The catalog is frozen
41
- # after the first build and consulted by the `dynamic_return`
42
- # rule at every gated call site. RBI files
43
- # share the catalog with project-source sigsboth produce
44
- # `MethodSignature` entries keyed by
45
- # `(class_name, method_name, kind)`. When a key collides
46
- # across files, the last-walked sig wins (ordering is
47
- # platform-dependent: `Dir.glob` returns directory entries
48
- # in filesystem order). Sorbet's full shim-override
49
- # semantics — `sorbet/rbi/shims/` overriding
50
- # `sorbet/rbi/gems/` — lands in a later slice once the
51
- # catalog gains per-source provenance.
32
+ # Architecture: per-run `Catalog` is built lazily on first access by walking every configured `paths:`
33
+ # entry's `.rb` files plus every `rbi_paths:` entry's `.rbi` files (slice 4) via the plugin's
34
+ # `IoBoundary`. The catalog is frozen after the first build and consulted by the `dynamic_return` rule
35
+ # at every gated call site. RBI files share the catalog with project-source sigs — both produce
36
+ # `MethodSignature` entries keyed by `(class_name, method_name, kind)`. When a key collides across
37
+ # files, the last-walked sig wins (ordering is platform-dependent: `Dir.glob` returns directory
38
+ # entries in filesystem order). Sorbet's full shim-override semantics`sorbet/rbi/shims/` overriding
39
+ # `sorbet/rbi/gems/` lands in a later slice once the catalog gains per-source provenance.
52
40
  #
53
- # The plugin emits `plugin.sorbet.parse-error` warnings for
54
- # malformed sig blocks (no block / empty block / no
55
- # `returns` or `void` terminus / two consecutive sigs / sig
56
- # not followed by a def) but never aborts a run.
41
+ # The plugin emits `plugin.sorbet.parse-error` warnings for malformed sig blocks (no block / empty
42
+ # block / no `returns` or `void` terminus / two consecutive sigs / sig not followed by a def) but
43
+ # never aborts a run.
57
44
  #
58
45
  # ## Configuration
59
46
  #
@@ -63,27 +50,23 @@ module Rigor
63
50
  # paths: ["lib", "app"] # directories to scan for `.rb` sigs; defaults to `paths:`
64
51
  # rbi_paths: ["sorbet/rbi"] # directories to scan for `.rbi` files; default shown
65
52
  #
66
- # The `paths:` config key narrows the plugin's `.rb` walk;
67
- # omit it to inherit the project-wide `paths:` value. The
68
- # `rbi_paths:` key controls where Sorbet's RBI tree is read
69
- # from defaults to `sorbet/rbi/` per Tapioca's standard
70
- # layout (`gems/`, `annotations/`, `dsl/`, `shims/`). Set
71
- # to `[]` to opt out of RBI loading entirely.
53
+ # The `paths:` config key narrows the plugin's `.rb` walk; omit it to inherit the project-wide
54
+ # `paths:` value. The `rbi_paths:` key controls where Sorbet's RBI tree is read from — defaults to
55
+ # `sorbet/rbi/` per Tapioca's standard layout (`gems/`, `annotations/`, `dsl/`, `shims/`). Set to `[]`
56
+ # to opt out of RBI loading entirely.
72
57
  class Sorbet < Rigor::Plugin::Base
73
58
  manifest(
74
59
  id: "sorbet",
75
60
  version: "0.1.0",
76
61
  description: "Ingests Sorbet `sig` blocks as method-signature contributions.",
77
62
  config_schema: {
78
- # `paths` keeps a `fetch`-with-default in `init` because its
79
- # default is dynamic (the project's configured `paths`), not a
80
- # static literal the manifest can declare.
63
+ # `paths` keeps a `fetch`-with-default in `init` because its default is dynamic (the project's
64
+ # configured `paths`), not a static literal the manifest can declare.
81
65
  "paths" => :array,
82
- # Default RBI directory tree. Matches the layout `tapioca init`
83
- # generates — see Sorbet's `rbi.md`. Slice 4 walks every `.rbi`
84
- # file under these roots recursively; the four standard Tapioca
85
- # subdirectories (`gems` / `annotations` / `dsl` / `shims`) are
86
- # picked up as a side effect of recursing into the parent root.
66
+ # Default RBI directory tree. Matches the layout `tapioca init` generates — see Sorbet's
67
+ # `rbi.md`. Slice 4 walks every `.rbi` file under these roots recursively; the four standard
68
+ # Tapioca subdirectories (`gems` / `annotations` / `dsl` / `shims`) are picked up as a side
69
+ # effect of recursing into the parent root.
87
70
  "rbi_paths" => { kind: :array, default: ["sorbet/rbi"] },
88
71
  "enforce_sigil" => { kind: :boolean, default: true }
89
72
  }
@@ -93,97 +76,70 @@ module Rigor
93
76
  @services = services
94
77
  @configured_paths = Array(config.fetch("paths", services.configuration.paths)).map(&:to_s)
95
78
  @rbi_paths = Array(config.fetch("rbi_paths")).map(&:to_s)
96
- # Schema default `true` — only files marked `# typed: true` /
97
- # `:strict` / `:strong` contribute their sigs. Set to
98
- # `false` to record every file's sigs regardless of
99
- # sigil (current behaviour pre-this-config).
79
+ # Schema default `true` — only files marked `# typed: true` / `:strict` / `:strong` contribute
80
+ # their sigs. Set to `false` to record every file's sigs regardless of sigil (current behaviour
81
+ # pre-this-config).
100
82
  @enforce_sigil = config.fetch("enforce_sigil")
101
- # Per-call-site assertion gating: `@sigil_by_path` (built
102
- # during catalog harvest) is consulted so `T.let` /
103
- # `T.cast` / `T.must` / `T.bind` / `T.assert_type!` only
104
- # fire in files Sorbet itself would enforce (`:true` /
105
- # `:strict` / `:strong`). With `enforce_sigil: false` the
106
- # gate is open everywhere. Missing-sigil paths (synthetic
107
- # fixtures, out-of-tree call sites) default to enforced —
108
- # failing-open suits spec ergonomics better than
109
- # failing-closed. See `assertion_enforced_here?`.
83
+ # Per-call-site assertion gating: `@sigil_by_path` (built during catalog harvest) is consulted so
84
+ # `T.let` / `T.cast` / `T.must` / `T.bind` / `T.assert_type!` only fire in files Sorbet itself
85
+ # would enforce (`:true` / `:strict` / `:strong`). With `enforce_sigil: false` the gate is open
86
+ # everywhere. Missing-sigil paths (synthetic fixtures, out-of-tree call sites) default to enforced
87
+ # failing-open suits spec ergonomics better than failing-closed. See `assertion_enforced_here?`.
110
88
  @sigil_by_path = {}
111
89
  @catalog = nil
112
90
  @parse_errors_by_path = {}
113
91
  @catalog_built = false
114
- # ADR-11 slice 6 — Prism nodes for `T.absurd` calls
115
- # we observed in the `dynamic_return` rule to be
116
- # *reachable* (i.e., their discriminant didn't narrow
117
- # to `bot`). `diagnostics_for_file` walks the per-file
118
- # AST and surfaces these as `plugin.sorbet.absurd-reachable`
119
- # warnings. Hash is keyed on the Prism node's
120
- # `object_id` because the runner only parses each file
121
- # once per run, so identity is stable across the two
122
- # plugin hooks.
92
+ # ADR-11 slice 6 — Prism nodes for `T.absurd` calls we observed in the `dynamic_return` rule to be
93
+ # *reachable* (i.e., their discriminant didn't narrow to `bot`). `diagnostics_for_file` walks the
94
+ # per-file AST and surfaces these as `plugin.sorbet.absurd-reachable` warnings. Hash is keyed on
95
+ # the Prism node's `object_id` because the runner only parses each file once per run, so identity
96
+ # is stable across the two plugin hooks.
123
97
  @reachable_absurd_nodes = {}.compare_by_identity
124
- # ADR-11 light follow-up — `T.reveal_type` calls
125
- # observed in the `dynamic_return` rule, paired with the
126
- # display string for the inferred type at the call site.
127
- # Mirrors the absurd-node compare-by-identity hash;
128
- # `diagnostics_for_file` surfaces each entry as a
98
+ # ADR-11 light follow-up — `T.reveal_type` calls observed in the `dynamic_return` rule, paired
99
+ # with the display string for the inferred type at the call site. Mirrors the absurd-node
100
+ # compare-by-identity hash; `diagnostics_for_file` surfaces each entry as a
129
101
  # `plugin.sorbet.reveal-type` `:info` diagnostic.
130
102
  @reveal_type_calls = {}.compare_by_identity
131
- # T.bind / T.assert_type! priority slice 1 —
132
- # `T.assert_type!` calls observed in
133
- # the `dynamic_return` rule whose static subtype check
134
- # FAILED, paired with the inferred + asserted type
135
- # display strings. Same compare-by-identity discipline.
136
- # `diagnostics_for_file` walks the file AST for
137
- # `T.assert_type!` calls and surfaces matching entries
138
- # as `plugin.sorbet.assert-type-mismatch` `:error`
139
- # diagnostics.
103
+ # T.bind / T.assert_type! priority slice 1 — `T.assert_type!` calls observed in the
104
+ # `dynamic_return` rule whose static subtype check FAILED, paired with the inferred + asserted
105
+ # type display strings. Same compare-by-identity discipline. `diagnostics_for_file` walks the
106
+ # file AST for `T.assert_type!` calls and surfaces matching entries as
107
+ # `plugin.sorbet.assert-type-mismatch` `:error` diagnostics.
140
108
  @assert_type_mismatches = {}.compare_by_identity
141
109
  end
142
110
 
143
111
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
144
112
  ensure_catalog
145
- # The catalog records errors under the canonicalised
146
- # (realpath-resolved) form; the runner may pass the
147
- # symlink-bearing form here. Look up under both so the
148
- # match is symlink-agnostic.
113
+ # The catalog records errors under the canonicalised (realpath-resolved) form; the runner may pass
114
+ # the symlink-bearing form here. Look up under both so the match is symlink-agnostic.
149
115
  errors = @parse_errors_by_path[path] || @parse_errors_by_path[canonicalize(path)] || []
150
116
  errors.map { |error| parse_error_diagnostic(path, error) }
151
117
  end
152
118
 
153
- # ADR-52 slice 4 — per-call return-type path via the
154
- # method-name-gated `dynamic_return` DSL. The recognised name
155
- # set is only known at run time (the catalog's `def` names come
156
- # from the lazy catalog build), so it is declared as a
157
- # callable: the engine `instance_exec`s it once per run on
158
- # first dispatch (always after `#prepare`) and memoises the
159
- # resolved Symbol Set. The gate is a safe over-approximation —
160
- # a project method merely *named* `cast` or `find` passes it
161
- # and is declined by the block's own `T.`-receiver / catalog
162
- # checks.
119
+ # ADR-52 slice 4 — per-call return-type path via the method-name-gated `dynamic_return` DSL. The
120
+ # recognised name set is only known at run time (the catalog's `def` names come from the lazy
121
+ # catalog build), so it is declared as a callable: the engine `instance_exec`s it once per run on
122
+ # first dispatch (always after `#prepare`) and memoises the resolved Symbol Set. The gate is a safe
123
+ # over-approximation a project method merely *named* `cast` or `find` passes it and is declined by
124
+ # the block's own `T.`-receiver / catalog checks.
163
125
  dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
164
126
  contribution_return_type(call_node, scope)
165
127
  end
166
128
 
167
- # ADR-52 slice 4 — `T.bind(self, T)`'s self-narrowing fact,
168
- # contributed via the method-gated `narrowing_facts` DSL. The
169
- # statement evaluator consults this path for narrowing facts.
170
- # The return-type half (`Constant[nil]`) flows through the
171
- # `dynamic_return` rule above; the block re-checks the `T.`
172
- # receiver via the recogniser, so an unrelated `bind` call
173
- # contributes nothing.
129
+ # ADR-52 slice 4 — `T.bind(self, T)`'s self-narrowing fact, contributed via the method-gated
130
+ # `narrowing_facts` DSL. The statement evaluator consults this path for narrowing facts. The
131
+ # return-type half (`Constant[nil]`) flows through the `dynamic_return` rule above; the block
132
+ # re-checks the `T.` receiver via the recogniser, so an unrelated `bind` call contributes nothing.
174
133
  narrowing_facts methods: [:bind] do |call_node, scope|
175
134
  bind_post_return_facts(call_node, scope)
176
135
  end
177
136
 
178
- # ADR-37 — the three per-call diagnostics ride the engine-owned
179
- # walk instead of three hand-rolled `walk_for_*` recursions. Each
180
- # candidate `T.` call is *recorded by object identity* during the
181
- # inference pass (the `dynamic_return` / `narrowing_facts` rules
182
- # above call `record_*`), so by the time these node rules fire in
183
- # the diagnostics phase the sets are populated; the membership
184
- # `delete` both gates the emission and pops the entry so a re-run
185
- # cannot double-fire. The recorded set is the gate — no per-node
186
- # `AbsurdRecognizer` / name check is needed here.
137
+ # ADR-37 — the three per-call diagnostics ride the engine-owned walk instead of three hand-rolled
138
+ # `walk_for_*` recursions. Each candidate `T.` call is *recorded by object identity* during the
139
+ # inference pass (the `dynamic_return` / `narrowing_facts` rules above call `record_*`), so by the
140
+ # time these node rules fire in the diagnostics phase the sets are populated; the membership
141
+ # `delete` both gates the emission and pops the entry so a re-run cannot double-fire. The recorded
142
+ # set is the gate no per-node `AbsurdRecognizer` / name check is needed here.
187
143
  node_rule Prism::CallNode do |node, _scope, path|
188
144
  next [] unless @reachable_absurd_nodes.delete(node)
189
145
 
@@ -206,10 +162,8 @@ module Rigor
206
162
 
207
163
  private
208
164
 
209
- # Run-time method-name gate for the `dynamic_return` rule
210
- # (ADR-52 slice 4): the static assertion vocabulary
211
- # (`T.let` / `T.cast` / …), `T.absurd`, and every method name
212
- # the catalog carries a sig for.
165
+ # Run-time method-name gate for the `dynamic_return` rule (ADR-52 slice 4): the static assertion
166
+ # vocabulary (`T.let` / `T.cast` / …), `T.absurd`, and every method name the catalog carries a sig for.
213
167
  def recognised_method_names
214
168
  ensure_catalog
215
169
  names = AssertionRecognizer::SORBET_ASSERTIONS.dup
@@ -218,54 +172,39 @@ module Rigor
218
172
  names
219
173
  end
220
174
 
221
- # Main contribution body for the `dynamic_return` rule.
222
- # Returns the bare `Rigor::Type` the contract expects.
223
- # Resolves the receiver in three passes:
175
+ # Main contribution body for the `dynamic_return` rule. Returns the bare `Rigor::Type` the contract
176
+ # expects. Resolves the receiver in three passes:
224
177
  #
225
- # 1. Constant receiver (`User.find(...)`) → singleton-side
226
- # catalog lookup.
227
- # 2. Nominal receiver-type (`user.name` where `user`'s
228
- # inferred type is `Nominal["User"]`) → instance-side
229
- # catalog lookup.
230
- # 3. Implicit-self (receiver-less inside a method body) →
231
- # current-class lookup via `implicit_self_lookup`.
178
+ # 1. Constant receiver (`User.find(...)`) → singleton-side catalog lookup.
179
+ # 2. Nominal receiver-type (`user.name` where `user`'s inferred type is `Nominal["User"]`) →
180
+ # instance-side catalog lookup.
181
+ # 3. Implicit-self (receiver-less inside a method body) → current-class lookup via
182
+ # `implicit_self_lookup`.
232
183
  def contribution_return_type(call_node, scope)
233
184
  return nil unless call_node.is_a?(Prism::CallNode)
234
185
 
235
- # ADR-11 slice 6 — `T.absurd(x)` exhaustiveness. Always
236
- # contributes a `bot` return (matches Sorbet's runtime
237
- # behaviour: `T.absurd` raises, so its value type is `bot`
238
- # and the engine's flow analysis treats code after it as
239
- # unreachable; the legacy contribution's `exceptional:
240
- # :raises` slot was never consumed on the dispatcher path —
241
- # only `return_type` survives the merge). When the
242
- # discriminant *isn't* narrowed to `bot` at this scope, also
243
- # records the call node so `diagnostics_for_file` can surface
244
- # a `plugin.sorbet.absurd-reachable` warning.
186
+ # ADR-11 slice 6 — `T.absurd(x)` exhaustiveness. Always contributes a `bot` return (matches
187
+ # Sorbet's runtime behaviour: `T.absurd` raises, so its value type is `bot` and the engine's flow
188
+ # analysis treats code after it as unreachable; the legacy contribution's `exceptional: :raises`
189
+ # slot was never consumed on the dispatcher path only `return_type` survives the merge). When
190
+ # the discriminant *isn't* narrowed to `bot` at this scope, also records the call node so
191
+ # `diagnostics_for_file` can surface a `plugin.sorbet.absurd-reachable` warning.
245
192
  if AbsurdRecognizer.absurd_call?(call_node)
246
193
  @reachable_absurd_nodes[call_node] = true unless AbsurdRecognizer.exhaustive?(call_node, scope)
247
194
  return Rigor::Type::Combinator.bot
248
195
  end
249
196
 
250
- # ADR-11 slice 2 — `T.let` / `T.cast` / `T.must` /
251
- # `T.unsafe` are checked first because they're cheaper
252
- # to recognise (no catalog walk required) and they
253
- # win over any cataloged signature: the user explicitly
254
- # asserted the type at the call site. The light
255
- # follow-up extends the recogniser to `T.must_because`
256
- # (alias of `T.must`) and `T.reveal_type` (passes the
257
- # type through; the human-facing diagnostic is recorded
258
- # here for `diagnostics_for_file` to emit).
197
+ # ADR-11 slice 2 — `T.let` / `T.cast` / `T.must` / `T.unsafe` are checked first because they're
198
+ # cheaper to recognise (no catalog walk required) and they win over any cataloged signature: the
199
+ # user explicitly asserted the type at the call site. The light follow-up extends the recogniser
200
+ # to `T.must_because` (alias of `T.must`) and `T.reveal_type` (passes the type through; the
201
+ # human-facing diagnostic is recorded here for `diagnostics_for_file` to emit).
259
202
  #
260
- # Per-call-site sigil gating: with `enforce_sigil: true`
261
- # (default), assertions only fire in files Sorbet itself
262
- # would enforce. Files at `# typed: false` (or
263
- # sigil-less, which Sorbet treats as `:false`) skip the
264
- # assertion path entirely so the dispatcher continues
265
- # through the next tier as if the wrapper weren't
266
- # there. The catalog tier already gates by sigil at
267
- # harvest time; this closes the matching gap for
268
- # caller-side recognition.
203
+ # Per-call-site sigil gating: with `enforce_sigil: true` (default), assertions only fire in files
204
+ # Sorbet itself would enforce. Files at `# typed: false` (or sigil-less, which Sorbet treats as
205
+ # `:false`) skip the assertion path entirely so the dispatcher continues through the next tier as
206
+ # if the wrapper weren't there. The catalog tier already gates by sigil at harvest time; this
207
+ # closes the matching gap for caller-side recognition.
269
208
  ensure_catalog
270
209
  if assertion_enforced_here?(scope)
271
210
  assertion = AssertionRecognizer.recognize(
@@ -283,10 +222,9 @@ module Rigor
283
222
  lookup_signature(call_node, scope)&.return_type
284
223
  end
285
224
 
286
- # The `narrowing_facts` body for `T.bind` — same sigil gate as
287
- # the return-type path, then the recogniser's
288
- # `post_return_facts` (the `Fact(target_kind: :self)` that
289
- # narrows `scope.self_type` for the rest of the block).
225
+ # The `narrowing_facts` body for `T.bind` — same sigil gate as the return-type path, then the
226
+ # recogniser's `post_return_facts` (the `Fact(target_kind: :self)` that narrows `scope.self_type`
227
+ # for the rest of the block).
290
228
  def bind_post_return_facts(call_node, scope)
291
229
  return nil unless call_node.is_a?(Prism::CallNode)
292
230
 
@@ -299,24 +237,16 @@ module Rigor
299
237
  contribution&.post_return_facts
300
238
  end
301
239
 
302
- # Per-call-site assertion gating (ADR-11). With
303
- # `enforce_sigil: false` the gate is fully open. With
304
- # `enforce_sigil: true` (default), the caller file's
305
- # sigil must reach `:true` / `:strict` / `:strong` for
306
- # assertions to fire. Three honest fallbacks:
240
+ # Per-call-site assertion gating (ADR-11). With `enforce_sigil: false` the gate is fully open. With
241
+ # `enforce_sigil: true` (default), the caller file's sigil must reach `:true` / `:strict` /
242
+ # `:strong` for assertions to fire. Three honest fallbacks:
307
243
  #
308
- # - `scope.source_path` is nil — synthetic call sites
309
- # (specs, virtual-node fixtures) have no file context.
310
- # Default to enforced so existing recogniser tests
311
- # keep working.
312
- # - the path is canonicalised to a form not in
313
- # `@sigil_by_path` the harvest never saw this file
314
- # (out-of-tree call site, or a path the
315
- # `configured_paths` config excluded). Sorbet itself
316
- # has no opinion on such files; default to enforced
317
- # so the recogniser still fires.
318
- # - the path IS in `@sigil_by_path` but at `:false` /
319
- # `:ignore` — gate closes.
244
+ # - `scope.source_path` is nil — synthetic call sites (specs, virtual-node fixtures) have no file
245
+ # context. Default to enforced so existing recogniser tests keep working.
246
+ # - the path is canonicalised to a form not in `@sigil_by_path` — the harvest never saw this file
247
+ # (out-of-tree call site, or a path the `configured_paths` config excluded). Sorbet itself has no
248
+ # opinion on such files; default to enforced so the recogniser still fires.
249
+ # - the path IS in `@sigil_by_path` but at `:false` / `:ignore` gate closes.
320
250
  def assertion_enforced_here?(scope)
321
251
  return true unless @enforce_sigil
322
252
 
@@ -336,8 +266,8 @@ module Rigor
336
266
  return nil if method_name.nil?
337
267
 
338
268
  if (singleton_target = constant_receiver_name(receiver))
339
- # `Post.find(...)` — direct singleton method, or
340
- # `extend M` lifting `M#find` to the extending class.
269
+ # `Post.find(...)` — direct singleton method, or `extend M` lifting `M#find` to the extending
270
+ # class.
341
271
  chain_lookup(singleton_target, method_name, anchor_kind: :singleton, mixin_kind: :extend)
342
272
  elsif receiver
343
273
  instance_chain_lookup(receiver, method_name, scope)
@@ -347,16 +277,13 @@ module Rigor
347
277
  end
348
278
 
349
279
  # ADR-11 slice 2 — implicit-self calls.
350
- # A receiver-less call inside a method body resolves against the
351
- # engine's own `scope.self_type`: `Nominal[Foo]` inside an
352
- # instance method (instance-side lookup), `Singleton[Foo]` inside
353
- # a `def self.x` body (singleton-side lookup, `extend` mixins).
354
- # Without this, an enforced sig on a sibling method was invisible
355
- # to in-class calls the engine's body-inference tiers then
356
- # re-typed the sibling's body, overriding an explicit
357
- # `T.untyped` opt-out (the dispatcher's plugin tier had already
358
- # run and declined). Anything else (toplevel / Dynamic / DSL
359
- # self) contributes nothing and the dispatcher continues.
280
+ # A receiver-less call inside a method body resolves against the engine's own `scope.self_type`:
281
+ # `Nominal[Foo]` inside an instance method (instance-side lookup), `Singleton[Foo]` inside a `def
282
+ # self.x` body (singleton-side lookup, `extend` mixins). Without this, an enforced sig on a sibling
283
+ # method was invisible to in-class calls the engine's body-inference tiers then re-typed the
284
+ # sibling's body, overriding an explicit `T.untyped` opt-out (the dispatcher's plugin tier had
285
+ # already run and declined). Anything else (toplevel / Dynamic / DSL self) contributes nothing and
286
+ # the dispatcher continues.
360
287
  def implicit_self_lookup(method_name, scope)
361
288
  self_type = scope&.self_type
362
289
  case self_type
@@ -377,9 +304,8 @@ module Rigor
377
304
 
378
305
  chain_lookup(receiver_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include)
379
306
  rescue StandardError
380
- # `scope.type_of` can raise on unrecognised synthetic
381
- # nodes; degrade to "no contribution" rather than
382
- # bubbling the failure into the dispatcher.
307
+ # `scope.type_of` can raise on unrecognised synthetic nodes; degrade to "no contribution" rather
308
+ # than bubbling the failure into the dispatcher.
383
309
  nil
384
310
  end
385
311
 
@@ -387,15 +313,13 @@ module Rigor
387
313
  #
388
314
  # For instance-side calls (`post.body`):
389
315
  # - `anchor_kind: :instance` (try `Post#body` first)
390
- # - `mixin_kind: :include` (then walk Post's `include`d
391
- # modules and try `Foo#body` on each)
316
+ # - `mixin_kind: :include` (then walk Post's `include`d modules and try `Foo#body` on each)
392
317
  #
393
318
  # For singleton-side calls (`Post.find`):
394
319
  # - `anchor_kind: :singleton` (try `Post.find` first)
395
- # - `mixin_kind: :extend` (then walk Post's `extend`ed
396
- # modules and try `Foo#find` *as :instance* `extend
397
- # Foo` lifts Foo's INSTANCE methods to the extending
398
- # class's SINGLETON methods, matching Ruby's MRO).
320
+ # - `mixin_kind: :extend` (then walk Post's `extend`ed modules and try `Foo#find` *as :instance* —
321
+ # `extend Foo` lifts Foo's INSTANCE methods to the extending class's SINGLETON methods, matching
322
+ # Ruby's MRO).
399
323
  def chain_lookup(class_name, method_name, anchor_kind:, mixin_kind:)
400
324
  each_class_form(class_name).each do |form|
401
325
  sig = @catalog.lookup(class_name: form, method_name: method_name, kind: anchor_kind)
@@ -413,8 +337,7 @@ module Rigor
413
337
  sig = @catalog.lookup(class_name: form, method_name: method_name, kind: :instance)
414
338
  return sig if sig
415
339
 
416
- # Transitive: an `include` inside the mixed-in
417
- # module is also inherited by the host class.
340
+ # Transitive: an `include` inside the mixed-in module is also inherited by the host class.
418
341
  mixin_modules_for(form, :include).each do |inner|
419
342
  queue << inner unless visited.include?(inner)
420
343
  end
@@ -424,20 +347,16 @@ module Rigor
424
347
  nil
425
348
  end
426
349
 
427
- # `Post` and `::Post` are routinely confused at the catalog
428
- # boundary (the walker records the lexical name; user code
429
- # often writes the rooted form). Try both at every lookup.
350
+ # `Post` and `::Post` are routinely confused at the catalog boundary (the walker records the lexical
351
+ # name; user code often writes the rooted form). Try both at every lookup.
430
352
  def each_class_form(class_name)
431
353
  [class_name, "::#{class_name}"]
432
354
  end
433
355
 
434
- # Resolution forms for a mixed-in module name. Tapioca's
435
- # generated DSL RBIs use the nested form
436
- # (`class Post; module GeneratedAttributeMethods; ...; end`);
437
- # hand-written shims often use the top-level form
438
- # (`module GeneratedAttributeMethods; ...; end` outside any
439
- # class); explicit rooting (`::GeneratedAttributeMethods`)
440
- # is occasionally seen. Try all three.
356
+ # Resolution forms for a mixed-in module name. Tapioca's generated DSL RBIs use the nested form
357
+ # (`class Post; module GeneratedAttributeMethods; ...; end`); hand-written shims often use the
358
+ # top-level form (`module GeneratedAttributeMethods; ...; end` outside any class); explicit rooting
359
+ # (`::GeneratedAttributeMethods`) is occasionally seen. Try all three.
441
360
  def forms_for_mixin(host_class, mixin_name)
442
361
  if mixin_name.start_with?("::")
443
362
  [mixin_name, mixin_name.delete_prefix("::")]
@@ -486,8 +405,7 @@ module Rigor
486
405
 
487
406
  # @param root [String] directory or single file.
488
407
  # @param catalog [Catalog]
489
- # @param extensions [Array<String>] file extensions to
490
- # accept (e.g. `[".rb"]` for project source,
408
+ # @param extensions [Array<String>] file extensions to accept (e.g. `[".rb"]` for project source,
491
409
  # `[".rbi"]` for Sorbet RBI tree).
492
410
  def harvest_path(root, catalog, extensions:)
493
411
  absolute = canonicalize(root)
@@ -498,18 +416,15 @@ module Rigor
498
416
  end
499
417
  end
500
418
  elsif File.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
501
- # `paths:` may list individual files (the demos do
502
- # this); walk them directly rather than skipping.
419
+ # `paths:` may list individual files (the demos do this); walk them directly rather than skipping.
503
420
  harvest_file(absolute, catalog)
504
421
  end
505
422
  end
506
423
 
507
- # Canonicalises a path through `File.realpath` so it
508
- # matches the form `Plugin::TrustPolicy#allow_read?` sees
509
- # (the runner builds the policy's roots from `Dir.pwd`,
510
- # which has symlinks resolved on macOS — `/tmp` →
511
- # `/private/tmp` etc.). Falls back to `File.expand_path`
512
- # when realpath fails (e.g. the path no longer exists).
424
+ # Canonicalises a path through `File.realpath` so it matches the form `Plugin::TrustPolicy#allow_read?`
425
+ # sees (the runner builds the policy's roots from `Dir.pwd`, which has symlinks resolved on macOS —
426
+ # `/tmp` `/private/tmp` etc.). Falls back to `File.expand_path` when realpath fails (e.g. the path
427
+ # no longer exists).
513
428
  def canonicalize(path)
514
429
  expanded = File.expand_path(path)
515
430
  File.exist?(expanded) ? File.realpath(expanded) : expanded
@@ -521,31 +436,23 @@ module Rigor
521
436
  contents = io_boundary.read_file(path)
522
437
  return if contents.nil?
523
438
 
524
- # ADR-11 slice 5 — honour Sorbet's `# typed: ignore`
525
- # magic comment by skipping the file entirely.
439
+ # ADR-11 slice 5 — honour Sorbet's `# typed: ignore` magic comment by skipping the file entirely.
526
440
  level = SigilDetector.detect(contents)
527
- # Per-call-site assertion gating consults this map at
528
- # recognition time. Recorded BEFORE the ignored
529
- # short-circuit so a `# typed: ignore` file still
530
- # reports its level to the gate (the gate then chooses
531
- # to suppress assertions there too — `ignore` is
532
- # stricter than `false`).
441
+ # Per-call-site assertion gating consults this map at recognition time. Recorded BEFORE the
442
+ # ignored short-circuit so a `# typed: ignore` file still reports its level to the gate (the gate
443
+ # then chooses to suppress assertions there too — `ignore` is stricter than `false`).
533
444
  @sigil_by_path[path] = level
534
445
  return if SigilDetector.ignored?(level)
535
446
 
536
447
  result = Prism.parse(contents)
537
448
  return unless result.errors.empty?
538
449
 
539
- # `enforce_sigil` follow-up — when on (default), files
540
- # at `:false` (or sigil-less, which Sorbet treats as
541
- # `:false`) are STILL walked so parse-error diagnostics
542
- # surface, but sigs flow into a discardable catalog
543
- # rather than the per-run one. Sorbet itself doesn't
544
- # enforce types at `# typed: false`, and Rigor mirrors
545
- # that for sig contributions. Assertion recognisers
546
- # (`T.let` / `T.cast` / `T.must` / `T.bind` /
547
- # `T.assert_type!`) stay live regardless of sigil — the
548
- # user wrote those deliberately.
450
+ # `enforce_sigil` follow-up — when on (default), files at `:false` (or sigil-less, which Sorbet
451
+ # treats as `:false`) are STILL walked so parse-error diagnostics surface, but sigs flow into a
452
+ # discardable catalog rather than the per-run one. Sorbet itself doesn't enforce types at `#
453
+ # typed: false`, and Rigor mirrors that for sig contributions. Assertion recognisers (`T.let` /
454
+ # `T.cast` / `T.must` / `T.bind` / `T.assert_type!`) stay live regardless of sigil — the user
455
+ # wrote those deliberately.
549
456
  sig_catalog = if @enforce_sigil && !SigilDetector.enforced?(level)
550
457
  Catalog.new
551
458
  else
@@ -555,15 +462,13 @@ module Rigor
555
462
  errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path)
556
463
  @parse_errors_by_path[path] = errors unless errors.empty?
557
464
  rescue Plugin::AccessDeniedError, Errno::ENOENT
558
- # Skip files outside the trusted read scope or that
559
- # vanished between glob and read; the plugin produces
560
- # no output for them.
465
+ # Skip files outside the trusted read scope or that vanished between glob and read; the plugin
466
+ # produces no output for them.
561
467
  nil
562
468
  end
563
469
 
564
- # Emits a `plugin.sorbet.absurd-reachable` warning for the
565
- # `T.absurd(x)` call recorded in `@reachable_absurd_nodes` during
566
- # inference; the node rule above does the identity match and pop.
470
+ # Emits a `plugin.sorbet.absurd-reachable` warning for the `T.absurd(x)` call recorded in
471
+ # `@reachable_absurd_nodes` during inference; the node rule above does the identity match and pop.
567
472
  def absurd_diagnostic(path, call_node)
568
473
  Rigor::Analysis::Diagnostic.from_node(
569
474
  call_node,
@@ -576,19 +481,16 @@ module Rigor
576
481
  )
577
482
  end
578
483
 
579
- # ADR-11 light follow-up — `T.reveal_type(expr)` records
580
- # the inferred type at recogniser time so the per-file
581
- # diagnostic hook can surface the human-facing message.
582
- # The reveal call's contribution already preserved the
583
- # inferred type for downstream chaining; this hash carries
584
- # the *display* string that the diagnostic shows.
484
+ # ADR-11 light follow-up — `T.reveal_type(expr)` records the inferred type at recogniser time so the
485
+ # per-file diagnostic hook can surface the human-facing message. The reveal call's contribution
486
+ # already preserved the inferred type for downstream chaining; this hash carries the *display*
487
+ # string that the diagnostic shows.
585
488
  def record_reveal_type_call(call_node, return_type)
586
489
  @reveal_type_calls[call_node] = display_for_type(return_type)
587
490
  end
588
491
 
589
492
  def display_for_type(type)
590
- # `Type#describe` is the human-facing display contract
591
- # used by `rigor type-of`'s text renderer.
493
+ # `Type#describe` is the human-facing display contract used by `rigor type-of`'s text renderer.
592
494
  return "untyped" if type.nil?
593
495
 
594
496
  type.respond_to?(:describe) ? type.describe : type.inspect
@@ -604,16 +506,11 @@ module Rigor
604
506
  )
605
507
  end
606
508
 
607
- # T.bind / T.assert_type! priority slice 1 — runs the
608
- # static subtype check at recogniser time and records the
609
- # call only when the inferred type is *provably
610
- # incompatible* with the asserted type. Gradual
611
- # consistency rules (`Type#accepts` mode `:gradual`): a
612
- # `Dynamic[top]` inferred type
613
- # silences the check; a definite `:no` records for
614
- # diagnostic emission; `:maybe` (uncertain) is treated as
615
- # "trust the user" and silenced — the runtime check is
616
- # there for those cases.
509
+ # T.bind / T.assert_type! priority slice 1 — runs the static subtype check at recogniser time and
510
+ # records the call only when the inferred type is *provably incompatible* with the asserted type.
511
+ # Gradual consistency rules (`Type#accepts` mode `:gradual`): a `Dynamic[top]` inferred type
512
+ # silences the check; a definite `:no` records for diagnostic emission; `:maybe` (uncertain) is
513
+ # treated as "trust the user" and silenced — the runtime check is there for those cases.
617
514
  def record_assert_type_check(call_node, scope)
618
515
  check = AssertionRecognizer.assert_type_check(call_node, scope)
619
516
  return if check.nil?