rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  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/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_children"
6
+ require_relative "context"
7
+ require_relative "errors"
8
+ require_relative "shadow"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # ADR-116 WD5 (ADR-53 Theme B): one traversal owns the declaration-context rules that `ScopeIndexer`'s table
13
+ # walkers each copied — what `self`, the cref and `Module.nesting` become under `class`/`module`, `class <<`,
14
+ # a `K = Class.new { … }`-family write, a bare factory block, and an eval-family block. A discovery table
15
+ # becomes a collector that receives events and never tracks context itself; the {Context} it is handed is
16
+ # the walk's.
17
+ #
18
+ # Require `rigor/inference/declaration_walk`, not this file: the rules the walk applies are `ScopeIndexer`
19
+ # functions, and that entry point loads them.
20
+ #
21
+ # The arms, in the order the walk tries them:
22
+ #
23
+ # - `class` / `module` — {Collector#on_declaration}, then the body under {Context#declaration_body}. The
24
+ # header's constant path and superclass expression are not walked. A header that renders no name raises
25
+ # no event and follows the `unrendered_header` rule (below).
26
+ # - `class << expr` — the expression under the enclosing context, the body under
27
+ # {Context#singleton_class_body}.
28
+ # - a constant write (the four spellings that can name a class) — {Collector#on_constant_write}; then, when
29
+ # the rvalue is the meta-new idiom, ONLY the factory call's receiver and arguments, under the enclosing
30
+ # context, and its block body, rebound to the class the write names. Nothing else of the write is
31
+ # walked: not the target's parent (`P` in `P::K = …`), not the guard's read in `K = K || …`, not a
32
+ # `.freeze` tail's call, not the block's parameters, and the factory call itself raises no
33
+ # {Collector#on_call}. A write whose rvalue is not the idiom walks its children, as any node does.
34
+ # - `def` — {Collector#on_def}, then the children.
35
+ # - a call — {Collector#on_call}; then a bare factory block (`Class.new { … }`, `Module.new`, `Struct.new`,
36
+ # `Data.define`) walks its receiver and arguments under the enclosing context and its body with an
37
+ # unnamed `self`, and an eval-family block (`class_eval`, `module_eval`, `class_exec`, `module_exec`,
38
+ # `instance_eval`, `instance_exec`) its receiver and arguments under the enclosing context and its body
39
+ # with `self` rebound to the receiver. Neither walks the block's parameters. Any other call walks its
40
+ # children. A collector following the `:ordinary_call` variant of the `factory_block` rule (below) sees
41
+ # a bare factory block walked as any other call instead.
42
+ #
43
+ # Every other node walks its children under the context it was reached with, in `compact_child_nodes`
44
+ # order, so a collector sees its events in the order the legacy walker it replaces accumulated them.
45
+ #
46
+ # ## The collector protocol
47
+ #
48
+ # A collector includes {Collector} and overrides the events it needs. Each handler answers {DESCEND} or
49
+ # {DECLINE}: {DECLINE} takes the node's whole subtree away from THIS collector, while the other collectors
50
+ # of the same run continue into it. Any other answer descends. The walk dispatches only the events some
51
+ # collector of the run overrides.
52
+ #
53
+ # ## Variants
54
+ #
55
+ # Where the legacy walkers disagree on a context rule, a port keeps its walker's answer by naming a
56
+ # variant of the rule in its class's `VARIANTS` (ADR-116 WD5; {RULE_VARIANTS} lists them). Collectors on
57
+ # different variants of a rule still share a run: where the variants give a subtree different contexts
58
+ # (a bare factory block, a meta-new or eval-family body, a header that renders no name) the walk goes down
59
+ # once per variant in use, each collector only in its own, and once for all of them everywhere else. A
60
+ # variant is a legacy answer kept on purpose, and #1521 tracks converging each one.
61
+ #
62
+ # The rule walk stays separate (ADR-53 rejected folding rule collectors into indexing): this walk only
63
+ # builds discovery tables.
64
+ module DeclarationWalk
65
+ # A handler's answers: go on into the node, or leave its subtree to the run's other collectors.
66
+ DESCEND = :descend
67
+ DECLINE = :decline
68
+
69
+ # Each context rule a collector may follow a legacy variant of, with its variants. The first is the
70
+ # walk's own rule, which a collector follows unless its `VARIANTS` names another.
71
+ #
72
+ # - `factory_block` — a bare `Class.new { … }` / `Module.new` / `Struct.new` / `Data.define` block.
73
+ # `:unnamed_self` walks the factory's receiver and arguments, then the body with an unnamed `self`,
74
+ # and skips the block's parameters (`walk_class_cvars`' rule). `:ordinary_call` walks the call's
75
+ # children like any call's — receiver, arguments, the block's parameters and body — under the
76
+ # enclosing context, so a `self::` header in the body anchors on the enclosing self
77
+ # (`walk_class_superclasses`' rule; `class C; Class.new { class self::E < S; end }; end` files
78
+ # `C::E`, a class Ruby never creates; #1521 item 8).
79
+ # - `anonymous_class_path` — the file path an anonymous class's synthetic name carries; answered by
80
+ # {Context#anonymous_class_path}, which documents `:whole_file` and `:outside_class_bodies` (#1521
81
+ # item 11).
82
+ # - `unrendered_header` — a `class` / `module` whose header renders no name, which only a parse error
83
+ # produces (`class foo`, a `module` keyword followed by a `def`). `:children` walks every child, the
84
+ # header's parts included, under the enclosing context (`walk_class_superclasses`' rule).
85
+ # `:skip` walks nothing below it (the member-layout walkers'). `:body_with_lost_nesting` walks the body
86
+ # alone under {Context#lost_header_body} (`walk_def_nestings`'). #1521 item 3.
87
+ # - `lexical_prefix` — the prefix meta-new and eval-family splits resolve against, answered by
88
+ # {Context#lexical_prefix}: `:prefix` or `walk_def_nestings`' `:nesting_head` (#1521 item 1).
89
+ RULE_VARIANTS = {
90
+ factory_block: %i[unnamed_self ordinary_call].freeze,
91
+ anonymous_class_path: %i[whole_file outside_class_bodies].freeze,
92
+ unrendered_header: %i[children skip body_with_lost_nesting].freeze,
93
+ lexical_prefix: %i[prefix nesting_head].freeze
94
+ }.freeze
95
+
96
+ # The event handlers a collector may override; each answers {DESCEND} until overridden.
97
+ module Collector
98
+ EVENTS = %i[on_declaration on_def on_call on_constant_write].freeze
99
+
100
+ # The legacy variants of {RULE_VARIANTS} this collector follows, `rule => variant`. A collector
101
+ # overrides the constant, documenting each entry where it declares it.
102
+ VARIANTS = {}.freeze
103
+
104
+ # A `class` / `module` header. `body` is the context its body is walked under; it exists even when
105
+ # the declaration has no body.
106
+ def on_declaration(_node, _context, _body)
107
+ DESCEND
108
+ end
109
+
110
+ def on_def(_node, _context)
111
+ DESCEND
112
+ end
113
+
114
+ # Every call, the factory and eval-family calls included, before the walk enters its block.
115
+ def on_call(_node, _context)
116
+ DESCEND
117
+ end
118
+
119
+ # `K = …`, `P::K = …`, `K ||= …` and `P::K ||= …`, before the walk decides whether the rvalue opens a
120
+ # meta-new body.
121
+ def on_constant_write(_node, _context)
122
+ DESCEND
123
+ end
124
+
125
+ # The {EVENTS} `klass` overrides. Answered once per class: the table is a module ivar, which only the
126
+ # main Ractor may touch, so a pool worker on the Ractor backend recomputes it instead (the #1055
127
+ # pattern in `FactStore::Target.local`); the answer is the same either way.
128
+ def self.events_of(klass)
129
+ return overridden_events(klass) unless Ractor.main?
130
+
131
+ @events_by_class[klass] ||= overridden_events(klass)
132
+ end
133
+ @events_by_class = {}.compare_by_identity
134
+
135
+ def self.overridden_events(klass)
136
+ EVENTS.reject { |event| klass.instance_method(event).owner.equal?(self) }.freeze
137
+ end
138
+ private_class_method :overridden_events
139
+
140
+ # The variant of `rule` `klass` follows: its `VARIANTS` entry, or the walk's own rule.
141
+ def self.variant_of(klass, rule)
142
+ klass::VARIANTS.fetch(rule) { RULE_VARIANTS.fetch(rule).first }
143
+ end
144
+
145
+ # Raises {UnknownVariant} unless every `VARIANTS` entry of `klass` names a rule and one of its
146
+ # variants, so a misspelt variant is a {ContractError} rather than the walk's rule followed silently:
147
+ # an error row on the file in a file's own index, and an aborted run in the project pre-pass.
148
+ def self.check_variants!(klass)
149
+ klass::VARIANTS.each do |rule, variant|
150
+ next if RULE_VARIANTS.fetch(rule, EMPTY).include?(variant)
151
+
152
+ raise UnknownVariant, "#{klass}: no #{rule.inspect} variant #{variant.inspect} (ADR-116 WD5)"
153
+ end
154
+ end
155
+ EMPTY = [].freeze
156
+ private_constant :EMPTY
157
+ end
158
+
159
+ NO_COLLECTORS = [].freeze
160
+ private_constant :NO_COLLECTORS
161
+
162
+ module_function
163
+
164
+ # Walks `root` once for every collector, from `context` (a file's top level by default).
165
+ def run(root, collectors, context = Context.root)
166
+ Traversal.for(collectors).walk(root, collectors, context)
167
+ collectors
168
+ end
169
+
170
+ # One run's dispatch: which events any collector overrides, so an event none of them handles costs no
171
+ # call per node.
172
+ class Traversal
173
+ # The traversal for `collectors`. A single-collector run's depends on the collector's class alone, so
174
+ # the main Ractor builds it once per class (the #1055 pattern in `FactStore::Target.local`); every
175
+ # other run builds its own.
176
+ def self.for(collectors)
177
+ return new(collectors) unless collectors.size == 1 && Ractor.main?
178
+
179
+ @single_runs[collectors.first.class] ||= new(collectors)
180
+ end
181
+ @single_runs = {}.compare_by_identity
182
+
183
+ def initialize(collectors)
184
+ @declarations = handles?(collectors, :on_declaration)
185
+ @defs = handles?(collectors, :on_def)
186
+ @calls = handles?(collectors, :on_call)
187
+ @constant_writes = handles?(collectors, :on_constant_write)
188
+ collectors.each { |collector| Collector.check_variants!(collector.class) }
189
+ @ordinary_factories = collectors.any? { |collector| ordinary_factory?(collector) }
190
+ @unnamed_factories = !collectors.all? { |collector| ordinary_factory?(collector) }
191
+ @unrendered_variants = collectors.any? { |collector| variant(collector, :unrendered_header) != :children }
192
+ @nesting_heads = collectors.any? { |collector| variant(collector, :lexical_prefix) == :nesting_head }
193
+ # Only a multi-collector run keeps its collectors, which a cached single-collector traversal must
194
+ # not hold on to, and the subsets it builds from them as it first needs each ({#subset}).
195
+ if collectors.size > 1
196
+ @run = collectors
197
+ @subsets = {}
198
+ end
199
+ freeze
200
+ end
201
+
202
+ def walk(node, collectors, context) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
203
+ return unless node.is_a?(Prism::Node)
204
+
205
+ # The arms are disjoint node classes, so their order is only a cost: the commonest kind is tried first.
206
+ case node
207
+ when Prism::CallNode
208
+ collectors = descending(collectors) { |c| c.on_call(node, context) } if @calls
209
+ return if collectors.empty?
210
+ # Both block arms need a literal block, which most calls lack.
211
+ return if node.block.is_a?(Prism::BlockNode) &&
212
+ (walk_factory_call?(node, collectors, context) || walk_eval_call?(node, collectors, context))
213
+ when Prism::DefNode
214
+ collectors = descending(collectors) { |c| c.on_def(node, context) } if @defs
215
+ return if collectors.empty?
216
+ when Prism::ClassNode, Prism::ModuleNode
217
+ return if walk_declaration?(node, collectors, context)
218
+ when Prism::SingletonClassNode
219
+ return walk_singleton_class(node, collectors, context)
220
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
221
+ Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode
222
+ collectors = descending(collectors) { |c| c.on_constant_write(node, context) } if @constant_writes
223
+ return if collectors.empty? || walk_meta_new_write?(node, collectors, context)
224
+ end
225
+
226
+ node.rigor_each_child { |child| walk(child, collectors, context) }
227
+ end
228
+
229
+ private
230
+
231
+ def handles?(collectors, event)
232
+ collectors.any? { |collector| Collector.events_of(collector.class).include?(event) }
233
+ end
234
+
235
+ # A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
236
+ # each collector alone (a pair in which one declines), the run without each collector (a larger run in
237
+ # which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
238
+ # them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
239
+ def subset(kind, collector = nil)
240
+ table = (@subsets[kind] ||= {}.compare_by_identity)
241
+ key = collector || kind
242
+ table.fetch(key) { table[key] = yield.freeze }
243
+ end
244
+
245
+ def alone(collector)
246
+ subset(:alone, collector) { [collector] }
247
+ end
248
+
249
+ def variant_split(rule, &)
250
+ subset(rule) do
251
+ second, first = @run.partition(&)
252
+ [first.freeze, second.freeze]
253
+ end
254
+ end
255
+
256
+ def variant(collector, rule)
257
+ Collector.variant_of(collector.class, rule)
258
+ end
259
+
260
+ def ordinary_factory?(collector)
261
+ variant(collector, :factory_block) == :ordinary_call
262
+ end
263
+
264
+ # The collectors that let the walk into the node the block asks each of them about. Every collector is
265
+ # asked, in order, whatever the others answer.
266
+ #
267
+ # Asking allocates nothing, so a node where no collector declines costs no object. Where exactly one
268
+ # declines, the answer is a {#subset} memoised for the run (the one survivor of a pair, or the whole run
269
+ # without the decliner), built the first time and free after that. Two cases copy instead: a decline
270
+ # inside a group of three or more that is not the whole run (a variant fork's group, or what is left of
271
+ # a run of four or more after an earlier decline), and two or more declines at one node of a group of
272
+ # three or more. Each allocates one Array.
273
+ def descending(collectors, &)
274
+ case collectors.size
275
+ when 0 then collectors
276
+ when 1 then yield(collectors.first) == DECLINE ? NO_COLLECTORS : collectors
277
+ when 2 then descending_pair(collectors, yield(collectors.first), yield(collectors.last))
278
+ else descending_many(collectors, &)
279
+ end
280
+ end
281
+
282
+ # An index loop rather than `each_with_index`, which allocates a block frame object (a T_IMEMO) per
283
+ # call: this runs at every node a run of three or more dispatches an event for.
284
+ def descending_many(collectors)
285
+ declined = nil
286
+ kept = nil
287
+ index = 0
288
+ while index < collectors.size
289
+ collector = collectors[index]
290
+ if yield(collector) != DECLINE
291
+ kept&.push(collector)
292
+ elsif declined.nil? && kept.nil?
293
+ declined = index
294
+ else
295
+ kept ||= collectors.take(index).tap { |list| list.delete_at(declined) }
296
+ end
297
+ index += 1
298
+ end
299
+ kept || (declined.nil? ? collectors : without(collectors, declined))
300
+ end
301
+
302
+ def without(collectors, index)
303
+ declined = collectors[index]
304
+ if collectors.equal?(@run)
305
+ return subset(:without, declined) { @run.reject { |collector| collector.equal?(declined) } }
306
+ end
307
+
308
+ collectors.dup.tap { |list| list.delete_at(index) }
309
+ end
310
+
311
+ def descending_pair(collectors, first, last)
312
+ if first == DECLINE
313
+ last == DECLINE ? NO_COLLECTORS : alone(collectors.last)
314
+ else
315
+ last == DECLINE ? alone(collectors.first) : collectors
316
+ end
317
+ end
318
+
319
+ # False only for a header that renders no prefix while every collector here follows the walk's
320
+ # `unrendered_header` rule, which then walks the node's children like any node's.
321
+ def walk_declaration?(node, collectors, context)
322
+ body_context = context.declaration_body(node)
323
+ return walk_unrendered_header?(node, collectors, context) if body_context.nil?
324
+
325
+ collectors = descending(collectors) { |c| c.on_declaration(node, context, body_context) } if @declarations
326
+ body = node.body
327
+ walk(body, collectors, body_context) if body && !collectors.empty?
328
+ true
329
+ end
330
+
331
+ # The `unrendered_header` rule: `:children` collectors walk every child under the enclosing context,
332
+ # `:body_with_lost_nesting` ones the body alone under {Context#lost_header_body}, `:skip` ones nothing.
333
+ def walk_unrendered_header?(node, collectors, context)
334
+ return false unless @unrendered_variants
335
+
336
+ children = collectors.select { |collector| variant(collector, :unrendered_header) == :children }
337
+ node.rigor_each_child { |child| walk(child, children, context) } unless children.empty?
338
+ lost = collectors.select { |collector| variant(collector, :unrendered_header) == :body_with_lost_nesting }
339
+ walk(node.body, lost, context.lost_header_body) if node.body && !lost.empty?
340
+ true
341
+ end
342
+
343
+ def walk_singleton_class(node, collectors, context)
344
+ walk(node.expression, collectors, context)
345
+ body = node.body
346
+ walk(body, collectors, context.singleton_class_body) if body
347
+ end
348
+
349
+ def walk_meta_new_write?(node, collectors, context)
350
+ enclosing, body, body_self = context.meta_new_split(node)
351
+ return false if enclosing.nil?
352
+
353
+ enclosing.each { |part| walk(part, collectors, context) }
354
+ walk_rebound_body(node, body, collectors, context, body_self, :meta_new) if body
355
+ true
356
+ end
357
+
358
+ # A meta-new or eval-family body, with `self` rebound to `owner`. The `lexical_prefix` rule decides
359
+ # `owner`, so where the run mixes its variants the `:nesting_head` collectors take the owner their
360
+ # prefix gives, and the body is walked once per distinct owner — once in all where the two agree.
361
+ def walk_rebound_body(node, body, collectors, context, owner, kind)
362
+ return walk(body, collectors, rebound(context, owner, kind)) unless @nesting_heads
363
+
364
+ prefixes, heads = prefix_groups(collectors)
365
+ head_owner = heads.empty? ? owner : split(node, context, kind, :nesting_head)[2]
366
+ if head_owner == owner
367
+ walk(body, collectors, rebound(context, owner, kind))
368
+ else
369
+ walk(body, prefixes, rebound(context, owner, kind)) unless prefixes.empty?
370
+ walk(body, heads, rebound(context, head_owner, kind))
371
+ end
372
+ end
373
+
374
+ def rebound(context, owner, kind)
375
+ kind == :eval ? context.eval_body(owner) : context.meta_new_body(owner)
376
+ end
377
+
378
+ def split(node, context, kind, lexical_variant)
379
+ kind == :eval ? context.eval_split(node, lexical_variant) : context.meta_new_split(node, lexical_variant)
380
+ end
381
+
382
+ # `[prefix, nesting_head]` collectors of a run that mixes the `lexical_prefix` variants.
383
+ def prefix_groups(collectors)
384
+ if collectors.equal?(@run)
385
+ return variant_split(:lexical_prefix) { |collector| variant(collector, :lexical_prefix) == :nesting_head }
386
+ end
387
+
388
+ heads, prefixes = collectors.partition { |collector| variant(collector, :lexical_prefix) == :nesting_head }
389
+ [prefixes, heads]
390
+ end
391
+
392
+ # The `factory_block` rule, for a call with a literal block. False when every collector here follows
393
+ # `:ordinary_call`, so the call walks its children like any call. Otherwise the receiver and arguments
394
+ # are walked once, for every collector, since both variants walk them under the enclosing context;
395
+ # the variants part only at the block: the `:unnamed_self` collectors walk its body with an unnamed
396
+ # `self`, the `:ordinary_call` ones the whole block, parameters included, under the enclosing context.
397
+ # Each collector sees the same events in the same order as in a run of its own.
398
+ def walk_factory_call?(node, collectors, context)
399
+ return false unless @unnamed_factories && ScopeIndexer.meta_new_constant_rvalue?(node)
400
+
401
+ unnamed = unnamed_factory_group(collectors)
402
+ return false if unnamed.empty?
403
+
404
+ walk(node.receiver, collectors, context)
405
+ node.arguments&.arguments&.each { |argument| walk(argument, collectors, context) }
406
+ block = node.block
407
+ walk(block.body, unnamed, context.factory_body) if block.body
408
+ walk(block, ordinary_factory_group(collectors), context) if unnamed.size < collectors.size
409
+ true
410
+ end
411
+
412
+ def unnamed_factory_group(collectors)
413
+ return collectors unless @ordinary_factories
414
+ return factory_split.first if collectors.equal?(@run)
415
+
416
+ collectors.reject { |collector| ordinary_factory?(collector) }
417
+ end
418
+
419
+ def ordinary_factory_group(collectors)
420
+ return factory_split.last if collectors.equal?(@run)
421
+
422
+ collectors.select { |collector| ordinary_factory?(collector) }
423
+ end
424
+
425
+ # `[unnamed_self, ordinary_call]` collectors of the whole run.
426
+ def factory_split
427
+ variant_split(:factory_block) { |collector| ordinary_factory?(collector) }
428
+ end
429
+
430
+ def walk_eval_call?(node, collectors, context)
431
+ enclosing, body, eval_self = context.eval_split(node)
432
+ return false if enclosing.nil?
433
+
434
+ enclosing.each { |part| walk(part, collectors, context) }
435
+ walk_rebound_body(node, body, collectors, context, eval_self, :eval) if body
436
+ true
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ADR-116 WD5 — the entry point for {Rigor::Inference::DeclarationWalk}. The walk lives in
4
+ # `declaration_walk/traversal`, and the context rules it applies are `ScopeIndexer` functions, so this file loads
5
+ # the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
6
+ # keeps the two free of a require cycle.
7
+ require_relative "scope_indexer"
@@ -76,8 +76,9 @@ module Rigor
76
76
  # walk over the analyzer's own parse produced. This holds per NODE and needs nothing about files.
