rigortype 0.2.7 → 0.2.9

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 (433) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/08-understanding-errors.md +3 -3
  8. data/docs/manual/01-installation.md +1 -0
  9. data/docs/manual/02-cli-reference.md +16 -7
  10. data/docs/manual/04-diagnostics.md +7 -4
  11. data/docs/manual/06-baseline.md +5 -2
  12. data/docs/manual/07-plugins.md +1 -1
  13. data/docs/manual/14-rails-quickstart.md +4 -2
  14. data/docs/manual/15-type-protection-coverage.md +27 -4
  15. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  16. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  17. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  18. data/lib/rigor/analysis/baseline.rb +56 -96
  19. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  20. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  21. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  23. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  24. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  25. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  26. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  27. data/lib/rigor/analysis/check_rules.rb +174 -274
  28. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  29. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  30. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  31. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  32. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  33. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  34. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  35. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  36. data/lib/rigor/analysis/diagnostic.rb +48 -75
  37. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  38. data/lib/rigor/analysis/fact_store.rb +12 -22
  39. data/lib/rigor/analysis/incremental.rb +47 -63
  40. data/lib/rigor/analysis/incremental_session.rb +74 -100
  41. data/lib/rigor/analysis/project_scan.rb +12 -23
  42. data/lib/rigor/analysis/result.rb +3 -5
  43. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  44. data/lib/rigor/analysis/run_stats.rb +38 -61
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  48. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  49. data/lib/rigor/analysis/runner.rb +220 -333
  50. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  51. data/lib/rigor/analysis/worker_session.rb +64 -102
  52. data/lib/rigor/ast/type_node.rb +6 -8
  53. data/lib/rigor/ast.rb +6 -10
  54. data/lib/rigor/bleeding_edge.rb +27 -35
  55. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  56. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  57. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  58. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  59. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  60. data/lib/rigor/cache/descriptor.rb +58 -94
  61. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  62. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  63. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  64. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  65. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  66. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  67. data/lib/rigor/cache/rbs_environment.rb +11 -19
  68. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  69. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  70. data/lib/rigor/cache/store.rb +248 -186
  71. data/lib/rigor/cli/annotate_command.rb +51 -88
  72. data/lib/rigor/cli/baseline_command.rb +10 -17
  73. data/lib/rigor/cli/check_command.rb +121 -199
  74. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  75. data/lib/rigor/cli/ci_detector.rb +9 -14
  76. data/lib/rigor/cli/command.rb +8 -14
  77. data/lib/rigor/cli/coverage_command.rb +62 -48
  78. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  79. data/lib/rigor/cli/coverage_report.rb +0 -2
  80. data/lib/rigor/cli/coverage_scan.rb +7 -11
  81. data/lib/rigor/cli/diagnostic_formats.rb +46 -62
  82. data/lib/rigor/cli/diff_command.rb +10 -20
  83. data/lib/rigor/cli/docs_command.rb +26 -40
  84. data/lib/rigor/cli/doctor_command.rb +8 -9
  85. data/lib/rigor/cli/explain_command.rb +4 -7
  86. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  87. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  88. data/lib/rigor/cli/lsp_command.rb +11 -19
  89. data/lib/rigor/cli/mcp_command.rb +4 -6
  90. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  91. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  92. data/lib/rigor/cli/options.rb +11 -19
  93. data/lib/rigor/cli/plugin_command.rb +18 -30
  94. data/lib/rigor/cli/plugins_command.rb +29 -51
  95. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  96. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  97. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  98. data/lib/rigor/cli/protection_renderer.rb +5 -7
  99. data/lib/rigor/cli/protection_report.rb +34 -18
  100. data/lib/rigor/cli/renderable.rb +4 -6
  101. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  102. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  103. data/lib/rigor/cli/skill_command.rb +30 -47
  104. data/lib/rigor/cli/skill_describe.rb +40 -64
  105. data/lib/rigor/cli/trace_command.rb +9 -16
  106. data/lib/rigor/cli/trace_renderer.rb +35 -51
  107. data/lib/rigor/cli/triage_command.rb +6 -10
  108. data/lib/rigor/cli/triage_renderer.rb +1 -2
  109. data/lib/rigor/cli/type_of_command.rb +14 -23
  110. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  111. data/lib/rigor/cli/type_scan_command.rb +6 -10
  112. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  113. data/lib/rigor/cli/type_scan_report.rb +2 -3
  114. data/lib/rigor/cli/upgrade_command.rb +2 -3
  115. data/lib/rigor/cli.rb +16 -28
  116. data/lib/rigor/config_audit.rb +35 -44
  117. data/lib/rigor/configuration/dependencies.rb +41 -70
  118. data/lib/rigor/configuration/severity_profile.rb +28 -42
  119. data/lib/rigor/configuration.rb +150 -241
  120. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  121. data/lib/rigor/environment/class_registry.rb +19 -26
  122. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  123. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  124. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  125. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  126. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  127. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  128. data/lib/rigor/environment/rbs_loader.rb +420 -518
  129. data/lib/rigor/environment/reflection.rb +28 -53
  130. data/lib/rigor/environment/reporters.rb +13 -24
  131. data/lib/rigor/environment.rb +225 -304
  132. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  133. data/lib/rigor/flow_contribution/element.rb +7 -12
  134. data/lib/rigor/flow_contribution/fact.rb +33 -57
  135. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  136. data/lib/rigor/flow_contribution/merger.rb +32 -47
  137. data/lib/rigor/flow_contribution.rb +37 -55
  138. data/lib/rigor/inference/acceptance.rb +133 -219
  139. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  140. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  141. data/lib/rigor/inference/budget_trace.rb +43 -63
  142. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  143. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  144. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  145. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  146. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  147. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  148. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  149. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  150. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  151. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  152. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  153. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  154. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  155. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  156. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  157. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  158. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  159. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  160. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  161. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  162. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  163. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  164. data/lib/rigor/inference/def_return_typer.rb +8 -14
  165. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  166. data/lib/rigor/inference/expression_typer.rb +794 -1154
  167. data/lib/rigor/inference/fallback.rb +8 -12
  168. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  169. data/lib/rigor/inference/flow_tracer.rb +25 -36
  170. data/lib/rigor/inference/fork_map.rb +87 -0
  171. data/lib/rigor/inference/hkt_body.rb +45 -68
  172. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  173. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  174. data/lib/rigor/inference/hkt_registry.rb +50 -76
  175. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  176. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  177. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  178. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  179. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  180. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  181. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  182. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  183. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  184. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  185. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  186. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  187. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  188. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  189. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  190. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  191. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  192. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  193. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  194. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  195. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  196. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  197. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  198. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  199. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  200. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  201. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  202. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  203. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  204. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  205. data/lib/rigor/inference/mutation_widening.rb +115 -158
  206. data/lib/rigor/inference/narrowing.rb +627 -727
  207. data/lib/rigor/inference/origin_lookup.rb +38 -0
  208. data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
  209. data/lib/rigor/inference/precision_scanner.rb +26 -35
  210. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  211. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  212. data/lib/rigor/inference/protection_scanner.rb +16 -18
  213. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  214. data/lib/rigor/inference/scope_indexer.rb +473 -821
  215. data/lib/rigor/inference/statement_evaluator.rb +754 -1114
  216. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  217. data/lib/rigor/inference/synthetic_method.rb +9 -16
  218. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  219. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  220. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  221. data/lib/rigor/language_server/buffer_table.rb +11 -18
  222. data/lib/rigor/language_server/completion_provider.rb +69 -116
  223. data/lib/rigor/language_server/debouncer.rb +16 -25
  224. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  225. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  226. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  227. data/lib/rigor/language_server/hover_provider.rb +8 -11
  228. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  229. data/lib/rigor/language_server/loop.rb +14 -19
  230. data/lib/rigor/language_server/project_context.rb +46 -74
  231. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  232. data/lib/rigor/language_server/server.rb +55 -83
  233. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  234. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  235. data/lib/rigor/language_server/uri.rb +8 -13
  236. data/lib/rigor/language_server.rb +4 -6
  237. data/lib/rigor/mcp/loop.rb +2 -3
  238. data/lib/rigor/mcp/server.rb +4 -7
  239. data/lib/rigor/mcp.rb +3 -6
  240. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  241. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  242. data/lib/rigor/plugin/base.rb +335 -518
  243. data/lib/rigor/plugin/blueprint.rb +14 -23
  244. data/lib/rigor/plugin/box.rb +18 -29
  245. data/lib/rigor/plugin/fact_store.rb +16 -26
  246. data/lib/rigor/plugin/inflector.rb +37 -53
  247. data/lib/rigor/plugin/io_boundary.rb +33 -56
  248. data/lib/rigor/plugin/isolation.rb +42 -55
  249. data/lib/rigor/plugin/load_error.rb +10 -15
  250. data/lib/rigor/plugin/loader.rb +30 -49
  251. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  252. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  253. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  254. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  255. data/lib/rigor/plugin/macro.rb +10 -15
  256. data/lib/rigor/plugin/manifest.rb +85 -144
  257. data/lib/rigor/plugin/node_context.rb +14 -22
  258. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  259. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  260. data/lib/rigor/plugin/registry.rb +132 -205
  261. data/lib/rigor/plugin/services.rb +21 -33
  262. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  263. data/lib/rigor/plugin/trust_policy.rb +24 -38
  264. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  265. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  266. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  267. data/lib/rigor/protection/mutator.rb +50 -70
  268. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  269. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  270. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  271. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  272. data/lib/rigor/rbs_extended.rb +107 -197
  273. data/lib/rigor/reflection.rb +68 -86
  274. data/lib/rigor/scope/discovery_index.rb +14 -19
  275. data/lib/rigor/scope.rb +255 -310
  276. data/lib/rigor/sig_gen/classification.rb +6 -10
  277. data/lib/rigor/sig_gen/generator.rb +197 -323
  278. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  279. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  280. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  281. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  282. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  283. data/lib/rigor/sig_gen/renderer.rb +7 -13
  284. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  285. data/lib/rigor/sig_gen/write_result.rb +8 -16
  286. data/lib/rigor/sig_gen/writer.rb +95 -174
  287. data/lib/rigor/sig_gen.rb +3 -6
  288. data/lib/rigor/signature_path_audit.rb +24 -30
  289. data/lib/rigor/source/constant_path.rb +10 -14
  290. data/lib/rigor/source/literals.rb +31 -45
  291. data/lib/rigor/source/node_locator.rb +9 -11
  292. data/lib/rigor/source/node_walker.rb +9 -13
  293. data/lib/rigor/source.rb +3 -4
  294. data/lib/rigor/testing.rb +16 -20
  295. data/lib/rigor/triage/catalogue.rb +38 -62
  296. data/lib/rigor/triage.rb +31 -52
  297. data/lib/rigor/trinary.rb +9 -13
  298. data/lib/rigor/type/acceptance_router.rb +4 -6
  299. data/lib/rigor/type/accepts_result.rb +10 -14
  300. data/lib/rigor/type/app.rb +19 -27
  301. data/lib/rigor/type/bot.rb +4 -6
  302. data/lib/rigor/type/bound_method.rb +10 -15
  303. data/lib/rigor/type/combinator.rb +165 -257
  304. data/lib/rigor/type/constant.rb +23 -34
  305. data/lib/rigor/type/data_class.rb +10 -15
  306. data/lib/rigor/type/data_instance.rb +14 -20
  307. data/lib/rigor/type/difference.rb +21 -32
  308. data/lib/rigor/type/dynamic.rb +3 -5
  309. data/lib/rigor/type/hash_shape.rb +32 -18
  310. data/lib/rigor/type/integer_range.rb +11 -16
  311. data/lib/rigor/type/intersection.rb +27 -42
  312. data/lib/rigor/type/nominal.rb +10 -15
  313. data/lib/rigor/type/plain_lattice.rb +9 -13
  314. data/lib/rigor/type/refined.rb +67 -114
  315. data/lib/rigor/type/singleton.rb +4 -6
  316. data/lib/rigor/type/struct_class.rb +11 -16
  317. data/lib/rigor/type/struct_instance.rb +15 -21
  318. data/lib/rigor/type/tuple.rb +14 -19
  319. data/lib/rigor/type/union.rb +30 -42
  320. data/lib/rigor/type_node/generic.rb +14 -26
  321. data/lib/rigor/type_node/identifier.rb +12 -19
  322. data/lib/rigor/type_node.rb +3 -12
  323. data/lib/rigor/value_semantics.rb +16 -21
  324. data/lib/rigor/version.rb +1 -1
  325. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
  337. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  340. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
  348. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  350. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  351. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  353. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  354. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  355. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  356. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  357. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  358. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  359. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  361. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  363. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  365. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  366. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  367. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  368. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  369. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  372. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  373. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  376. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  377. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  378. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  379. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  380. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  381. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  385. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  386. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  389. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  390. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
  398. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
  399. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  400. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  406. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  408. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  409. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  411. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  416. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  417. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  426. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  427. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  428. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  429. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  430. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  431. data/sig/rigor/environment.rbs +1 -0
  432. data/sig/rigor/scope.rbs +6 -0
  433. metadata +9 -1
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-82 — the effective {DynamicOrigin} cause of a receiver *expression*, unifying the two provenance
8
+ # lookups so a chain and a bare variable resolve the same way:
9
+ #
10
+ # - the cause recorded on the expression's own node (`Scope#dynamic_origins`), covering a call node whose
11
+ # dispatch recorded a cause (WD2's user-method tiers, or WD6's chain inheritance), and
12
+ # - for a bare local / instance-variable read, the cause propagated from its binding (WD1's
13
+ # `Scope#local_origins` / `#ivar_origins`) — the read node itself carries none, the cause was recorded
14
+ # on the assignment's rhs.
15
+ #
16
+ # Consulted by `ProtectionScanner` (to label a hole) and `ExpressionTyper` (WD6 — to inherit a Dynamic
17
+ # receiver's cause onto the call it produces, so provenance survives a method chain).
18
+ module OriginLookup
19
+ module_function
20
+
21
+ # @param scope [Rigor::Scope]
22
+ # @param node [Prism::Node, nil] the receiver expression's node (nil for an implicit-self receiver)
23
+ # @return [Symbol, nil] the effective dynamic-origin cause, or nil when none is known
24
+ def origin_for(scope, node)
25
+ return nil if node.nil?
26
+
27
+ scope.dynamic_origins[node] || propagated(scope, node)
28
+ end
29
+
30
+ def propagated(scope, node)
31
+ case node
32
+ when Prism::LocalVariableReadNode then scope.local_origin(node.name)
33
+ when Prism::InstanceVariableReadNode then scope.ivar_origin(node.name)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,14 +3,15 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "scope_indexer"
6
+ require_relative "fork_map"
6
7
  require_relative "../source/node_walker"
