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
@@ -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
@@ -7,10 +7,10 @@ require_relative "../source/node_walker"
7
7
 
8
8
  module Rigor
9
9
  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.
10
+ # Pure argument-type classification for {ParameterInferenceCollector} — no collector state,
11
+ # so it lives outside the orchestration class. Decides which argument types may seed a
12
+ # parameter (concrete enough for the protection metric to bite) and widens a literal
13
+ # argument to its nominal.
14
14
  module ParameterArgTypes
15
15
  module_function
16
16
 
@@ -20,10 +20,9 @@ module Rigor
20
20
  FalseClass => "FalseClass", NilClass => "NilClass"
21
21
  }.freeze
22
22
 
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`.
23
+ # A parameter holds a *value of* a type across its lifetime, not a pinned literal — so a
24
+ # `Constant<"text">` argument widens to its nominal (`String`); recurses through unions so
25
+ # `Constant<"a"> | Constant<"b">` collapses to `String`.
27
26
  def widen_for_param(type)
28
27
  case type
29
28
  when Type::Constant
@@ -41,10 +40,10 @@ module Rigor
41
40
  nil
42
41
  end
43
42
 
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`.
43
+ # The dispatch class for a receiver type, for the subset the collector resolves to a user
44
+ # `def` (mirrors `CheckRules#concrete_class_name` for the carriers a user-method receiver
45
+ # is typed as). `class_name_of` is the Nominal/Singleton-only variant for an implicit-self
46
+ # `self_type`.
48
47
  def concrete_class_name(type)
49
48
  case type
50
49
  when Type::Nominal, Type::Singleton then type.class_name
@@ -57,9 +56,8 @@ module Rigor
57
56
  type.class_name if type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton)
58
57
  end
59
58
 
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).
59
+ # Whether `type` is too gradual to seed (not a concrete dispatch target) — the negation of
60
+ # `ProtectionScanner#concrete_receiver?` (a union is concrete only when every arm is).
63
61
  def non_concrete?(type)
64
62
  case type
65
63
  when Type::Dynamic, Type::Top, Type::Bot then true
@@ -71,55 +69,50 @@ module Rigor
71
69
 
72
70
  # ADR-67 WD3 + WD5 — call-site parameter type inference (capped fixpoint).
73
71
  #
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).
72
+ # A `def` parameter with no RBS signature types `untyped` (the gradual entry point), so a
73
+ # param flowing into an ivar or a receiver drains everything downstream to `Dynamic`. This
74
+ # pass closes that hole: it walks every project file, types each call's positional
75
+ # arguments in their lexical scope, resolves which user-defined `def` each call targets,
76
+ # and records the **union of resolved call-site argument types** per parameter — TypeProf's
77
+ # "a parameter's type is the union of every actual argument across all call sites". WD5: it
78
+ # iterates (cap {DEFAULT_ROUNDS}), re-seeding each round with the previous round's inferred
79
+ # parameters, so a parameter passed *another* parameter is typed one hop further per round
80
+ # (round 1 alone is the single-level pass).
85
81
  #
86
82
  # 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.
83
+ # `StatementEvaluator#build_method_entry_scope` reconstructs from the lexical class path —
84
+ # so the consumer can seed an undeclared parameter with its inferred type. The table is
85
+ # precision-additive only: it never feeds a parameter-boundary diagnostic (an inferred type
86
+ # lives solely as a body local, and the boundary rules consult RBS, not body locals — see
87
+ # ADR-67 WD1), so being wrong cannot manufacture a false positive at a caller.
93
88
  #
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.
89
+ # Soundness (WD4): the inferred type is a sound over-approximation only when every
90
+ # contributed call site resolves to a concrete argument type. Any `Dynamic` / `Top` /
91
+ # `Bot` argument (a `send`/dynamic-dispatch caller, or a parameter not yet typed in an
92
+ # earlier round) **poisons** the parameter, which then contributes nothing this round (it
93
+ # may type in a later round once its own argument resolves). Unresolved call sites do not
94
+ # contribute.
100
95
  #
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).
96
+ # What it does NOT do yet (deferred — the check-wiring slice): feeding the table into the
97
+ # `check` walk (only `coverage --protection` consumes it today), keyword / optional / rest
98
+ # / block parameters, inherited-method receivers, top-level helpers, and a rigorous
99
+ # closed-call-site-set proof (the union is optimistic over resolved sites — acceptable
100
+ # because the only consumer is the protection metric, which runs no diagnostics).
107
101
  class ParameterInferenceCollector
