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
@@ -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
 
@@ -6,18 +6,15 @@ module Rigor
6
6
  # The set of HTTP-status symbols `have_http_status` accepts.
7
7
  #
8
8
  # ADR-39 — the HTTP status symbols come from the **real**
9
- # `Rack::Utils::SYMBOL_TO_STATUS_CODE` (the authority `have_http_status`
10
- # itself resolves through `Rack::Utils.status_code`), not a vendored
11
- # snapshot: a snapshot that lagged the installed Rack would flag a
12
- # newly-added status symbol as a false `unknown-symbol` (a false
13
- # positive on working code). When Rack cannot be loaded the symbol
14
- # set is unknowable, so {known_symbols} returns `nil` and the caller
15
- # **declines** to flag anything — reduced coverage, never a guess.
9
+ # `Rack::Utils::SYMBOL_TO_STATUS_CODE` (the authority `have_http_status` itself resolves through
10
+ # `Rack::Utils.status_code`), not a vendored snapshot: a snapshot that lagged the installed Rack
11
+ # would flag a newly-added status symbol as a false `unknown-symbol` (a false positive on working
12
+ # code). When Rack cannot be loaded the symbol set is unknowable, so {known_symbols} returns `nil`
13
+ # and the caller **declines** to flag anything reduced coverage, never a guess.
16
14
  #
17
- # The Rails status-group aliases (`:success`, `:missing`, …) are a
18
- # small, stable convention set defined by Rails
19
- # (`ActionDispatch::TestResponse`) not a growing catalogue — so
20
- # they are kept as a constant rather than pulling in actionpack.
15
+ # The Rails status-group aliases (`:success`, `:missing`, …) are a small, stable convention set
16
+ # defined by Rails (`ActionDispatch::TestResponse`) not a growing catalogue — so they are kept as a
17
+ # constant rather than pulling in actionpack.
21
18
  module HttpStatusCodes
22
19
  RAILS_STATUS_GROUP_ALIASES = %i[
23
20
  informational success successful redirect
@@ -28,11 +25,10 @@ module Rigor
28
25
 
29
26
  module_function
30
27
 
31
- # The authoritative set of accepted `have_http_status` symbols
32
- # (real Rack status symbols ∪ the Rails group aliases), or `nil`
33
- # when it cannot be determined because Rack is unavailable. A
34
- # `nil` return means "decline" — the caller MUST NOT flag any
35
- # symbol as unknown (ADR-39: never guess from a stale copy).
28
+ # The authoritative set of accepted `have_http_status` symbols (real Rack status symbols ∪ the
29
+ # Rails group aliases), or `nil` when it cannot be determined because Rack is unavailable. A `nil`
30
+ # return means "decline" the caller MUST NOT flag any symbol as unknown (ADR-39: never guess from
31
+ # a stale copy).
36
32
  def known_symbols
37
33
  rack = rack_status_symbols
38
34
  return nil if rack.nil?
@@ -40,9 +36,8 @@ module Rigor
40
36
  rack | RAILS_STATUS_GROUP_ALIASES
41
37
  end
42
38
 
43
- # The real `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys, or `nil` when
44
- # Rack is not loadable. Lazy require (core Rigor carries no Rack
45
- # dependency; the plugin brings it); memoised for the process.
39
+ # The real `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys, or `nil` when Rack is not loadable. Lazy
40
+ # require (core Rigor carries no Rack dependency; the plugin brings it); memoised for the process.
46
41
  def rack_status_symbols
47
42
  return @rack_status_symbols if defined?(@rack_status_symbols)
48
43