7
8
 
8
9
  module Rigor
9
10
  module Inference
10
- # Pure argument-type classification for {ParameterInferenceCollector} — no
11
- # collector state, so it lives outside the orchestration class. Decides which
12
- # argument types may seed a parameter (concrete enough for the protection
13
- # metric to bite) and widens a literal argument to its nominal.
11
+ # Pure argument-type classification for {ParameterInferenceCollector} — no collector state,
12
+ # so it lives outside the orchestration class. Decides which argument types may seed a
13
+ # parameter (concrete enough for the protection metric to bite) and widens a literal
14
+ # argument to its nominal.
14
15
  module ParameterArgTypes
15
16
  module_function
16
17
 
@@ -20,10 +21,9 @@ module Rigor
20
21
  FalseClass => "FalseClass", NilClass => "NilClass"
21
22
  }.freeze
22
23
 
23
- # A parameter holds a *value of* a type across its lifetime, not a pinned
24
- # literal — so a `Constant<"text">` argument widens to its nominal
25
- # (`String`); recurses through unions so `Constant<"a"> | Constant<"b">`
26
- # collapses to `String`.
24
+ # A parameter holds a *value of* a type across its lifetime, not a pinned literal — so a
25
+ # `Constant<"text">` argument widens to its nominal (`String`); recurses through unions so
26
+ # `Constant<"a"> | Constant<"b">` collapses to `String`.
27
27
  def widen_for_param(type)