108
102
  EMPTY = {}.freeze
109
103
 
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.
104
+ # A defensive widening cap (ADR-41): a parameter unioned from more than this many distinct
105
+ # concrete call-site types is widened to `untyped` (poisoned) rather than carrying an
106
+ # unbounded union.
113
107
  MAX_CALL_SITE_TYPES = 16
114
108
 
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.
109
+ # WD5 — the call-site union is a worklist fixpoint: each round re-types the project with
110
+ # the previous round's inferred parameters seeded, so a parameter passed *another*
111
+ # parameter is typed one hop further per round. Capped (no true-convergence requirement —
112
+ # the metric tolerates a bounded approximation, and the table can oscillate at the margin
113
+ # since a newly resolved receiver can surface a fresh untyped-argument call site). The cap
114
+ # matches the `Inference::BodyFixpoint` cap-3 convention. Round 1 alone is the
115
+ # single-level pass.
123
116
  DEFAULT_ROUNDS = 3
124
117
 
125
118
  # @param files [Array<String>] project `.rb` paths to scan for call sites.
@@ -136,10 +129,10 @@ module Rigor
136
129
  @environment = environment
137
130
  @target_ruby = target_ruby
138
131
  @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).
132
+ # Reset per round (see {#run_round}). `[[class, method, kind], param_sym]` => [Type] of
133
+ # observed concrete arguments (a default-block Hash, not a `{}` literal, so the
134
+ # analyzer types its reads generically — {#finalize}), plus the ids widened to
135
+ # `untyped` (an untyped / over-cap argument).
143
136
  @type_observations = Hash.new { |hash, id| hash[id] = [] }
144
137
  @poisoned_params = Set.new
145
138
  end
@@ -159,8 +152,8 @@ module Rigor
159
152
 
160
153
  private
161
154
 
162
- # Parse every project file once; rounds re-index the cached ASTs against an
163
- # evolving seed rather than re-parsing.
155
+ # Parse every project file once; rounds re-index the cached ASTs against an evolving seed
156
+ # rather than re-parsing.
164
157
  def parse_all
165
158
  @files.filter_map do |path|
166
159
  result = Prism.parse(File.read(path), filepath: path, version: @target_ruby)
@@ -170,8 +163,8 @@ module Rigor
170
163
  end
171
164
  end
172
165
 
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.
166
+ # One fixpoint round: re-type every file with `seed_table` (the previous round's inferred
167
+ # parameters) seeded, collecting the next round's table.
175
168
  def run_round(parsed, discovery_tables, seed_table)
176
169
  @type_observations = Hash.new { |hash, id| hash[id] = [] }
177
170
  @poisoned_params = Set.new
@@ -185,10 +178,10 @@ module Rigor
185
178
  finalize
186
179
  end
187
180
 
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).
181
+ # A scope carrying the cross-file discovery index (so `Foo.new` receivers and
182
+ # implicit-self calls resolve to a user `def`) plus the prior round's inferred parameters
183
+ # (so an argument that reads a parameter types to its current inferred type — the WD5
184
+ # propagation).
192
185
  def build_seed_scope(discovery_tables, seed_table)
193
186
  base = Scope.empty(environment: @environment)
194
187
  tables = discovery_tables
@@ -209,9 +202,8 @@ module Rigor
209
202
  end
210
203
  tables
211
204
  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.
205
+ # Discovery is best-effort; a malformed corner of the project must not crash the
206
+ # protection scan. Without discovery the collector simply resolves fewer call sites.
215
207
  {}
216
208
  end
217
209
 
@@ -240,35 +232,41 @@ module Rigor
240
232
  return if callee.nil?
241
233
 
242
234
  class_name, method, kind, def_node = callee
243
- requireds = simple_requireds(def_node)
244
- return if requireds.nil? || requireds.size != args.size
235
+ requireds = leading_requireds(def_node)
236
+ # Enough positional arguments to fill every leading required (extra args map to trailing
237
+ # optional / rest parameters, which this pass does not infer). Fewer would be an arity error
238
+ # or a mis-resolved callee; skip rather than mis-attribute.
239
+ return if requireds.nil? || args.size < requireds.size
245
240
 
246
241
  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))
242
+ requireds.each_with_index do |param, i|
243
+ arg_scope = index[args[i]]
244
+ accumulate(key, param.name, arg_scope&.type_of(args[i]))
250
245
  end
251
246
  end