77
77
  # 2. AGREEMENT — where both tables answer for the same DEF (through different objects), they answer the
78
78
  # same chain. A bundle is only reused when the file's content SHA-256 matches
79
- # ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and `build_def_nestings` is a
80
- # pure function of that file's AST, so identical bytes yield an identical chain.
79
+ # ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and the def-nesting table
80
+ # (`ScopeIndexer.declaration_walk_tables`) is a pure function of that file's AST, so identical bytes
81
+ # yield an identical chain.
81
82
  #
82
83
  # What is NOT true — and was asserted here before it was measured — is that a file takes exactly ONE of
83
84
  # the two branches per run. It does so in the cross-file pre-pass only. An UNCHANGED file re-analysed as
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../source/node_children"
7
+ require_relative "return_barrier"
7
8
 
8
9
  module Rigor
9
10
  module Inference
@@ -13,15 +14,14 @@ module Rigor
13
14
  # The inferred type is the union of:
14
15
  #
15
16
  # - the body's last-statement type, and
16
- # - the type of every explicit `return value` reachable in the body. Nested `def` / lambda / block bodies
17
- # are return barriers — their `return`s do not bubble up to the enclosing method.
17
+ # - the type of every explicit `return value` that exits the method. A `return` inside an ordinary block does
18
+ # (control-flow-analysis.md § "Non-local exits"); one inside a {ReturnBarrier} — a nested `def`, a lambda,
19
+ # or a `define_method` body — does not. The barriers are the ones `StatementEvaluator`'s return sink stops
20
+ # at, so the proposal and `rigor check`'s view of callers agree (issue #1382).
18
21
  #