28
28
  case type
29
29
  when Type::Constant
@@ -41,10 +41,10 @@ module Rigor
41
41
  nil
42
42
  end
43
43
 
44
- # The dispatch class for a receiver type, for the subset the collector
45
- # resolves to a user `def` (mirrors `CheckRules#concrete_class_name` for the
46
- # carriers a user-method receiver is typed as). `class_name_of` is the
47
- # Nominal/Singleton-only variant for an implicit-self `self_type`.
44
+ # The dispatch class for a receiver type, for the subset the collector resolves to a user
45
+ # `def` (mirrors `CheckRules#concrete_class_name` for the carriers a user-method receiver
46
+ # is typed as). `class_name_of` is the Nominal/Singleton-only variant for an implicit-self
47
+ # `self_type`.
48
48
  def concrete_class_name(type)
49
49
  case type
50
50
  when Type::Nominal, Type::Singleton then type.class_name
@@ -57,9 +57,8 @@ module Rigor
57
57
  type.class_name if type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton)
58
58
  end
59
59
 
60
- # Whether `type` is too gradual to seed (not a concrete dispatch target) —
61
- # the negation of `ProtectionScanner#concrete_receiver?` (a union is
62
- # concrete only when every arm is).
60
+ # Whether `type` is too gradual to seed (not a concrete dispatch target) — the negation of
61
+ # `ProtectionScanner#concrete_receiver?` (a union is concrete only when every arm is).
63
62
  def non_concrete?(type)