252
247
 
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.
248
+ # The plain leading positional arguments. A trailing keyword hash (`f(x, k: 1)`) or block-pass
249
+ # (`f(x, &blk)`) is dropped it occupies no positional slot, so the leading args still map to
250
+ # the leading required parameters. Returns nil when the call carries a splat (`f(*xs, x)`) or
251
+ # forwarding (`f(...)`) argument: those make an argument's position depend on runtime length,
252
+ # breaking the positional-index ↔ parameter mapping, so the call site is skipped.
257
253
  def positional_args(call_node)
258
254
  arguments = call_node.arguments
259
255
  return [] if arguments.nil?
260
256
 
261
257
  list = arguments.arguments
262
- return nil if list.any? { |arg| non_plain_argument?(arg) }
258
+ return nil if list.any? { |arg| position_breaking_argument?(arg) }
263
259
 
264
- list
260
+ list.reject { |arg| non_positional_argument?(arg) }
265
261
  end
266
262
 
267
- def non_plain_argument?(arg)
268
- arg.is_a?(Prism::SplatNode) ||
269
- arg.is_a?(Prism::KeywordHashNode) ||
263
+ def position_breaking_argument?(arg)
264
+ arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode)
265
+ end
266
+
267
+ def non_positional_argument?(arg)
268
+ arg.is_a?(Prism::KeywordHashNode) ||
270
269
  arg.is_a?(Prism::BlockArgumentNode) ||
271
- arg.is_a?(Prism::ForwardingArgumentsNode) ||
272
270
  arg.is_a?(Prism::AssocNode) ||
273
271
  arg.is_a?(Prism::AssocSplatNode)
274
272
  end
@@ -307,16 +305,19 @@ module Rigor
307
305
  per_class && per_class[method]
308
306
  end
309
307
 
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)
308
+ # The leading required-positional parameters the ones a call's leading positional arguments
309
+ # map to. Ruby orders parameters requireds-first, so optional / rest / keyword / block
310
+ # parameters that FOLLOW the requireds do not disturb the leading positional-index ↔
311
+ # required-parameter mapping; this covers the common `def f(x, opts = {}, **kw)` /
312
+ # `def f(x, y, &block)` shapes the earlier all-required contract skipped entirely. Returns nil
313
+ # only when a required slot is a destructured `(a, b)` (no bindable name) or a post-rest
314
+ # required (`def f(a, *b, c)` — `c` maps to a trailing arg, breaking the leading-prefix
315
+ # assumption for the whole call), which would mis-attribute arguments.
316
+ def leading_requireds(def_node)
315
317
  params = def_node.parameters
316
318
  return [] if params.nil?
317
319
  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?
320
+ return nil unless params.posts.empty?
320
321
  return nil unless params.requireds.all?(Prism::RequiredParameterNode)
321
322
 
322
323
  params.requireds
@@ -335,26 +336,25 @@ module Rigor
335
336
  end
336
337
  end
337
338
 
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.
339
+ # A poisoned parameter is dropped from the observation store and recorded so later call
340
+ # sites short-circuit. `id` is the `[[class, method, kind], param]` pair.
341
341
  def poison(id)
342
342
  @poisoned_params << id
343
343
  @type_observations.delete(id)
344
344
  end
345
345
 
346
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.
347
+ # `result` is a default-block Hash (not a `{}` literal) so the analyzer types its reads
348
+ # generically rather than folding the empty shape — the nesting writes stay plain
349
+ # assignments, no literal-fold conditions.
350
350
  result = Hash.new { |hash, key| hash[key] = {} }
351
351
  @type_observations.each do |id, observations|
352
352
  next if @poisoned_params.include?(id)
353
353
  next if observations.empty?
354
354
 
355
355
  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.
356
+ # A union that collapsed to a non-concrete shape (e.g. a gradual arm leaked in) is no
357
+ # better than `untyped`; drop it.
358
358
  next if ParameterArgTypes.non_concrete?(union)
359
359
 
360
360
  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
@@ -8,22 +8,17 @@ require_relative "../source/constant_path"
8
8
 
9
9
  module Rigor
10
10
  module Inference
11
- # ADR-17 slice 2 — pre-pass scanner. Walks every file the user
12
- # listed under `pre_eval:` and harvests every `def` /
13
- # `def self.` declaration inside a class / module body into a
14
- # {ProjectPatchedMethods} registry the dispatcher consults
15
- # below the plugin tier.
11
+ # ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
12
+ # harvests every `def` / `def self.` declaration inside a class / module body into a
13
+ # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier.
16
14
  #
