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
@@ -6,28 +6,22 @@ require "rigor/type"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class Rspec < Rigor::Plugin::Base
9
- # Resolves the runtime type of a
10
- # `let(:name) { body }` or `subject(:name) { body }`
11
- # block by pattern-matching its body's tail expression.
9
+ # Resolves the runtime type of a `let(:name) { body }` or `subject(:name) { body }` block by
10
+ # pattern-matching its body's tail expression.
12
11
  #
13
12
  # Recognised body shapes (floor):
14
13
  #
15
14
  # - `ConstantClass.new(...)` → `Nominal[ConstantClass]`
16
15
  # - `Foo::Bar.new(...)` → `Nominal[Foo::Bar]`
17
- # - `described_class.new(...)` → `Nominal[<describe const>]`
18
- # when the surrounding describe block has a constant
19
- # anchor.
20
- # - `create(:factory)` / `build(:factory)` /
21
- # `build_stubbed(:factory)` / `attributes_for(:factory)`
22
- # → `Nominal[<factory.model_class>]` when the
23
- # `:factory_index` ADR-9 fact is available and the
16
+ # - `described_class.new(...)` → `Nominal[<describe const>]` when the surrounding describe block has
17
+ # a constant anchor.
18
+ # - `create(:factory)` / `build(:factory)` / `build_stubbed(:factory)` / `attributes_for(:factory)` →
19
+ # `Nominal[<factory.model_class>]` when the `:factory_index` ADR-9 fact is available and the
24
20
  # factory name is known.
25
- # - `FactoryBot.create(:factory)` etc. — same as the
26
- # implicit-receiver factorybot form.
21
+ # - `FactoryBot.create(:factory)` etc. — same as the implicit-receiver factorybot form.
27
22
  #
28
- # Returns `nil` for any other body shape. Multi-statement
29
- # bodies use the LAST statement (the block's return
30
- # value).
23
+ # Returns `nil` for any other body shape. Multi-statement bodies use the LAST statement (the block's
24
+ # return value).
31
25
  module LetTypeResolver