64
63
  case type
65
64
  when Type::Dynamic, Type::Top, Type::Bot then true
@@ -71,55 +70,50 @@ module Rigor
71
70
 
72
71
  # ADR-67 WD3 + WD5 — call-site parameter type inference (capped fixpoint).
73
72
  #
74
- # A `def` parameter with no RBS signature types `untyped` (the gradual entry
75
- # point), so a param flowing into an ivar or a receiver drains everything
76
- # downstream to `Dynamic`. This pass closes that hole: it walks every project
77
- # file, types each call's positional arguments in their lexical scope,
78
- # resolves which user-defined `def` each call targets, and records the
79
- # **union of resolved call-site argument types** per parameter TypeProf's
80
- # "a parameter's type is the union of every actual argument across all call
81
- # sites". WD5: it iterates (cap {DEFAULT_ROUNDS}), re-seeding each round with
82
- # the previous round's inferred parameters, so a parameter passed *another*
83
- # parameter is typed one hop further per round (round 1 alone is the
84
- # single-level pass).
73
+ # A `def` parameter with no RBS signature types `untyped` (the gradual entry point), so a
74
+ # param flowing into an ivar or a receiver drains everything downstream to `Dynamic`. This
75
+ # pass closes that hole: it walks every project file, types each call's positional
76
+ # arguments in their lexical scope, resolves which user-defined `def` each call targets,
77
+ # and records the **union of resolved call-site argument types** per parameter — TypeProf's
78
+ # "a parameter's type is the union of every actual argument across all call sites". WD5: it
79
+ # iterates (cap {DEFAULT_ROUNDS}), re-seeding each round with the previous round's inferred
80
+ # parameters, so a parameter passed *another* parameter is typed one hop further per round
81
+ # (round 1 alone is the single-level pass).
85
82
  #
86
83
  # The result is keyed by `[class_name, method_name, kind]` — the same triple
87
- # `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical
88
- # class path — so the consumer can seed an undeclared parameter with its
89
- # inferred type. The table is precision-additive only: it never feeds a
90
- # parameter-boundary diagnostic (an inferred type lives solely as a body
91
- # local, and the boundary rules consult RBS, not body locals see ADR-67
92
- # WD1), so being wrong cannot manufacture a false positive at a caller.
84
+ # `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical class path —
85
+ # so the consumer can seed an undeclared parameter with its inferred type. The table is
86
+ # precision-additive only: it never feeds a parameter-boundary diagnostic (an inferred type
87
+ # lives solely as a body local, and the boundary rules consult RBS, not body locals — see
88
+ # ADR-67 WD1), so being wrong cannot manufacture a false positive at a caller.
93
89
  #
94
- # Soundness (WD4): the inferred type is a sound over-approximation only when
95
- # every contributed call site resolves to a concrete argument type. Any
96
- # `Dynamic` / `Top` / `Bot` argument (a `send`/dynamic-dispatch caller, or a
97
- # parameter not yet typed in an earlier round) **poisons** the parameter,
98
- # which then contributes nothing this round (it may type in a later round
99
- # once its own argument resolves). Unresolved call sites do not contribute.
90
+ # Soundness (WD4): the inferred type is a sound over-approximation only when every
91
+ # contributed call site resolves to a concrete argument type. Any `Dynamic` / `Top` /
92
+ # `Bot` argument (a `send`/dynamic-dispatch caller, or a parameter not yet typed in an
93
+ # earlier round) **poisons** the parameter, which then contributes nothing this round (it
94
+ # may type in a later round once its own argument resolves). Unresolved call sites do not
95
+ # contribute.
100
96
  #
101
- # What it does NOT do yet (deferred — the check-wiring slice): feeding the
102
- # table into the `check` walk (only `coverage --protection` consumes it
103
- # today), keyword / optional / rest / block parameters, inherited-method
104
- # receivers, top-level helpers, and a rigorous closed-call-site-set proof
105
- # (the union is optimistic over resolved sites acceptable because the only
106
- # consumer is the protection metric, which runs no diagnostics).
97
+ # What it does NOT do yet (deferred — the check-wiring slice): feeding the table into the
98
+ # `check` walk (only `coverage --protection` consumes it today), keyword / optional / rest
99
+ # / block parameters, inherited-method receivers, top-level helpers, and a rigorous
100
+ # closed-call-site-set proof (the union is optimistic over resolved sites — acceptable
101
+ # because the only consumer is the protection metric, which runs no diagnostics).
107
102
  class ParameterInferenceCollector
108
103
  EMPTY = {}.freeze
109
104
 
110
- # A defensive widening cap (ADR-41): a parameter unioned from more than
111
- # this many distinct concrete call-site types is widened to `untyped`
112
- # (poisoned) rather than carrying an unbounded union.
105
+ # A defensive widening cap (ADR-41): a parameter unioned from more than this many distinct
106
+ # concrete call-site types is widened to `untyped` (poisoned) rather than carrying an
107
+ # unbounded union.
113
108
  MAX_CALL_SITE_TYPES = 16
114
109
 
115
- # WD5 — the call-site union is a worklist fixpoint: each round re-types the
116
- # project with the previous round's inferred parameters seeded, so a
117
- # parameter passed *another* parameter is typed one hop further per round.
118
- # Capped (no true-convergence requirement the metric tolerates a bounded
119
- # approximation, and the table can oscillate at the margin since a newly
120
- # resolved receiver can surface a fresh untyped-argument call site). The
121
- # cap matches the `Inference::BodyFixpoint` cap-3 convention. Round 1 alone
122
- # is the single-level pass.
110
+ # WD5 — the call-site union is a worklist fixpoint: each round re-types the project with
111
+ # the previous round's inferred parameters seeded, so a parameter passed *another*
112
+ # parameter is typed one hop further per round. Capped (no true-convergence requirement —
113
+ # the metric tolerates a bounded approximation, and the table can oscillate at the margin
114
+ # since a newly resolved receiver can surface a fresh untyped-argument call site). The cap
115
+ # matches the `Inference::BodyFixpoint` cap-3 convention. Round 1 alone is the
116
+ # single-level pass.
123
117
  DEFAULT_ROUNDS = 3