17
- # The walker is intentionally a strict subset of
18
- # {Rigor::Inference::ScopeIndexer}'s machinery: it only needs
19
- # `class C; def m; end; end` shape recognition, not full
20
- # inference. Parse errors degrade to a fail-soft `:warning`
21
- # `pre-eval.parse-error` diagnostic accumulated alongside
22
- # the registry; per ADR-17 § "Failure modes" a parse failure
23
- # in a pre-eval file MUST NOT abort the rest of the run.
15
+ # The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
16
+ # only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
17
+ # to a fail-soft `:warning` `pre-eval.parse-error` diagnostic accumulated alongside the registry;
18
+ # per ADR-17 § "Failure modes" a parse failure in a pre-eval file MUST NOT abort the rest of the run.
24
19
  module ProjectPatchedScanner
25
- # Frozen scan outcome carrying the populated registry and
26
- # the per-file warnings the runner emits at run start.
20
+ # Frozen scan outcome carrying the populated registry and the per-file warnings the runner emits
21
+ # at run start.
27
22
  class Result < Data.define(:registry, :diagnostics)
28
23
  def initialize(registry:, diagnostics: [])
29
24
  super(
@@ -35,18 +30,13 @@ module Rigor
35
30
 
36
31
  module_function
37
32
 
38
- # @param paths [Array<String>] absolute paths to the
39
- # pre-eval files. The runner has already validated that
40
- # each path exists (slice-1 `pre-eval.file-not-found`
41
- # `:error` covers missing entries); the scanner does NOT
42
- # re-check existence.
43
- # @param buffer [Rigor::Analysis::BufferBinding, nil]
44
- # editor-mode buffer binding. When set, the scanner reads
45
- # the buffer's physical bytes if a pre-eval entry matches
46
- # the logical path, so users editing a monkey-patch file
47
- # see the in-flight version in their analysis.
48
- # @return [Result] the populated registry plus any
49
- # per-file warnings.
33
+ # @param paths [Array<String>] absolute paths to the pre-eval files. The runner has already
34
+ # validated that each path exists (slice-1 `pre-eval.file-not-found` `:error` covers missing
35
+ # entries); the scanner does NOT re-check existence.
36
+ # @param buffer [Rigor::Analysis::BufferBinding, nil] editor-mode buffer binding. When set, the
37
+ # scanner reads the buffer's physical bytes if a pre-eval entry matches the logical path, so
38
+ # users editing a monkey-patch file see the in-flight version in their analysis.
39
+ # @return [Result] the populated registry plus any per-file warnings.
50
40
  def scan(paths, buffer: nil)
51
41
  entries = []
52
42
  diagnostics = []
@@ -58,13 +48,10 @@ module Rigor
58
48
  )
59
49
  end
60
50
 
61
- # ADR-17 § "Failure modes" — when two pre-eval entries
62
- # declare the same `(class_name, method_name, kind)` triple,
63
- # emit one `:info` `pre-eval.duplicate-declaration`
64
- # diagnostic per collision. The registry's first-write-wins
65
- # behaviour is unchanged; the diagnostic just makes the
66
- # shadowing visible so users notice when a later patch
67
- # is silently masked.
51
+ # ADR-17 § "Failure modes" — when two pre-eval entries declare the same `(class_name, method_name,
52
+ # kind)` triple, emit one `:info` `pre-eval.duplicate-declaration` diagnostic per collision. The
53
+ # registry's first-write-wins behaviour is unchanged; the diagnostic just makes the shadowing
54
+ # visible so users notice when a later patch is silently masked.
68
55
  def duplicate_declaration_diagnostics(entries)
69
56
  seen = {}
70
57
  entries.each_with_object([]) do |entry, acc|
@@ -129,10 +116,9 @@ module Rigor
129
116
  end
130
117
  private_class_method :parse_error_diagnostic
131
118
 
132
- # Builds a diagnostic Hash-shape the runner translates to a
133
- # `Rigor::Analysis::Diagnostic`. The scanner intentionally
134
- # does NOT depend on the analysis layer (it's a pre-pass);
135
- # the runner adapts at the call site.
119
+ # Builds a diagnostic Hash-shape the runner translates to a `Rigor::Analysis::Diagnostic`. The
120
+ # scanner intentionally does NOT depend on the analysis layer (it's a pre-pass); the runner
121
+ # adapts at the call site.
136
122
  def build_diagnostic(path:, line:, column:, severity:, rule:, message:)
137
123
  { path: path, line: line, column: column, severity: severity, rule: rule, message: message }
138
124
  end