rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "controller_scan"
6
+ require_relative "erb_compiler"
7
+ require_relative "view_assigns"
8
+ require_relative "view_units"
9
+
10
+ module Rigor
11
+ module Plugin
12
+ class Actionpack < Rigor::Plugin::Base
13
+ # #1047 — the locals a render site passes, indexed by the template they are passed TO.
14
+ #
15
+ # #393 seeded a template's `locals:` from the Rails 7.1 strict-locals comment only, so a partial
16
+ # without one read its own parameters as method calls on the view context. That absence has a
17
+ # measured cost rather than a theoretical one: redmine's `app/views/common/_other.html.erb` opens
18
+ # with the standard optional-local preamble
19
+ #
20
+ # <% path = nil unless defined? path %>
21
+ #
22
+ # and the compiled Ruby really does assign nil there, because nothing told the unit that `path` is
23
+ # bound — so the flow rules folded three live branches and `flow.` had to join `call.` in the
24
+ # plugin's default suppressed set for that reason alone
25
+ # (`docs/notes/20260917-erb-template-units.md` § 4). Tracing the render site is what lets the flow
26
+ # rules back into a view.
27
+ #
28
+ # ## Why over-binding is the safe direction and under-binding is not
29
+ #
30
+ # A name this index seeds that the render site did not really pass costs nothing: the local binds
31
+ # `Dynamic`, `defined?` reads it as bound, and no rule folds anything. A name it MISSES is the
32
+ # false positive above. So the union across render sites is deliberately generous — a name bound at
33
+ # only some of a partial's render sites is seeded anyway, typed `Dynamic`, never left absent — and
34
+ # every shape below that cannot be settled from literals contributes a NAME with no type rather
35
+ # than nothing.
36
+ #
37
+ # The one thing that is not generous is the TYPE. A type is a claim the engine acts on, so a name
38
+ # keeps a concrete type only when every site that passes it passes the same one; anything else is
39
+ # {ViewUnits::UNKNOWN_LOCAL}, which the engine binds `Dynamic[top]` (ADR-5).
40
+ #
41
+ # ## What a render site is read for
42
+ #
43
+ # Both sides of the render, because both bind the same thing:
44
+ #
45
+ # - a **controller** site, through {Analyzer.render_target_for} — `render partial: "card",
46
+ # locals: { user: @user }`;
47
+ # - a **template** site, in the compiled Ruby of every view — the same keyword form, plus the
48
+ # positional-hash form `render "card", user: @user`, which is locals in a view and OPTIONS in a
49
+ # controller (`ActionView::Helpers::RenderingHelper#render(options, locals)` against
50
+ # `ActionController::Base#render(options)`). Reading the controller form as locals would seed a
51
+ # `status` local for every `render :show, status: :ok` in the corpus.
52
+ #
53
+ # `collection:` and `object:` bind a local named after the partial, or after `as:` when given, and a
54
+ # collection additionally binds the `_counter` and `_iteration` companions Rails provides.
55
+ #
56
+ # ## The compiled-source cache
57
+ #
58
+ # Building the index compiles every template in the project, and `#template_units_for_file` is about
59
+ # to compile each of them again. So the builder keeps what it compiled, keyed by path and guarded by
60
+ # the template's own scrubbed bytes, and the hook reuses it — a full run does exactly the compile
61
+ # work it did before this feature, one pass instead of two. An editor buffer's bytes differ from the
62
+ # file's, so its template falls through to its own compile; the index itself is built once per
63
+ # plugin instance and revalidated once per collection pass against {Builder.fingerprint}, so an
64
+ # unsaved buffer's render sites are NOT read — its partials keep the locals the saved file passes
65
+ # until the save.
66
+ class RenderLocals
67
+ # What a name is seeded with when no site settled a type for it, or when two disagreed.
68
+ UNKNOWN = ViewUnits::UNKNOWN_LOCAL
69
+
70
+ # Finders and constructors whose Rails implementation raises rather than returning nil — the same
71
+ # closed set {ViewAssigns::NON_NIL_PRODUCERS} names, and for the same reason: a nil-free nominal
72
+ # standing in for a runtime nil licenses folds that report correct code.
73
+ NON_NIL_PRODUCERS = ViewAssigns::NON_NIL_PRODUCERS
74
+
75
+ # `{ "common/_other" => { "path" => UNKNOWN, … } }`, keyed by logical name with the format
76
+ # stripped — a partial's locals do not depend on which format rendered it.
77
+ attr_reader :by_template
78
+
79
+ # The {Builder.fingerprint} of the sources this index was built from, taken BEFORE they were read —
80
+ # so an edit that lands during the build reads as a change on the next pass rather than being
81
+ # recorded as already seen. nil for {.empty}, which therefore never matches a real fingerprint.
82
+ attr_reader :fingerprint
83
+
84
+ # Every method name a project helper `def`s — see {ViewUnits.self_declared_locals}.
85
+ attr_reader :helper_methods
86
+
87
+ def initialize(by_template, compiled = {}, fingerprint: nil, helper_methods: {})
88
+ @by_template = by_template.freeze
89
+ @compiled = compiled.freeze
90
+ @fingerprint = fingerprint
91
+ @helper_methods = helper_methods.freeze
92
+ freeze
93
+ end
94
+
95
+ def self.empty
96
+ new({})
97
+ end
98
+
99
+ # `common/_other.html` → the names every site that renders `common/_other` passed.
100
+ def seeds_for(logical_name)
101
+ @by_template.fetch(logical_name.to_s.sub(%r{\.[^./]+\z}, ""), {})
102
+ end
103
+
104
+ # The `[compiled, line_map, transform_id, parse_ok]` this index already computed for `path`, or nil
105
+ # when it read different bytes (an editor buffer) or never saw the file. The bytes are compared
106
+ # rather than trusted: a unit compiled from the file on disk must never stand in for one the editor
107
+ # is holding.
108
+ def compiled_for(path, text)
109
+ entry = @compiled[path.to_s]
110
+ return nil if entry.nil? || entry.first != text
111
+
112
+ entry.drop(1)
113
+ end
114
+
115
+ # Builds the index from the controller sources and the templates the plugin claims.
116
+ class Builder
117
+ def initialize(io_boundary:, controller_search_paths:, view_search_paths:, view_assigns:,
118
+ helper_search_paths: [])
119
+ @io_boundary = io_boundary
120
+ @controller_search_paths = controller_search_paths
121
+ @view_search_paths = view_search_paths
122
+ @helper_search_paths = helper_search_paths
123
+ @view_assigns = view_assigns
124
+ # `{ target => [ { name => type_name_or_nil }, … ] }` — one hash per render site, merged only
125
+ # once every site has been seen, because "bound at some sites" is a property of the whole set.
126
+ @sites = Hash.new { |hash, key| hash[key] = [] }
127
+ end
128
+
129
+ def build
130
+ fingerprint = self.class.fingerprint(
131
+ io_boundary: @io_boundary, controller_search_paths: @controller_search_paths,
132
+ view_search_paths: @view_search_paths, helper_search_paths: @helper_search_paths
133
+ )
134
+ harvest_controllers
135
+ compiled = harvest_templates
136
+ RenderLocals.new(@sites.transform_values { |sites| merge(sites) }, compiled,
137
+ fingerprint: fingerprint, helper_methods: harvest_helper_methods)
138
+ end
139
+
140
+ # What the index was built from, as one String: every controller, helper and template path with
141
+ # its size and mtime. A glob catches a file appearing or vanishing and a stat catches an edit;
142
+ # neither reads a byte, so revalidating costs one `stat` per file, once per collection pass.
143
+ #
144
+ # There is deliberately no content-digest fallback, unlike the collector's ADR-87 pack: a
145
+ # byte-identical `touch` moves the fingerprint and rebuilds this index while the collector still
146
+ # carries the units. That costs one rebuild and never a wrong answer — the rebuilt index is the
147
+ # index the carried units were seeded from — so a digest per file per pass is not worth buying.
148
+ def self.fingerprint(io_boundary:, controller_search_paths:, view_search_paths:, helper_search_paths: [])
149
+ files = source_files(io_boundary, controller_search_paths, "*.rb") +
150
+ source_files(io_boundary, helper_search_paths, "*.rb") +
151
+ source_files(io_boundary, view_search_paths, "*.erb")
152
+ files.map do |path|
153
+ stat = File.stat(path)
154
+ "#{path}\0#{stat.size}\0#{stat.mtime.to_r}"
155
+ rescue SystemCallError
156
+ "#{path}\0gone"
157
+ end.join("\n")
158
+ end
159
+
160
+ # Project-relative paths, the spelling `#template_units_for_file` is handed and the spelling the
161
+ # compiled cache is keyed by.
162
+ def self.source_files(io_boundary, roots, pattern)
163
+ root_prefix = "#{Dir.pwd}#{File::SEPARATOR}"
164
+ roots.flat_map do |root|
165
+ absolute = File.expand_path(root)
166
+ next [] unless io_boundary.directory?(absolute)
167
+
168
+ Dir.glob(File.join(absolute, "**", pattern)).map { |path| path.delete_prefix(root_prefix) }
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ # Two sites that agree on a name and its type keep the type; anything else — a disagreement, a
175
+ # site that settled no type, or a site that did not pass the name at all — is `Dynamic`.
176
+ def merge(sites)
177
+ names = sites.flat_map(&:keys).uniq
178
+ names.to_h do |name|
179
+ types = sites.map { |site| site.key?(name) ? site[name] : :absent }.uniq
180
+ [name, types.length == 1 && types.first.is_a?(String) ? types.first : UNKNOWN]
181
+ end
182
+ end
183
+
184
+ HELPER_DEF = /^[ \t]*def[ \t]+(?:self\.)?([a-z_][A-Za-z0-9_]*[?!]?)/
185
+ private_constant :HELPER_DEF
186
+
187
+ # `{ "current_user" => true }` for every `def` in a project helper. A line scan rather than a
188
+ # parse: the answer only has to be a superset of the names a helper can answer, and a name read
189
+ # out of a comment or a heredoc merely leaves one template's `defined?` test unseeded — which is
190
+ # what #393 shipped for every name.
191
+ def harvest_helper_methods
192
+ source_files(@helper_search_paths, "*.rb").each_with_object({}) do |path, names|
193
+ read(path)&.scan(HELPER_DEF)&.each { |(name)| names[name] = true }
194
+ end
195
+ end
196
+
197
+ def harvest_controllers
198
+ source_files(@controller_search_paths, "*.rb").each do |path|
199
+ tree = parse_file(path)
200
+ next if tree.nil?
201
+
202
+ ControllerScan.each_controller(tree, []) do |node, namespace|
203
+ harvest_controller(node, namespace)
204
+ end
205
+ end
206
+ end
207
+
208
+ def harvest_controller(node, namespace)
209
+ segments = namespace + ControllerScan.constant_segments(node.constant_path)
210
+ prefix = ControllerScan.controller_path(segments)
211
+ return if prefix.nil?
212
+
213
+ ControllerScan.method_bodies(node).each do |action, body|
214
+ ivars = @view_assigns.seeds_for("#{prefix}/#{action}")
215
+ ControllerScan.each_render(body) do |call|
216
+ record(controller_target(call, prefix), call, ivars, view_side: false)
217
+ end
218
+ end
219
+ end
220
+
221
+ # {Analyzer.render_target_for} already answers this for a controller: `render :edit`,
222
+ # `render "admin/shared/form"`, `render partial: "card"`. Its `[:template, name]` /
223
+ # `[:partial, name]` pair reduces to the name, because the locals of a template and of a partial
224
+ # are seeded the same way.
225
+ def controller_target(call, prefix)
226
+ Analyzer.render_target_for(call, prefix)&.last
227
+ end
228
+
229
+ # Every claimed template, compiled once. Returns the `{ path => [text, compiled, map, id, ok] }`
230
+ # cache `#template_units_for_file` reuses.
231
+ def harvest_templates
232
+ source_files(@view_search_paths, "*.erb").each_with_object({}) do |path, compiled|
233
+ text = read(path)
234
+ next if text.nil?
235
+
236
+ text = ErbCompiler.scrub(text)
237
+ source, line_map, transform = ErbCompiler.compile(text)
238
+ result = Prism.parse(source)
239
+ compiled[path] = [text, source, line_map, transform, result.errors.empty?]
240
+ next unless result.errors.empty?
241
+
242
+ harvest_template(path, result.value)
243
+ end
244
+ rescue ErbCompiler::Unmappable
245
+ {}
246
+ end
247
+
248
+ def harvest_template(path, tree)
249
+ name = ViewUnits.logical_name(path, @view_search_paths).sub(%r{\.[^./]+\z}, "")
250
+ directory = name.include?("/") ? File.dirname(name) : ""
251
+ ivars = @view_assigns.seeds_for(name)
252
+ ControllerScan.each_render(tree) do |call|
253
+ record(template_target(call, directory), call, ivars, view_side: true)
254
+ end
255
+ end
256
+
257
+ # The view-side reading of a render target, which differs from the controller's in exactly one
258
+ # place: a bare positional name is a PARTIAL here. `template:` is the escape hatch an author uses
259
+ # when they mean otherwise, and `layout:` in a view names a partial too — the same reading
260
+ # {Effects::CalleeRule.rails_render_partial} takes, for the same reason.
261
+ def template_target(call, directory)
262
+ explicit = string_option(call, "template")
263
+ return qualify(explicit, directory) if explicit
264
+
265
+ partial = string_option(call, "partial") || string_option(call, "layout")
266
+ return partialize(qualify(partial, directory)) if partial
267
+
268
+ positional = (call.arguments&.arguments || []).grep_v(Prism::KeywordHashNode).first
269
+ return nil unless positional.is_a?(Prism::StringNode) || positional.is_a?(Prism::SymbolNode)
270
+
271
+ partialize(qualify(positional.unescaped, directory))
272
+ end
273
+
274
+ def record(target, call, ivars, view_side:)
275
+ return if target.nil? || target.empty?
276
+
277
+ locals = locals_at(call, target, ivars, view_side: view_side)
278
+ @sites[target] << locals
279
+ end
280
+
281
+ # The names one render site binds, and the type of each where the site settled one.
282
+ def locals_at(call, target, ivars, view_side:)
283
+ arguments = call.arguments&.arguments || []
284
+ options = arguments.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
285
+ locals = {}
286
+ # `render "card", user: @user` — the view's second positional argument IS the locals hash, and
287
+ # the controller's is options. Read alongside an explicit `locals:` rather than instead of it,
288
+ # because over-binding is the safe direction.
289
+ collect_pairs(options, locals, ivars) if view_side && !arguments.first.is_a?(Prism::KeywordHashNode)
290
+ collect_pairs(option(call, "locals"), locals, ivars)
291
+ collect_object_locals(call, target, locals, ivars)
292
+ locals
293
+ end
294
+
295
+ # `locals: { user: @user }` and its hashrocket spelling. A key that is not a literal symbol or
296
+ # string — a computed key, a `**splat` — names nothing this can seed, and is skipped rather than
297
+ # guessed at.
298
+ def collect_pairs(hash, locals, ivars)
299
+ return unless hash.is_a?(Prism::HashNode) || hash.is_a?(Prism::KeywordHashNode)
300
+
301
+ hash.elements.each do |element|
302
+ next unless element.is_a?(Prism::AssocNode)
303
+
304
+ name = literal_name(element.key)
305
+ next if name.nil? || !name.match?(/\A[a-z_][A-Za-z0-9_]*\z/)
306
+
307
+ locals[name] = value_type(element.value, ivars)
308
+ end
309
+ end
310
+
311
+ # `object:` binds one local named after the partial; `collection:` binds the same name plus the
312
+ # `_counter` and `_iteration` companions Rails provides, and never a type — the local is the
313
+ # ELEMENT, and nothing at the site names the element's class.
314
+ def collect_object_locals(call, target, locals, ivars)
315
+ base = literal_name(option(call, "as")) || File.basename(target).delete_prefix("_")
316
+ return unless base.match?(/\A[a-z_][A-Za-z0-9_]*\z/)
317
+
318
+ object = option(call, "object")
319
+ collection = option(call, "collection")
320
+ locals[base] = value_type(object, ivars) if object
321
+ return if collection.nil?
322
+
323
+ locals[base] = nil
324
+ locals["#{base}_counter"] = nil
325
+ locals["#{base}_iteration"] = nil
326
+ end
327
+
328
+ # The two shapes a render site settles a type from, and nothing else: a non-nil-able finder or
329
+ # constructor on a constant (`User.find(params[:id])`), and an ivar the rendering unit's own
330
+ # seeds already typed. Anything else is a name with no type.
331
+ def value_type(node, ivars)
332
+ case node
333
+ when Prism::InstanceVariableReadNode then ivars[node.name.to_s]
334
+ when Prism::CallNode then produced_type(node)
335
+ end
336
+ end
337
+
338
+ def produced_type(call)
339
+ return nil unless NON_NIL_PRODUCERS.include?(call.name)
340
+
341
+ receiver = call.receiver
342
+ return nil unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
343
+
344
+ name = ControllerScan.constant_segments(receiver).join("::")
345
+ name.empty? ? nil : name
346
+ end
347
+
348
+ def option(call, key)
349
+ hash = call.arguments&.arguments&.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
350
+ pair = hash&.elements&.find do |element|
351
+ element.is_a?(Prism::AssocNode) && literal_name(element.key) == key
352
+ end
353
+ pair&.value
354
+ end
355
+
356
+ def string_option(call, key)
357
+ value = option(call, key)
358
+ value.is_a?(Prism::StringNode) || value.is_a?(Prism::SymbolNode) ? value.unescaped : nil
359
+ end
360
+
361
+ def literal_name(node)
362
+ node.unescaped if node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
363
+ end
364
+
365
+ def qualify(name, directory)
366
+ return nil if name.nil? || name.empty?
367
+ return name if name.include?("/") || directory.empty?
368
+
369
+ "#{directory}/#{name}"
370
+ end
371
+
372
+ def partialize(name)
373
+ return nil if name.nil?
374
+
375
+ directory, separator, base = name.rpartition("/")
376
+ return base.start_with?("_") ? name : "_#{name}" if separator.empty?
377
+
378
+ base.start_with?("_") ? name : "#{directory}/_#{base}"
379
+ end
380
+
381
+ def source_files(roots, pattern)
382
+ self.class.source_files(@io_boundary, roots, pattern)
383
+ end
384
+
385
+ def read(path)
386
+ @io_boundary.read_file(File.expand_path(path))
387
+ rescue Plugin::AccessDeniedError, Errno::ENOENT
388
+ nil
389
+ end
390
+
391
+ def parse_file(path)
392
+ contents = read(path)
393
+ return nil if contents.nil?
394
+
395
+ result = Prism.parse(contents)
396
+ result.errors.empty? ? result.value : nil
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end