124
118
 
125
119
  # @param files [Array<String>] project `.rb` paths to scan for call sites.
@@ -127,25 +121,34 @@ module Rigor
127
121
  # @param target_ruby [String, nil] Prism parse target.
128
122
  # @param max_rounds [Integer] the WD5 fixpoint cap (1 = single-level).
129
123
  # @return [Hash{[String,Symbol,Symbol] => Hash{Symbol => Rigor::Type}}] frozen.
130
- def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
131
- new(files: files, environment: environment, target_ruby: target_ruby, max_rounds: max_rounds).collect
124
+ def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
125
+ new(files: files, environment: environment, target_ruby: target_ruby,
126
+ max_rounds: max_rounds, workers: workers).collect
132
127
  end
133
128
 
134
- def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
129
+ def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
135
130
  @files = files
136
131
  @environment = environment
137
132
  @target_ruby = target_ruby
138
133
  @max_rounds = max_rounds
139
- # Reset per round (see {#run_round}). `[[class, method, kind], param_sym]`
140
- # => [Type] of observed concrete arguments (a default-block Hash, not a
141
- # `{}` literal, so the analyzer types its reads generically {#finalize}),
142
- # plus the ids widened to `untyped` (an untyped / over-cap argument).
134
+ # P3-10 — fork-parallelism for the per-round re-typing (the dominant coverage-protection cost). A
135
+ # round's per-file typing is independent; contributions merge associatively (see {#merge_round}), so
136
+ # forking over file slices is byte-identical to the sequential pass. 0/1 → sequential.
137
+ @workers = workers
138
+ # Reset per round (see {#run_round}). `[[class, method, kind], param_sym]` => [Type] of
139
+ # observed concrete arguments (a default-block Hash, not a `{}` literal, so the
140
+ # analyzer types its reads generically — {#finalize}), plus the ids widened to
141
+ # `untyped` (an untyped / over-cap argument).
143
142
  @type_observations = Hash.new { |hash, id| hash[id] = [] }
144
143
  @poisoned_params = Set.new
145
144
  end
146
145
 
147
146
  def collect
148
147
  parsed = parse_all
148
+ # Force the full RBS load on the parent so forked round-workers copy-on-write inherit a warm
149
+ # environment instead of each rebuilding it (mirrors the check / scan fork pools). A no-op on the
150
+ # sequential path.
151
+ @environment.rbs_loader&.prewarm if ForkMap.parallel?([@workers, parsed.size].min)
149
152
  discovery = discovery_seed_tables
150
153
  table = EMPTY
151
154
  @max_rounds.times do
@@ -159,8 +162,8 @@ module Rigor
159
162
 
160
163
  private
161
164
 
162
- # Parse every project file once; rounds re-index the cached ASTs against an
163
- # evolving seed rather than re-parsing.
165
+ # Parse every project file once; rounds re-index the cached ASTs against an evolving seed
166
+ # rather than re-parsing.
164
167
  def parse_all
165
168
  @files.filter_map do |path|
166
169
  result = Prism.parse(File.read(path), filepath: path, version: @target_ruby)
@@ -170,25 +173,58 @@ module Rigor
170
173
  end
171
174
  end
172
175
 
173
- # One fixpoint round: re-type every file with `seed_table` (the previous
174
- # round's inferred parameters) seeded, collecting the next round's table.
176
+ # One fixpoint round: re-type every file with `seed_table` (the previous round's inferred
177
+ # parameters) seeded, collecting the next round's table. The per-file typing is fork-mapped over
178
+ # `@workers` (byte-identical to sequential — see {#merge_round}); each slice returns a marshalable
179
+ # `[observations, poisoned]` contribution the parent merges in slice order.
175
180
  def run_round(parsed, discovery_tables, seed_table)
181
+ seed_scope = build_seed_scope(discovery_tables, seed_table)
182
+ contributions = ForkMap.call(items: parsed, workers: @workers) do |slice|
183
+ accumulate_slice(slice, seed_scope)
184
+ end
185
+ merge_round(contributions)
186
+ end
187
+
188
+ # Types every call in one contiguous file slice, returning a marshalable contribution: the per-`id`
189
+ # observed argument types (default proc stripped for `Marshal`) and the poisoned-`id` list. Uses the
190
+ # per-process observation ivars, so a forked worker and the sequential single-slice call share this
191
+ # exact code.
192
+ def accumulate_slice(parsed_slice, seed_scope)
176
193
  @type_observations = Hash.new { |hash, id| hash[id] = [] }
177
194
  @poisoned_params = Set.new
178
- seed_scope = build_seed_scope(discovery_tables, seed_table)
179
- parsed.each do |path, ast|
195
+ parsed_slice.each do |path, ast|
180
196
  index = ScopeIndexer.index(ast, default_scope: seed_scope.with_source_path(path))
181
197
  Source::NodeWalker.each(ast) do |node|
182
198
  record_call(node, index) if node.is_a?(Prism::CallNode)
183
199
  end
184
200
  end
185
- finalize
201
+ # Copy into a plain Hash, dropping the default proc — a Marshal-unfriendly proc the fork worker
202
+ # would otherwise fail to dump. (`.to_h` returns self here, keeping the proc, so copy explicitly.)
203
+ observations = @type_observations.each_with_object({}) { |(id, types), plain| plain[id] = types }
204
+ [observations, @poisoned_params.to_a]
186
205
  end
187
206
 