19
22
  # Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
20
23
  # def-line annotator) and the sig-generator share one source of truth.
21
24
  module DefReturnTyper
22
- RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
23
- private_constant :RETURN_BARRIER_NODES
24
-
25
25
  module_function
26
26
 
27
27
  def call(def_node, scope_index)
@@ -60,10 +60,14 @@ module Rigor
60
60
 
61
61
  def collect_return_types(node, scope_index, out)
62
62
  return unless node.is_a?(Prism::Node)
63
- return if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
63
+ return if ReturnBarrier.node?(node)
64
64
 
65
65
  type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
66
- node.rigor_each_child { |c| collect_return_types(c, scope_index, out) }
66
+ # A barrier call's receiver and arguments still run in the method; only its block is cut off.
67
+ barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
68
+ node.rigor_each_child do |c|
69
+ collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
70
+ end
67
71
  end
68
72
 
69
73
  def type_return_node(return_node, scope_index, out)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #963 — the lexical `self` of a `define_method(:name) { ... }` block.
8
+ #
9
+ # A block has no `self` of its own, so every block body enters with `Scope#entering_opaque_block`
10
+ # and the enclosing class body's `self_type` (`Singleton[C]`) rides along unchanged. For
11
+ # `define_method` that carrier is the wrong side of the class: `Module#define_method` turns the
12
+ # block into an INSTANCE method of the receiver, and Ruby runs its body with `self` bound to the
13
+ # receiving instance. A bare `text` inside the block therefore reaches `C`'s member / attr reader,
14
+ # while `Singleton[C]` answers only `C`'s class methods.
15
+ #
16
+ # The consequence #963 records is #618's veto going the wrong way: `ExpressionTyper#self_type_answers?`
17
+ # asked the singleton side, found nothing, and let a same-named top-level `def text` bind ahead of the
18
+ # reader — typing `text.upcase` inside `class Line < Struct.new(:text); define_method(:shout) { text.upcase }`
19
+ # as the top-level def's `nil`. Narrowing the block body's `self_type` to `Nominal[C]` makes the
20
+ # existing veto ask the instance side, which is the side Ruby dispatches on; nothing in the veto
21
+ # itself changes.
22
+ #
23
+ # The match is deliberately narrow — the receiver must be the class body's own `self`, and that `self`
24
+ # must be a known `Singleton[C]` on the instance side of the class. `Foo.define_method(...)` on some other
25
+ # receiver keeps the unmodelled-self answer, and so does a `class << ...` BODY, where `self` is the
26
+ # singleton class and the call defines a CLASS method: an instance reader is not in that MRO, MRI reaches
27
+ # the top-level `def` there, and Rigor must answer the same. The body is the exclusion, not the lexical
28
+ # singleton frame — a `def` reached from that body has the class object as its `self`, so the call defines
29
+ # an instance method and the narrowing applies. `Scope#singleton_class_body?` is what draws that line, and
30
+ # carrying it on the scope is what lets both block-entry paths apply it identically.
31
+ # A `module_function` module is the one shape where both answers are right at once: MRI makes the block
32
+ # BOTH an instance method, where the reader resolves, and a module function on the module object, where it
33
+ # does not. Narrowing silences the module-function half, which is the false-positive-averse direction
34
+ # AGENTS.md § Implementation Guidelines asks for.
35
+ module DefineMethodBlockSelf
36
+ module_function
37
+
38
+ # @return the block body's narrowed `self_type`, or `nil` when the call shape does not match.
39
+ def narrow_self_type_for(scope:, call_node:)
40
+ return nil if scope&.singleton_class_body?
41
+ return nil unless define_method_on_lexical_self?(call_node)
42
+
43
+ self_type = scope&.self_type
44
+ return nil unless self_type.is_a?(Type::Singleton)
45
+
46
+ class_name = self_type.class_name
47
+ return nil if class_name.nil?
48
+
49
+ instance_type_for(class_name, scope.environment)
50
+ end
51
+
52
+ def define_method_on_lexical_self?(call_node)
53
+ return false unless call_node.is_a?(Prism::CallNode)
54
+ return false unless call_node.name == :define_method
55
+
56
+ call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
57
+ end
58
+
59
+ def instance_type_for(class_name, environment)
60
+ environment&.nominal_for_name(class_name) || Type::Nominal.new(class_name)
61
+ end
62
+ end
63
+ end
64
+ end