32
26
  FACTORYBOT_ENTRY_METHODS = %i[
33
27
  create build build_stubbed attributes_for
@@ -38,9 +32,8 @@ module Rigor
38
32
 
39
33
  # @param block_node [Prism::BlockNode]
40
34
  # @param describe_const [String, nil]
41
- # @param factory_index [Object, nil] something
42
- # responding to `find(factory_name)` and returning
43
- # an entry that responds to `model_class`.
35
+ # @param factory_index [Object, nil] something responding to `find(factory_name)` and returning an
36
+ # entry that responds to `model_class`.
44
37
  # @param environment [Rigor::Environment, nil]
45
38
  # @return [Rigor::Type, nil]
46
39
  def resolve(block_node, describe_const:, factory_index:, environment:)
@@ -103,10 +96,8 @@ module Rigor
103
96
  nominal_for(model_class, environment: environment)
104
97
  end
105
98
 
106
- # Recognises both `create(:name)` (implicit receiver,
107
- # via `include FactoryBot::Syntax::Methods`) and the
108
- # explicit `FactoryBot.create(:name)` /
109
- # `FactoryGirl.create(:name)` (legacy) forms.
99
+ # Recognises both `create(:name)` (implicit receiver, via `include FactoryBot::Syntax::Methods`)
100
+ # and the explicit `FactoryBot.create(:name)` / `FactoryGirl.create(:name)` (legacy) forms.
110
101
  def factorybot_call?(call_node)
111
102
  return false unless FACTORYBOT_ENTRY_METHODS.include?(call_node.name)
112
103
 
@@ -8,59 +8,42 @@ require "rigor/flow_contribution/fact"
8
8
  module Rigor
9
9
  module Plugin
10
10
  class Rspec < Rigor::Plugin::Base
11
- # Recognises `expect(x).to MATCHER`
12
- # patterns at per-call recognition time and emits
13
- # `post_return_facts` that narrow the named local on the
14
- # post-call edge.
11
+ # Recognises `expect(x).to MATCHER` patterns at per-call recognition time and emits
12
+ # `post_return_facts` that narrow the named local on the post-call edge.
15
13
  #
16
- # The supported matchers are the lowest-false-positive
17
- # floor of the RSpec matcher DSL:
14
+ # The supported matchers are the lowest-false-positive floor of the RSpec matcher DSL:
18
15
  #
19
- # - `be_a(T)` / `be_kind_of(T)` — `x is_a?(T)` style class
20
- # membership; narrows `x` to `T`.
21
- # - `be_instance_of(T)` exact-class match; narrows
22
- # `x` to `T` (the engine currently treats Nominal[T]
23
- # uniformly, so the distinction with `be_a` is observed
24
- # at the carrier level but not the runtime).
16
+ # - `be_a(T)` / `be_kind_of(T)` — `x is_a?(T)` style class membership; narrows `x` to `T`.
17
+ # - `be_instance_of(T)` — exact-class match; narrows `x` to `T` (the engine currently treats
18
+ # Nominal[T] uniformly, so the distinction with `be_a` is observed at the carrier level but not the
19
+ # runtime).
25
20
  # - `be_nil` — narrows `x` to `Constant<nil>`.
26
- # - `eq(LITERAL)` for a literal-value argument — narrows
27
- # `x` to `Constant<literal>`.
21
+ # - `eq(LITERAL)` for a literal-value argument — narrows `x` to `Constant<literal>`.
28
22
  #
29
- # The matchers below this floor (`be_truthy` / `be_falsey`
30
- # / `be_within` / `be > / < / >=` / `include` / `start_with`
31
- # / `match(regex)` / `raise_error` / receive-style mocks)
32
- # require either edge-aware fragments (`truthy_facts` /
33
- # `falsey_facts` rather than `post_return_facts`) or
34
- # diagnostic-only enforcement; both are queued for follow-up
35
- # slices.
23
+ # The matchers below this floor (`be_truthy` / `be_falsey` / `be_within` / `be > / < / >=` /
24
+ # `include` / `start_with` / `match(regex)` / `raise_error` / receive-style mocks) require either
25
+ # edge-aware fragments (`truthy_facts` / `falsey_facts` rather than `post_return_facts`) or
26
+ # diagnostic-only enforcement; both are queued for follow-up slices.
36
27
  #
37
28
  # The analyzer fires ONLY when:
38
29
  #
39
- # 1. The call node is one of `<recv>.to(matcher)` /
40
- # `<recv>.not_to(matcher)` / `<recv>.to_not(matcher)`.
41
- # `not_to` / `to_not` flip the fact's `negative` flag
42
- # so the engine narrows AWAY from the matcher's type
43
- # (e.g. `not_to be_nil` removes nil from the receiver).
44
- # 2. The receiver is `expect(<local_var>)` — exactly one
45
- # positional argument that's a LocalVariableReadNode.
46
- # Composite receivers (`expect(foo.bar)`,
47
- # `expect { ... }.to raise_error`) fall through.
48
- # 3. The matcher is one of the recognised forms above
49
- # (`be_a` / `be_kind_of` / `be_instance_of` /
50
- # `be_an_instance_of` / `be_nil` / `eq(literal)` /
51
- # `eql(literal)` / `match(/regex/)`).
30
+ # 1. The call node is one of `<recv>.to(matcher)` / `<recv>.not_to(matcher)` /
31
+ # `<recv>.to_not(matcher)`. `not_to` / `to_not` flip the fact's `negative` flag so the engine
32
+ # narrows AWAY from the matcher's type (e.g. `not_to be_nil` removes nil from the receiver).
33
+ # 2. The receiver is `expect(<local_var>)` exactly one positional argument that's a
34
+ # LocalVariableReadNode. Composite receivers (`expect(foo.bar)`, `expect { ... }.to raise_error`)
35
+ # fall through.
36
+ # 3. The matcher is one of the recognised forms above (`be_a` / `be_kind_of` / `be_instance_of` /
37
+ # `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)`).
52
38
  module MatcherAnalyzer
53
39
  module_function
54
40
 
55
- # @param call_node [Prism::CallNode] the call whose
56
- # contribution we're computing. Returns nil when the
57
- # call shape does not match `expect(local).to matcher`.
58
- # @param environment [Rigor::Environment, nil] the
59
- # surrounding environment used to resolve a matcher's
60
- # class-name argument to a `Type::Nominal`. When nil,
61
- # class-name resolution falls back to a bare
62
- # `Nominal[<name>]` carrier (sound — the receiver
63
- # constant may be a user class not in RBS).
41
+ # @param call_node [Prism::CallNode] the call whose contribution we're computing. Returns nil when
42
+ # the call shape does not match `expect(local).to matcher`.
43
+ # @param environment [Rigor::Environment, nil] the surrounding environment used to resolve a
44
+ # matcher's class-name argument to a `Type::Nominal`. When nil, class-name resolution falls back
45
+ # to a bare `Nominal[<name>]` carrier (sound — the receiver constant may be a user class not in
46
+ # RBS).
64
47
  # @return [Rigor::FlowContribution, nil]
65
48
  def contribution_for(call_node, environment:)
66
49
  verb = assertion_verb(call_node)
@@ -136,11 +119,9 @@ module Rigor
136
119
  when :eq, :eql
137
120
  constant_type_for_literal_arg(matcher)
138
121
  when :match
139
- # `match(/regex/)` narrows x to String. `match("...")`
140
- # or `match(arbitrary_object)` falls through the
141
- # broader matcher dispatch needs the receiver to be a
142
- # String, but we can only assert that for a literal
143
- # regex.
122
+ # `match(/regex/)` narrows x to String. `match("...")` or `match(arbitrary_object)` falls
123
+ # through the broader matcher dispatch needs the receiver to be a String, but we can only
124
+ # assert that for a literal regex.
144
125
  string_type_for_regex_arg(matcher)
145
126
  end
146
127
  end
@@ -184,10 +165,9 @@ module Rigor
184
165
  NO_LITERAL = Object.new.freeze
185
166
  private_constant :NO_LITERAL
186
167
 
187
- # Returns the Ruby value of a literal-AST argument, or
188
- # `NO_LITERAL` when the node isn't a recognised literal
189
- # shape. Recognised forms cover the common `eq(literal)`
190
- # case: integer, float, true/false/nil, string, symbol.
168
+ # Returns the Ruby value of a literal-AST argument, or `NO_LITERAL` when the node isn't a
169
+ # recognised literal shape. Recognised forms cover the common `eq(literal)` case: integer, float,
170
+ # true/false/nil, string, symbol.
191
171
  def literal_value_for(node)
192
172
  case node
193
173
  when Prism::IntegerNode then node.value
@@ -206,10 +186,8 @@ module Rigor
206
186
  args.nil? || args.empty?
207
187
  end
208
188
 
209
- # Renders a `Prism::ConstantReadNode` /
210
- # `Prism::ConstantPathNode` chain as a `"Foo::Bar"`
211
- # String. Returns nil when the node isn't a constant
212
- # reference.
189
+ # Renders a `Prism::ConstantReadNode` / `Prism::ConstantPathNode` chain as a `"Foo::Bar"` String.
190
+ # Returns nil when the node isn't a constant reference.
213
191
  def constant_path_name(node)
214
192
  case node
215
193
  when Prism::ConstantReadNode
@@ -6,52 +6,40 @@ require "rigor/source/literals"
6
6
  module Rigor
7
7
  module Plugin
8
8
  class Rspec < Rigor::Plugin::Base
9
- # Walks an RSpec spec file's AST and yields, for each
10
- # describe / context block (including the outer
11
- # `RSpec.describe`), a `Scope` value with the `let`
12
- # and `subject` declarations recorded inside that
9
+ # Walks an RSpec spec file's AST and yields, for each describe / context block (including the outer
10
+ # `RSpec.describe`), a `Scope` value with the `let` and `subject` declarations recorded inside that
13
11
  # scope.
14
12
  #
15
- # Scope hierarchy is preserved: each `Scope` carries
16
- # a list of its `nested_scopes`. The analyzer uses the
17
- # hierarchy to detect cross-scope shadowing.
13
+ # Scope hierarchy is preserved: each `Scope` carries a list of its `nested_scopes`. The analyzer uses
14
+ # the hierarchy to detect cross-scope shadowing.
18
15
  #
19
- # Recognised scope methods (when called without a
20
- # receiver, or with `RSpec` as the receiver):
16
+ # Recognised scope methods (when called without a receiver, or with `RSpec` as the receiver):
21
17
  #
22
- # - `describe` / `context` — both open a new nested
23
- # scope.
18
+ # - `describe` / `context` — both open a new nested scope.
24
19
  # - `RSpec.describe` — the outermost scope.
25
20
  #
26
- # Recognised declaration methods (inside any scope,
27
- # called without a receiver):
21
+ # Recognised declaration methods (inside any scope, called without a receiver):
28
22
  #
29
- # - `let(:name) { ... }` — caches the block result
30
- # per-example; recorded as a Declaration.
31
- # - `let!(:name) { ... }` — same, but evaluated
32
- # eagerly via a `before` hook; same shape for our
23
+ # - `let(:name) { ... }` — caches the block result per-example; recorded as a Declaration.
24
+ # - `let!(:name) { ... }` — same, but evaluated eagerly via a `before` hook; same shape for our
33
25
  # purposes.
34
- # - `subject(:name) { ... }` — special-cases name
35
- # `:subject` when called without a name.
26
+ # - `subject(:name) { ... }` — special-cases name `:subject` when called without a name.
36
27
  module ScopeWalker
37
28
  SCOPE_METHODS = %i[describe context].freeze
38
29
  DECLARATION_METHODS = %i[let let! subject].freeze
39
30
 
40
31
  # @!attribute [r] kind
41
- # `:describe`, `:context`, or `:rspec_describe` for
42
- # the root.
32
+ # `:describe`, `:context`, or `:rspec_describe` for the root.
43
33
  # @!attribute [r] declarations
44
34
  # `Array<Declaration>` declared in this scope.
45
35
  # @!attribute [r] nested_scopes
46
36
  # `Array<Scope>` nested under this scope.
47
37
  # @!attribute [r] location
48
- # `Prism::Location` of the call node that opened
49
- # this scope.
38
+ # `Prism::Location` of the call node that opened this scope.
50
39
  Scope = Struct.new(:kind, :declarations, :nested_scopes, :location, keyword_init: true)
51
40
 
52
41
  # @!attribute [r] name
53
- # `Symbol` declared name (`:user`, `:subject`,
54
- # ...).
42
+ # `Symbol` declared name (`:user`, `:subject`, ...).
55
43
  # @!attribute [r] kind
56
44
  # `:let`, `:let!`, or `:subject`.
57
45
  # @!attribute [r] location
@@ -62,18 +50,15 @@ module Rigor
62
50
 
63
51
  module_function
64
52
 
65
- # Walks the parsed file and returns an array of
66
- # top-level scopes (each `RSpec.describe` is a
67
- # separate root). Files with no recognised scopes
68
- # return an empty array.
53
+ # Walks the parsed file and returns an array of top-level scopes (each `RSpec.describe` is a
54
+ # separate root). Files with no recognised scopes return an empty array.
69
55
  def collect_scopes(root)
70
56
  scopes = []
71
57
  walk_top_level(root, scopes)
72
58
  scopes
73
59
  end
74
60
 
75
- # Walks every scope in a tree (root + descendants)
76
- # and yields each in turn.
61
+ # Walks every scope in a tree (root + descendants) and yields each in turn.
77
62
  def each_scope(scope, &)
78
63
  yield scope
79
64
  scope.nested_scopes.each { |child| each_scope(child, &) }
@@ -89,8 +74,7 @@ module Rigor
89
74
  end
90
75
  end
91
76
 
92
- # Returns true for `RSpec.describe ... do |...| ...
93
- # end` calls.
77
+ # Returns true for `RSpec.describe ... do |...| ... end` calls.
94
78
  def rspec_describe_call?(node)
95
79
  return false unless node.is_a?(Prism::CallNode)
96
80
  return false unless node.name == :describe
@@ -100,10 +84,8 @@ module Rigor
100
84
  %w[RSpec ::RSpec].include?(receiver_name)
101
85
  end
102
86
 
103
- # Returns true for `describe ... do ... end` /
104
- # `context ... do ... end` (called without an
105
- # explicit receiver — `RSpec.describe` is handled
106
- # separately by `rspec_describe_call?`).
87
+ # Returns true for `describe ... do ... end` / `context ... do ... end` (called without an
88
+ # explicit receiver `RSpec.describe` is handled separately by `rspec_describe_call?`).
107
89
  def nested_scope_call?(node)
108
90
  node.is_a?(Prism::CallNode) &&
109
91
  SCOPE_METHODS.include?(node.name) &&
@@ -117,9 +99,8 @@ module Rigor
117
99
  node.receiver.nil?
118
100
  end
119
101
 
120
- # Constructs a Scope from a describe / context /
121
- # RSpec.describe call node. Walks the block body
122
- # for declarations + nested scopes.
102
+ # Constructs a Scope from a describe / context / RSpec.describe call node. Walks the block body for
103
+ # declarations + nested scopes.
123
104
  def build_scope(call_node, kind:)
124
105
  declarations = []
125
106
  nested = []
@@ -146,8 +127,7 @@ module Rigor
146
127
 
147
128
  def build_declaration(call_node)
148
129
  first_arg = call_node.arguments&.arguments&.first
149
- # `subject(&block)` (no name) defaults to the
150
- # implicit subject; record it as `:subject`.
130
+ # `subject(&block)` (no name) defaults to the implicit subject; record it as `:subject`.
151
131
  if first_arg.nil?
152
132
  return nil unless call_node.name == :subject
153
133
 
@@ -10,62 +10,41 @@ require_relative "rspec/let_type_resolver"
10
10
 
11
11
  module Rigor
12
12
  module Plugin
13
- # rigor-rspec — validates RSpec `let` / `subject`
14
- # declarations within each describe / context scope,
15
- # narrows `expect(x).to MATCHER` assertions downstream,
16
- # and binds `let`/`subject` locals to their inferred
17
- # return types inside `it` bodies.
13
+ # rigor-rspec — validates RSpec `let` / `subject` declarations within each describe / context scope,
14
+ # narrows `expect(x).to MATCHER` assertions downstream, and binds `let`/`subject` locals to their
15
+ # inferred return types inside `it` bodies.
18
16
  #
19
17
  # Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
20
18
  # Ships four checks:
21
19
  #
22
- # 1. **Duplicate `let` / `subject` declarations** in
23
- # the same scope (`warning`). RSpec's runtime lets
24
- # the last declaration win, so the first one is
25
- # silently shadowed almost always a copy-paste
26
- # bug.
27
- # 2. **Self-referencing `let` / `subject`** — calling
28
- # the declared name *inside* its own block body
29
- # (`error`). At runtime this infinite-loops; users
30
- # typically meant to call a different method or
20
+ # 1. **Duplicate `let` / `subject` declarations** in the same scope (`warning`). RSpec's runtime lets
21
+ # the last declaration win, so the first one is silently shadowed — almost always a copy-paste bug.
22
+ # 2. **Self-referencing `let` / `subject`** — calling the declared name *inside* its own block body
23
+ # (`error`). At runtime this infinite-loops; users typically meant to call a different method or
31
24
  # forgot to introduce a `super`.
32
- # 3. **`expect(x).to MATCHER` narrowing** — narrows
33
- # the named local `x` on the post-call edge for
34
- # eight matchers (see `MatcherAnalyzer`).
35
- # 4. **`let`/`subject` local binding** — binds `let`
36
- # / `subject` names in `it` bodies to the block's
25
+ # 3. **`expect(x).to MATCHER` narrowing** — narrows the named local `x` on the post-call edge for eight
26
+ # matchers (see `MatcherAnalyzer`).
27
+ # 4. **`let`/`subject` local binding** — binds `let` / `subject` names in `it` bodies to the block's
37
28
  # inferred return type (see `LetTypeResolver`).
38
29
  #
39
30
  # ## Configuration
40
31
  #
41
- # No configuration knobs. The plugin walks every
42
- # analysed file for `RSpec.describe ... do` blocks;
43
- # spec files outside the project's `paths:` are not
44
- # scanned.
32
+ # No configuration knobs. The plugin walks every analysed file for `RSpec.describe ... do` blocks; spec
33
+ # files outside the project's `paths:` are not scanned.
45
34
  #
46
35
  # ## Limitations
47
36
  #
48
- # - **No let-typo detection.** Detecting an `it`
49
- # block's reference to a misspelled `let` name
50
- # requires resolving every method call inside the
51
- # block against the let scope chain, the included
52
- # modules, the matchers DSL, and helper methods.
53
- # Reliable diagnostics here need a much heavier
54
- # walker see the README's `Future direction`.
55
- # - **No mock-target validation.**
56
- # `expect(x).to receive(:nme)` validating against
57
- # `x`'s methods is a separate slice; it overlaps with
58
- # the engine's general method-existence
59
- # diagnostics and needs careful coordination to avoid
60
- # double-firing.
61
- # - **No shared-context resolution.** `include_context`,
62
- # `shared_context`, and `it_behaves_like` are
63
- # recognised as scope-opening calls but their
64
- # declarations are not pulled into the host scope.
65
- # - **Constant validation is not done here.**
66
- # `RSpec.describe SomeClass do` does not validate
67
- # `SomeClass`; the engine's `inference.unresolved-constant`
68
- # already catches that.
37
+ # - **No let-typo detection.** Detecting an `it` block's reference to a misspelled `let` name requires
38
+ # resolving every method call inside the block against the let scope chain, the included modules,
39
+ # the matchers DSL, and helper methods. Reliable diagnostics here need a much heavier walker — see
40
+ # the README's `Future direction`.
41
+ # - **No mock-target validation.** `expect(x).to receive(:nme)` validating against `x`'s methods is a
42
+ # separate slice; it overlaps with the engine's general method-existence diagnostics and needs
43
+ # careful coordination to avoid double-firing.
44
+ # - **No shared-context resolution.** `include_context`, `shared_context`, and `it_behaves_like` are
45
+ # recognised as scope-opening calls but their declarations are not pulled into the host scope.
46
+ # - **Constant validation is not done here.** `RSpec.describe SomeClass do` does not validate
47
+ # `SomeClass`; the engine's `inference.unresolved-constant` already catches that.
69
48
  class Rspec < Rigor::Plugin::Base
70
49
  manifest(
71
50
  id: "rspec",
@@ -79,10 +58,9 @@ module Rigor
79
58
  { plugin_id: "factorybot", name: :factory_index, optional: true }
80
59
  ],
81
60
  additional_initializers: [
82
- # ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`,
83
- # and `subject { @ivar = … }` establish ivar state before `it` bodies
84
- # run. Declaring them here suppresses the read-before-write nil
85
- # widening that would otherwise appear on those ivars in `it` / `specify`
61
+ # ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`, and `subject { @ivar = … }`
62
+ # establish ivar state before `it` bodies run. Declaring them here suppresses the
63
+ # read-before-write nil widening that would otherwise appear on those ivars in `it` / `specify`
86
64
  # sibling blocks.
87
65
  Rigor::Plugin::AdditionalInitializer.new(
88
66
  receiver_constraint: "RSpec::ExampleGroup",
@@ -92,53 +70,44 @@ module Rigor
92
70
  )
93
71
 
94
72
  def init(_services)
95
- # Per-path `LetScopeIndex` cache. The let-binding
96
- # `dynamic_return` rule (and its `file_methods:` gate)
97
- # consult the index per call node; building it once
98
- # per file is essential for performance.
73
+ # Per-path `LetScopeIndex` cache. The let-binding `dynamic_return` rule (and its `file_methods:`
74
+ # gate) consult the index per call node; building it once per file is essential for performance.
99
75
  @let_index_cache = {}
100
76
  end
101
77
 
102
78
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
103
- # Build the let-scope index for this file while we
104
- # have the parsed root in hand the let-binding rule
105
- # picks it up from `@let_index_cache` keyed on path.
79
+ # Build the let-scope index for this file while we have the parsed root in hand — the let-binding
80
+ # rule picks it up from `@let_index_cache` keyed on path.
106
81
  @let_index_cache[path] ||= LetScopeIndex.build(root)
107
82
  Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
108
83
  end
109
84
 
110
- # ADR-37 slice 2 — matcher narrowing
111
- # (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
85
+ # ADR-37 slice 2 — matcher narrowing (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
112
86
  # method-gated by the engine on the expectation verbs.
113
87
  narrowing_facts methods: %i[to not_to to_not] do |call_node, scope|
114
88
  MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
115
89
  end
116
90
 
117
- # ADR-52 slice 5a — binds local reads in `it` /
118
- # spec bodies to their `let(:name) { ... }` block's inferred return
119
- # type. The name set varies per file (each spec file's
120
- # `describe`/`let` structure), so the rule gates on the per-file
121
- # `file_methods:` form: the engine resolves the file's let names
122
- # once per analysed file and consults the block only for a listed
123
- # name; the line-scoped shadowing resolution stays in the block.
91
+ # ADR-52 slice 5a — binds local reads in `it` / spec bodies to their `let(:name) { ... }` block's
92
+ # inferred return type. The name set varies per file (each spec file's `describe`/`let` structure),
93
+ # so the rule gates on the per-file `file_methods:` form: the engine resolves the file's let names
94
+ # once per analysed file and consults the block only for a listed name; the line-scoped shadowing
95
+ # resolution stays in the block.
124
96
  dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
125
97
  let_binding_return_type(call_node, scope)
126
98
  end
127
99
 
128
100
  private
129
101
 
130
- # The `file_methods:` gate set — every `let` / `subject` name the
131
- # file declares anywhere. A safe over-approximation of the block's
132
- # own `let_block_at` line-scoped lookup (a name read outside its
102
+ # The `file_methods:` gate set — every `let` / `subject` name the file declares anywhere. A safe
103
+ # over-approximation of the block's own `let_block_at` line-scoped lookup (a name read outside its
133
104
  # describe scope passes the gate and is declined by the block).
134
105
  def let_names_for(path)
135
106
  let_scope_index_for(path)&.let_names || []
136
107
  end
137
108
 
138
- # When the call node is a no-receiver
139
- # method call (`user`, `subject`, etc.) inside an RSpec
140
- # `describe` block whose lets include a matching name,
141
- # return the let block's inferred type.
109
+ # When the call node is a no-receiver method call (`user`, `subject`, etc.) inside an RSpec
110
+ # `describe` block whose lets include a matching name, return the let block's inferred type.
142
111
  def let_binding_return_type(call_node, scope)
143
112
  return nil if scope.nil?
144
113
  return nil unless candidate_call?(call_node)
@@ -163,10 +132,9 @@ module Rigor
163
132
  call_node.is_a?(Prism::CallNode) &&
164
133
  call_node.receiver.nil? &&
165
134
  call_node.block.nil? &&
166
- # Calls with arguments are matcher / DSL invocations,
167
- # not let-bound name reads. `subject` / `user` etc.
168
- # without args are the implicit-method-call shape RSpec
169
- # uses to expose let / subject in `it` bodies.
135
+ # Calls with arguments are matcher / DSL invocations, not let-bound name reads. `subject` /
136
+ # `user` etc. without args are the implicit-method-call shape RSpec uses to expose let / subject
137
+ # in `it` bodies.
170
138
  (call_node.arguments.nil? || call_node.arguments.arguments.empty?)
171
139
  end
172
140
 
@@ -7,48 +7,39 @@ require_relative "http_status_codes"
7
7
  module Rigor
8
8
  module Plugin
9
9
  class RspecRails < Rigor::Plugin::Base
10
- # Validates `expect(response).to have_http_status(arg)` /
11
- # the negated `.not_to have_http_status(arg)` and the
12
- # bare matcher form `should have_http_status(arg)`.
10
+ # Validates `expect(response).to have_http_status(arg)` / the negated `.not_to have_http_status(arg)`
11
+ # and the bare matcher form `should have_http_status(arg)`.
13
12
  #
14
13
  # Recognised argument shapes:
15
14
  #
16
- # - **IntegerNode**: must be in `100..599`. Numbers outside
17
- # that range fire `have_http_status.out-of-range`.
18
- # - **SymbolNode**: must be one of the known
19
- # `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys OR a Rails
20
- # status-group alias (`:success` / `:successful` /
21
- # `:missing` / `:redirect` / `:error` / `:client_error` /
22
- # `:server_error` / `:informational`). Unknown symbols
23
- # fire `have_http_status.unknown-symbol` (typo-flavoured).
24
- # - **StringNode**: passed through silently (a literal
25
- # `"200"` is accepted by Rails at runtime; we don't
26
- # second-guess the user's intent for the String form).
27
- # - **Anything else** (variable, method call, computed
28
- # expression): skipped — the plugin can't statically
29
- # prove the runtime value.
15
+ # - **IntegerNode**: must be in `100..599`. Numbers outside that range fire
16
+ # `have_http_status.out-of-range`.
17
+ # - **SymbolNode**: must be one of the known `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys OR a Rails
18
+ # status-group alias (`:success` / `:successful` / `:missing` / `:redirect` / `:error` /
19
+ # `:client_error` / `:server_error` / `:informational`). Unknown symbols fire
20
+ # `have_http_status.unknown-symbol` (typo-flavoured).
21
+ # - **StringNode**: passed through silently (a literal `"200"` is accepted by Rails at runtime; we
22
+ # don't second-guess the user's intent for the String form).
23
+ # - **Anything else** (variable, method call, computed expression): skipped — the plugin can't
24
+ # statically prove the runtime value.
30
25
  #
31
- # Walks every `Prism::CallNode` looking for matcher
32
- # invocations rather than only the `expect(...).to ...`
33
- # chain because `have_http_status` is also used as a
34
- # plain matcher inside Rails' `assert_response` shim and
35
- # in shared-context bodies; the diagnostic is the same
36
- # regardless of the surrounding chain.
26
+ # Walks every `Prism::CallNode` looking for matcher invocations rather than only the
27
+ # `expect(...).to ...` chain because `have_http_status` is also used as a plain matcher inside Rails'
28
+ # `assert_response` shim and in shared-context bodies; the diagnostic is the same regardless of the
29
+ # surrounding chain.
37
30
  module HaveHttpStatusAnalyzer
38
- # One matcher violation: the kebab-case `rule` and the
39
- # human-readable `message`. Carries no location/path the
40
- # caller (the `node_rule` block) positions the diagnostic at the
41
- # matcher's `message_loc` via `Plugin::Base#diagnostic`.
31
+ # One matcher violation: the kebab-case `rule` and the human-readable `message`. Carries no
32
+ # location/path — the caller (the `node_rule` block) positions the diagnostic at the matcher's
33
+ # `message_loc` via `Plugin::Base#diagnostic`.
42
34
  Violation = Struct.new(:rule, :message, keyword_init: true)
43
35
 
44
36
  MATCHER_NAME = :have_http_status
45
37
 
46
38
  module_function
47
39
 
48
- # The matcher violation for a single CallNode, or nil when the
49
- # node is not a `have_http_status(<int|symbol>)` matcher or its
50
- # argument is statically valid. ADR-37: the engine owns the
51
- # walk, so this is per-node logic — no traversal here.
40
+ # The matcher violation for a single CallNode, or nil when the node is not a
41
+ # `have_http_status(<int|symbol>)` matcher or its argument is statically valid. ADR-37: the engine
42
+ # owns the walk, so this is per-node logic no traversal here.
52
43
  #
53
44
  # @param call_node [Prism::Node]
54
45
  # @return [Violation, nil]
@@ -58,12 +49,9 @@ module Rigor
58
49
  diagnostic_for(call_node)
59
50
  end
60
51
 
61
- # `have_http_status` is a matcher — called either with
62
- # no receiver (`have_http_status(200)` inside `.to(...)`)
63
- # or with a receiver chain we don't care about here.
64
- # Either way we want the call node whose name is
65
- # `have_http_status` and that carries exactly one
66
- # positional argument.
52
+ # `have_http_status` is a matcher — called either with no receiver (`have_http_status(200)` inside
53
+ # `.to(...)`) or with a receiver chain we don't care about here. Either way we want the call node
54
+ # whose name is `have_http_status` and that carries exactly one positional argument.
67
55
  def call_to_matcher?(node)
68
56
  node.is_a?(Prism::CallNode) &&
69
57
  node.name == MATCHER_NAME &&
@@ -97,8 +85,8 @@ module Rigor
97
85
  def symbol_violation(symbol_node)
98
86
  sym = symbol_node.unescaped.to_sym
99
87
  known = HttpStatusCodes.known_symbols
100
- # ADR-39: Rack unavailable → the accepted symbol set is
101
- # unknowable, so decline rather than flag from a guess.
88
+ # ADR-39: Rack unavailable → the accepted symbol set is unknowable, so decline rather than flag
89
+ # from a guess.
102
90
  return nil if known.nil?
103
91
  return nil if known.include?(sym)
104
92