188
- # A scope carrying the cross-file discovery index (so `Foo.new` receivers
189
- # and implicit-self calls resolve to a user `def`) plus the prior round's
190
- # inferred parameters (so an argument that reads a parameter types to its
191
- # current inferred type the WD5 propagation).
207
+ # Merges the per-slice contributions into the round's table. Associative and order-preserving, so the
208
+ # result is identical to a sequential single-slice run: a parameter is poisoned if ANY slice poisoned
209
+ # it, its observations are the file-order concatenation across slices, and the {MAX_CALL_SITE_TYPES}
210
+ # cap re-applies over the merged total (a per-slice sub-cap can undercount, so the parent enforces the
211
+ # real cap — the same "poisoned" outcome the sequential mid-stream cap reaches).
212
+ def merge_round(contributions)
213
+ poisoned = contributions.each_with_object(Set.new) { |(_obs, pois), set| set.merge(pois) }
214
+ observations = {}
215
+ contributions.each do |(obs, _pois)|
216
+ obs.each do |id, types|
217
+ (observations[id] ||= []).concat(types) unless poisoned.include?(id)
218
+ end
219
+ end
220
+ observations.each { |id, types| poisoned << id if types.length > MAX_CALL_SITE_TYPES }
221
+ finalize(observations, poisoned)
222
+ end
223
+
224
+ # A scope carrying the cross-file discovery index (so `Foo.new` receivers and
225
+ # implicit-self calls resolve to a user `def`) plus the prior round's inferred parameters
226
+ # (so an argument that reads a parameter types to its current inferred type — the WD5
227
+ # propagation).
192
228
  def build_seed_scope(discovery_tables, seed_table)
193
229
  base = Scope.empty(environment: @environment)
194
230
  tables = discovery_tables
@@ -209,9 +245,8 @@ module Rigor
209
245
  end
210
246
  tables
211
247
  rescue StandardError
212
- # Discovery is best-effort; a malformed corner of the project must not
213
- # crash the protection scan. Without discovery the collector simply
214
- # resolves fewer call sites.
248
+ # Discovery is best-effort; a malformed corner of the project must not crash the
249
+ # protection scan. Without discovery the collector simply resolves fewer call sites.
215
250
  {}
216
251
  end
217
252
 
@@ -240,35 +275,41 @@ module Rigor
240
275
  return if callee.nil?
241
276
 
242
277
  class_name, method, kind, def_node = callee
243
- requireds = simple_requireds(def_node)
244
- return if requireds.nil? || requireds.size != args.size
278
+ requireds = leading_requireds(def_node)
279
+ # Enough positional arguments to fill every leading required (extra args map to trailing
280
+ # optional / rest parameters, which this pass does not infer). Fewer would be an arity error
281
+ # or a mis-resolved callee; skip rather than mis-attribute.
282
+ return if requireds.nil? || args.size < requireds.size
245
283
 
246
284
  key = [class_name, method, kind]
247
- args.each_with_index do |arg, i|
248
- arg_scope = index[arg]
249
- accumulate(key, requireds[i].name, arg_scope&.type_of(arg))
285
+ requireds.each_with_index do |param, i|
286
+ arg_scope = index[args[i]]
287
+ accumulate(key, param.name, arg_scope&.type_of(args[i]))
250
288
  end
251
289
  end
252
290
 
253
- # The plain positional arguments, or nil when the call carries any
254
- # non-plain argument (splat / keyword / block-pass / forwarding) those
255
- # break the positional-index parameter mapping, so the call site is
256
- # skipped rather than mis-attributed.
291
+ # The plain leading positional arguments. A trailing keyword hash (`f(x, k: 1)`) or block-pass
292
+ # (`f(x, &blk)`) is dropped it occupies no positional slot, so the leading args still map to
293
+ # the leading required parameters. Returns nil when the call carries a splat (`f(*xs, x)`) or
294
+ # forwarding (`f(...)`) argument: those make an argument's position depend on runtime length,
295
+ # breaking the positional-index ↔ parameter mapping, so the call site is skipped.
257
296
  def positional_args(call_node)
258
297
  arguments = call_node.arguments
259
298
  return [] if arguments.nil?
260
299
 
261
300
  list = arguments.arguments
262
- return nil if list.any? { |arg| non_plain_argument?(arg) }
301
+ return nil if list.any? { |arg| position_breaking_argument?(arg) }
302
+
303
+ list.reject { |arg| non_positional_argument?(arg) }
304
+ end
263
305
 
264
- list
306
+ def position_breaking_argument?(arg)
307
+ arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode)
265
308
  end
266
309
 
267
- def non_plain_argument?(arg)
268
- arg.is_a?(Prism::SplatNode) ||
269
- arg.is_a?(Prism::KeywordHashNode) ||
310
+ def non_positional_argument?(arg)
311
+ arg.is_a?(Prism::KeywordHashNode) ||
270
312
  arg.is_a?(Prism::BlockArgumentNode) ||
271
- arg.is_a?(Prism::ForwardingArgumentsNode) ||
272
313
  arg.is_a?(Prism::AssocNode) ||
273
314
  arg.is_a?(Prism::AssocSplatNode)
274
315
  end
@@ -307,16 +348,19 @@ module Rigor
307
348
  per_class && per_class[method]
308
349
  end
309
350
 
310
- # The required-positional parameters, or nil when the method's parameter
311
- # list is not a simple all-required shape (matching the single-level
312
- # contract `ExpressionTyper#user_method_param_shape_simple?` uses) or
313
- # contains a destructured `(a, b)` slot (no bindable name).
314
- def simple_requireds(def_node)
351
+ # The leading required-positional parameters the ones a call's leading positional arguments
352
+ # map to. Ruby orders parameters requireds-first, so optional / rest / keyword / block
353
+ # parameters that FOLLOW the requireds do not disturb the leading positional-index ↔
354
+ # required-parameter mapping; this covers the common `def f(x, opts = {}, **kw)` /
355
+ # `def f(x, y, &block)` shapes the earlier all-required contract skipped entirely. Returns nil
356
+ # only when a required slot is a destructured `(a, b)` (no bindable name) or a post-rest
357
+ # required (`def f(a, *b, c)` — `c` maps to a trailing arg, breaking the leading-prefix
358
+ # assumption for the whole call), which would mis-attribute arguments.
359
+ def leading_requireds(def_node)
315
360
  params = def_node.parameters
316
361
  return [] if params.nil?
317
362
  return nil unless params.is_a?(Prism::ParametersNode)
318
- return nil unless params.optionals.empty? && params.rest.nil? && params.posts.empty? &&
319
- params.keywords.empty? && params.keyword_rest.nil? && params.block.nil?
363
+ return nil unless params.posts.empty?
320
364
  return nil unless params.requireds.all?(Prism::RequiredParameterNode)
321
365
 
322
366
  params.requireds
@@ -335,26 +379,27 @@ module Rigor
335
379
  end
336
380
  end
337
381
 
338
- # A poisoned parameter is dropped from the observation store and recorded
339
- # so later call sites short-circuit. `id` is the `[[class, method, kind],
340
- # param]` pair.
382
+ # A poisoned parameter is dropped from the observation store and recorded so later call
383
+ # sites short-circuit. `id` is the `[[class, method, kind], param]` pair.
341
384
  def poison(id)
342
385
  @poisoned_params << id
343
386
  @type_observations.delete(id)
344
387
  end
345
388
 
346
- def finalize
347
- # `result` is a default-block Hash (not a `{}` literal) so the analyzer
348
- # types its reads generically rather than folding the empty shape — the
349
- # nesting writes stay plain assignments, no literal-fold conditions.
389
+ # Builds the round's frozen `[class, method, kind] => {param => Type}` table from the merged
390
+ # observations and poisoned set.
391
+ def finalize(merged_observations, poisoned)
392
+ # `result` is a default-block Hash (not a `{}` literal) so the analyzer types its reads
393
+ # generically rather than folding the empty shape — the nesting writes stay plain
394
+ # assignments, no literal-fold conditions.
350
395
  result = Hash.new { |hash, key| hash[key] = {} }
351
- @type_observations.each do |id, observations|
352
- next if @poisoned_params.include?(id)
396
+ merged_observations.each do |id, observations|
397
+ next if poisoned.include?(id)
353
398
  next if observations.empty?
354
399
 
355
400
  union = Type::Combinator.union(*observations)
356
- # A union that collapsed to a non-concrete shape (e.g. a gradual arm
357
- # leaked in) is no better than `untyped`; drop it.
401
+ # A union that collapsed to a non-concrete shape (e.g. a gradual arm leaked in) is no
402
+ # better than `untyped`; drop it.
358
403
  next if ParameterArgTypes.non_concrete?(union)
359
404
 
360
405
  key, param = id
@@ -6,15 +6,13 @@ require_relative "scope_indexer"
6
6
 
7
7
  module Rigor
8
8
  module Inference
9
- # Measures the *type quality* of inferred expressions — not whether the
10
- # engine recognises an AST node class (that is `CoverageScanner`'s job),
11
- # but whether the type it produces carries useful static information.
9
+ # Measures the *type quality* of inferred expressions — not whether the engine recognises an AST node
10
+ # class (that is `CoverageScanner`'s job), but whether the type it produces carries useful static
11
+ # information.
12
12
  #
13
- # Each visited *expression* node is classified into one of eight
14
- # precision tiers (non-expression syntax nodes argument /
15
- # parameter lists, parameter declarations, hash pairs, statement
16
- # wrappers, clause headers — are skipped; see
17
- # {NON_EXPRESSION_NODE_TYPES}):
13
+ # Each visited *expression* node is classified into one of eight precision tiers (non-expression syntax
14
+ # nodes argument / parameter lists, parameter declarations, hash pairs, statement wrappers, clause
15
+ # headers are skipped; see {NON_EXPRESSION_NODE_TYPES}):
18
16
  #
19
17
  # :constant — Constant[T]: literal value known exactly
20
18
  # :nominal — Nominal/Singleton: class identity known
@@ -25,42 +23,35 @@ module Rigor
25
23
  # :dynamic_top — Dynamic[Top]: completely opaque (the "untyped" hole)
26
24
  # :top — Top: universal supertype (no information)
27
25
  #
28
- # The summary exposes `precision_ratio` (constant+nominal+shaped+refined+bot
29
- # over total) and `opaque_ratio` (dynamic_top+top over total).
26
+ # The summary exposes `precision_ratio` (constant+nominal+shaped+refined+bot over total) and
27
+ # `opaque_ratio` (dynamic_top+top over total).
30
28
  #
31
- # For Union types the *worst* member tier is used, since the union is only
32
- # as precise as its least-precise constituent. Intersection uses the *best*
33
- # member (the most specific side wins). Difference follows its base type.
29
+ # For Union types the *worst* member tier is used, since the union is only as precise as its
30
+ # least-precise constituent. Intersection uses the *best* member (the most specific side wins).
31
+ # Difference follows its base type.
34
32
  class PrecisionScanner
35
33
  TIERS = %i[
36
34
  constant nominal shaped refined bot
37
35
  dynamic_specific dynamic_top top
38
36
  ].freeze
39
37
 
40
- # Prism node classes that do not denote a value-producing
41
- # expression, so typing them is meaningless they have no
42
- # runtime value to carry a type. Counting them (they always fall
43
- # to the `dynamic_top` fallback) silently diluted the precision
44
- # ratio: on a real survey target (shugo/textbringer) they were
45
- # ~49% of every "opaque" node, dragging the headline number ~13
46
- # points below the true expression-level precision. We exclude
47
- # them from BOTH numerator and denominator so the ratio measures
48
- # what it claims to — the type quality of actual expressions.
38
+ # Prism node classes that do not denote a value-producing expression, so typing them is meaningless —
39
+ # they have no runtime value to carry a type. Counting them (they always fall to the `dynamic_top`
40
+ # fallback) silently diluted the precision ratio: on a real survey target (shugo/textbringer) they
41
+ # were ~49% of every "opaque" node, dragging the headline number ~13 points below the true
42
+ # expression-level precision. We exclude them from BOTH numerator and denominator so the ratio
43
+ # measures what it claims to the type quality of actual expressions.
49
44
  #
50
- # The set is deliberately CONSERVATIVE: only nodes that are
51
- # unambiguously non-expressions in Ruby's grammar are listed
52
- # argument / parameter list containers and the parameter
53
- # declarations inside them; the `key => value` pair node (its key
54
- # and value are themselves walked and counted); the program /
55
- # statements sequence wrappers (their value is the last child,
56
- # already counted listing them avoids double-counting); and the
57
- # clause-header nodes whose body, not the header, carries the
58
- # value. Anything that *could* be a value expression (`BlockNode`,
59
- # `BeginNode`, `ImplicitNode`, `ParenthesesNode`, splats, …) is
60
- # left in so a genuine inference gap stays visible.
45
+ # The set is deliberately CONSERVATIVE: only nodes that are unambiguously non-expressions in Ruby's
46
+ # grammar are listed argument / parameter list containers and the parameter declarations inside
47
+ # them; the `key => value` pair node (its key and value are themselves walked and counted); the
48
+ # program / statements sequence wrappers (their value is the last child, already counted — listing
49
+ # them avoids double-counting); and the clause-header nodes whose body, not the header, carries the
50
+ # value. Anything that *could* be a value expression (`BlockNode`, `BeginNode`, `ImplicitNode`,
51
+ # `ParenthesesNode`, splats, …) is left in so a genuine inference gap stays visible.
61
52
  #
62
- # Compared by class NAME so a Prism version that lacks one of the
63
- # newer node classes does not break loading.
53
+ # Compared by class NAME so a Prism version that lacks one of the newer node classes does not break
54
+ # loading.
64
55
  NON_EXPRESSION_NODE_TYPES = %w[
65
56
  Prism::ProgramNode
66
57
  Prism::StatementsNode
@@ -2,33 +2,24 @@
2
2
 
3
3
  module Rigor
4
4
  module Inference
5
- # ADR-17 § "Inference contract" — project-wide patched-method
6
- # registry populated by the pre-eval pre-pass (slice 2) from
7
- # the user's `.rigor.yml` `pre_eval:` list.
5
+ # ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
6
+ # pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
8
7
  #
9
- # Each entry records one `def` declaration the pre-pass
10
- # observed inside a class / module body. The dispatcher's
11
- # `try_project_patched_method` tier consults this registry
12
- # between the plugin tier and the dependency-source tier so
13
- # project-side `lib/core_ext/string_extensions.rb` patches
14
- # are visible to cross-file dispatch.
8
+ # Each entry records one `def` declaration the pre-pass observed inside a class / module body. The
9
+ # dispatcher's `try_project_patched_method` tier consults this registry between the plugin tier and
10
+ # the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
11
+ # to cross-file dispatch.
15
12
  #
16
- # The dispatcher answers `Dynamic[T]` (with a heuristic static
17
- # facet) when `Entry#return_type` is non-nil, or `Dynamic[Top]`
18
- # when the heuristic declined (`nil`). See {Entry} for the
19
- # per-field contract.
13
+ # The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
14
+ # non-nil, or `Dynamic[Top]` when the heuristic declined (`nil`). See {Entry} for the per-field
15
+ # contract.
20
16
  class ProjectPatchedMethods
21
- # Frozen value-object recording one `def` observed by the
22
- # pre-pass. `class_name` is the qualified prefix
23
- # (`"String"`, `"Foo::Bar"`); `method_name` is the
24
- # declared name; `kind` is `:instance` or `:singleton`;
25
- # `source_path` / `source_line` carry attribution for
26
- # diagnostics; `return_type` is the
27
- # {Analysis::DependencySourceInference::ReturnTypeHeuristic}-
28
- # extracted static facet (a `Rigor::Type::*`) or `nil`
29
- # when the heuristic declined. The dispatcher wraps a
30
- # non-nil `return_type` in `Dynamic[T]`; a `nil`
31
- # `return_type` falls back to `Dynamic[top]`.
17
+ # Frozen value-object recording one `def` observed by the pre-pass. `class_name` is the qualified
18
+ # prefix (`"String"`, `"Foo::Bar"`); `method_name` is the declared name; `kind` is `:instance` or
19
+ # `:singleton`; `source_path` / `source_line` carry attribution for diagnostics; `return_type` is
20
+ # the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
21
+ # `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
22
+ # `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
32
23
  Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type) do
33
24
  def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil)
34
25
  super
@@ -37,11 +28,9 @@ module Rigor
37
28
 
38
29
  attr_reader :by_key
39
30
 
40
- # @param entries [Array<Entry>] flat list of declarations
41
- # observed during the pre-pass. First-write-wins on
42
- # `(class_name, method_name, kind)` duplicates so the
43
- # `pre-eval.duplicate-declaration` diagnostic emission
44
- # stays decoupled from registry behaviour.
31
+ # @param entries [Array<Entry>] flat list of declarations observed during the pre-pass.
32
+ # First-write-wins on `(class_name, method_name, kind)` duplicates so the
33
+ # `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
45
34
  def initialize(entries: [])
46
35
  @by_key = entries.each_with_object({}) do |entry, acc|
47
36
  key = [entry.class_name, entry.method_name, entry.kind]
@@ -50,9 +39,8 @@ module Rigor
50
39
  freeze
51
40
  end
52
41
 
53
- # @return [Entry, nil] the recorded entry for the given
54
- # `(class_name, method_name, kind)` triple, or `nil`
55
- # when no pre-eval file declared it.
42
+ # @return [Entry, nil] the recorded entry for the given `(class_name, method_name, kind)` triple,
43
+ # or `nil` when no pre-eval file declared it.
56
44
  def lookup(class_name:, method_name:, kind:)
57
45
  @by_key[[class_name, method_name, kind]]
58
46
  end