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
@@ -4,6 +4,12 @@ require "prism"
4
4
 
5
5
  require_relative "../source/node_walker"
6
6
  require_relative "block_parameter_binder"
7
+ require_relative "element_read_widening"
8
+ require_relative "index_write_widening"
9
+ require_relative "mutation_widening"
10
+ require_relative "optimistic_origin"
11
+ require_relative "receiver_alias"
12
+ require_relative "unknown_store_widening"
7
13
 
8
14
  module Rigor
9
15
  module Inference
@@ -16,11 +22,21 @@ module Rigor
16
22
  # A write counts across every local-write form — plain `=` (`LocalVariableWriteNode`), the operator /
17
23
  # `||=` / `&&=` compound forms, and a multi-assign target (`x, y = …` → `LocalVariableTargetNode` under
18
24
  # a `MultiWriteNode`) — at ANY depth: a block is a closure, so a write inside a nested block binds the
19
- # same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals) and names
20
- # not bound in the outer scope are excluded; a write to either is not a captured rebind of an outer
21
- # variable.
22
- module CapturedLocals
23
- LOCAL_WRITE_NODES = [
25
+ # same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals), names
26
+ # not bound in the outer scope, a write a nested block's own parameter or block-local shadows, and a
27
+ # write inside a nested `def` or class body ({.outer_local?}) are excluded; a write to any of them is
28
+ # not a captured rebind of an outer variable.
29
+ #
30
+ # All three also ask for the instance variables the body rebinds (`ivars: true`). The per-element fold asks
31
+ # for every other binding that outlives an iteration as well (`non_locals: true`): the class variables and
32
+ # globals the body rebinds, and the instance variable behind an attribute setter it calls on `self`. Names
33
+ # keep their sigil, so a map over every kind never collides, and {.bound_type} / {.bind} reach each name
34
+ # through its own kind of binding.
35
+ #
36
+ # {.content_mutations} is the sibling set on the same terms: the captured outer locals the body mutates
37
+ # IN PLACE rather than rebinds, which the rebind set cannot see and the per-element fold needs as well.
38
+ module CapturedLocals # rubocop:disable Metrics/ModuleLength
39
+ LOCAL_WRITE_NODES = Set[
24
40
  Prism::LocalVariableWriteNode,
25
41
  Prism::LocalVariableOperatorWriteNode,
26
42
  Prism::LocalVariableOrWriteNode,
@@ -28,24 +44,386 @@ module Rigor
28
44
  Prism::LocalVariableTargetNode
29
45
  ].freeze
30
46
 
47
+ # The write forms of the bindings other than locals that outlive an iteration: instance variables, class
48
+ # variables and globals.
49
+ NON_LOCAL_WRITE_NODES = Set[
50
+ Prism::InstanceVariableWriteNode,
51
+ Prism::InstanceVariableOperatorWriteNode,
52
+ Prism::InstanceVariableOrWriteNode,
53
+ Prism::InstanceVariableAndWriteNode,
54
+ Prism::InstanceVariableTargetNode,
55
+ Prism::ClassVariableWriteNode,
56
+ Prism::ClassVariableOperatorWriteNode,
57
+ Prism::ClassVariableOrWriteNode,
58
+ Prism::ClassVariableAndWriteNode,
59
+ Prism::ClassVariableTargetNode,
60
+ Prism::GlobalVariableWriteNode,
61
+ Prism::GlobalVariableOperatorWriteNode,
62
+ Prism::GlobalVariableOrWriteNode,
63
+ Prism::GlobalVariableAndWriteNode,
64
+ Prism::GlobalVariableTargetNode
65
+ ].freeze
66
+
67
+ # The call forms that invoke an attribute setter: `self.w = v`, the compound `self.w += v` / `||=` / `&&=`,
68
+ # and a multi-assign target `self.w, x = …`.
69
+ SETTER_CALL_NODES = Set[
70
+ Prism::CallNode, Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
71
+ Prism::CallTargetNode
72
+ ].freeze
73
+ private_constant :SETTER_CALL_NODES
74
+
75
+ # An attribute writer's name: an identifier followed by `=`. `==`, `!=`, `<=`, `>=`, `===` and `[]=` are
76
+ # not setters.
77
+ SETTER_NAME = /\A[A-Za-z_]\w*=\z/
78
+ private_constant :SETTER_NAME
79
+
80
+ # The binding the per-element fold lays under a block's parameters: each name's type across iterations, and
81
+ # the optimistic nil-freeness mark an iteration's own rebind gave it, which {.bind} adds to the one the
82
+ # call site already holds — as `Scope#join` unions the marks of the scopes it joins. `repeated` is the
83
+ # `RepeatedOrWrites::Marks` of the index `||=` sites whose slot an earlier run may have filled, marked on the
84
+ # same scope: those of the per-element fold's `position`, or every site when no position is given.
85
+ Bindings = Data.define(:types, :marks, :repeated) do
86
+ def names = types.keys
87
+
88
+ def lay(scope, position: nil)
89
+ bound = types.reduce(scope) do |acc, (name, type)|
90
+ CapturedLocals.bind(acc, name, type, optimistic: marks[name])
91
+ end
92
+ bound.with_repeated_or_writes(repeated.at(position))
93
+ end
94
+ end
95
+
96
+ # The empty answers, shared: every block-bearing call's return pass asks both questions, and nearly every
97
+ # body answers nothing to either.
98
+ NO_NAMES = [].freeze
99
+ NO_SITES = {}.freeze
100
+ private_constant :NO_NAMES, :NO_SITES
101
+
31
102
  module_function
32
103
 
33
104
  # @param base_scope — the call-site scope the block closes over.
105
+ # @param ivars — also collect the instance variables the body rebinds. An ivar is not captured — the
106
+ # block shares the caller's `self` — but it outlives an iteration, and the call, exactly as a captured
107
+ # local does. It counts on the same terms as a local (every write form, any depth, bound in
108
+ # `base_scope`), except that one still on its class-wide binding does not: ADR-58's declaration seed is
109
+ # the union of every write in the class, this body's included, so nothing the body stores can move it,
110
+ # and rebinding it would only drop the declaration mark that keeps its nil from being diagnostic fuel.
111
+ # A nested block that rebinds `self` (`o.instance_eval`) writes another object's ivar, and a nested
112
+ # `def` runs only when called; both still count, because an `instance_eval` without a receiver, or a
113
+ # call to that `def` inside the body, does write this one.
114
+ # @param non_locals — the per-element fold's superset of `ivars`: the class variables and globals the body
115
+ # rebinds count too, on the same terms, and so does an attribute setter called on `self` (`self.w = v`)
116
+ # as a rebind of the instance variable it is named after, `@w`: the `attr_writer` / `attr_accessor`
117
+ # convention stores there, and no write node shows it. A hand-written setter storing elsewhere is not
118
+ # seen.
34
119
  # @return the captured names the body writes, each once, in first-write order.
35
- def writes(block_node, base_scope)
120
+ def writes(block_node, base_scope, ivars: false, non_locals: false)
121
+ body = block_node.body
122
+ return NO_NAMES if body.nil?
123
+
124
+ introduced = nil
125
+ names = nil
126
+ outliving = non_locals || ivars
127
+ Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
128
+ name =
129
+ if LOCAL_WRITE_NODES.include?(descendant.class)
130
+ captured_local_write(descendant, ancestors, base_scope) { introduced ||= introduced_locals(block_node) }
131
+ elsif outliving
132
+ rebindable_outliving_write(descendant, base_scope, non_locals)
133
+ end
134
+ (names ||= []) << name if name
135
+ end
136
+ names ? names.uniq : NO_NAMES
137
+ end
138
+
139
+ def rebindable_outliving_write(node, base_scope, non_locals)
140
+ name = outliving_write_name(node, non_locals)
141
+ name if name && rebindable_non_local?(base_scope, name)
142
+ end
143
+
144
+ # The outer local a local-write node rebinds, or nil when the call site does not bind it, the write resolves
145
+ # inside a nested block or `def` ({.outer_local?}), or the block introduces it (the block yields the
146
+ # introduced set, computed only when needed).
147
+ def captured_local_write(node, ancestors, base_scope)
148
+ name = node.name
149
+ return nil unless base_scope.locals.key?(name) && outer_local?(node, ancestors)
150
+
151
+ yield.include?(name) ? nil : name
152
+ end
153
+
154
+ # The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only.
155
+ def outliving_write_name(node, non_locals)
156
+ name = non_local_write_name(node)
157
+ non_locals || (name && ivar_name?(name) && NON_LOCAL_WRITE_NODES.include?(node.class)) ? name : nil
158
+ end
159
+
160
+ # The instance, class or global variable `node` rebinds, or nil when it rebinds none of them.
161
+ def non_local_write_name(node)
162
+ return node.name if NON_LOCAL_WRITE_NODES.include?(node.class)
163
+
164
+ setter_ivar_name(node)
165
+ end
166
+
167
+ # `@w` for an attribute setter called on `self` (`self.w = …` and its compound and target forms), else nil.
168
+ def setter_ivar_name(node)
169
+ return nil unless SETTER_CALL_NODES.include?(node.class) && node.receiver.is_a?(Prism::SelfNode)
170
+
171
+ setter = node.respond_to?(:write_name) ? node.write_name : node.name
172
+ return nil unless SETTER_NAME.match?(setter)
173
+
174
+ :"@#{setter.to_s.delete_suffix('=')}"
175
+ end
176
+
177
+ def rebindable_non_local?(scope, name)
178
+ variable_kind(name) == :ivar ? rebindable_ivar?(scope, name) : !bound_type(scope, name).nil?
179
+ end
180
+
181
+ def rebindable_ivar?(scope, name)
182
+ !scope.ivar(name).nil? && !scope.declaration_sourced?(:ivar, name)
183
+ end
184
+
185
+ # The binding `scope` holds for a name from {.writes}.
186
+ def bound_type(scope, name)
187
+ case variable_kind(name)
188
+ when :ivar then scope.ivar(name)
189
+ when :cvar then scope.cvar(name)
190
+ when :global then scope.global(name)
191
+ else scope.local(name)
192
+ end
193
+ end
194
+
195
+ # `scope` with a name from {.writes} bound to `type`, keeping the name's optimistic nil-freeness mark
196
+ # (issue #286). `Scope#with_local` / `#with_ivar` drop it as a fresh write should, but here `type`
197
+ # stands for the binding across iterations, and a value that was nil-free only optimistically still is:
198
+ # without the mark `x.nil?` folds to `false` where the runtime answers `true`. `optimistic` is a mark the
199
+ # binding carries besides the one `scope` already holds — one an iteration's own rebind made. Class
200
+ # variables and globals carry no mark. {OptimisticOrigin.carry_local_mark} owns the re-marking, and with
201
+ # it the miss answer the mark records (issue #1302).
202
+ def bind(scope, name, type, optimistic: nil)
203
+ case variable_kind(name)
204
+ when :ivar then OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
205
+ when :cvar then scope.with_cvar(name, type)
206
+ when :global then scope.with_global(name, type)
207
+ else OptimisticOrigin.carry_local_mark(scope, scope.with_local(name, type), name, optimistic)
208
+ end
209
+ end
210
+
211
+ # The optimistic nil-freeness mark `scope` holds for a name from {.writes}, or nil.
212
+ def optimistic_mark(scope, name)
213
+ case variable_kind(name)
214
+ when :ivar then scope.optimistic_ivar(name)
215
+ when :local then scope.optimistic_local(name)
216
+ end
217
+ end
218
+
219
+ def ivar_name?(name) = variable_kind(name) == :ivar
220
+
221
+ # Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
222
+ # `@`, and a local with none of them.
223
+ def variable_kind(name)
224
+ if name.start_with?("$") then :global
225
+ elsif name.start_with?("@@") then :cvar
226
+ elsif name.start_with?("@") then :ivar
227
+ else :local
228
+ end
229
+ end
230
+
231
+ # The nodes that change a receiver's CONTENT without rebinding it: a call to a name the straight-line
232
+ # widening responds to ({MutationWidening::SHAPE_MUTATORS}), and the index writes that store through
233
+ # `[]=` without being a `[]=` call ({IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES}, the one list the
234
+ # block-return threading gate and the captured-local write-back read too).
235
+ INDEX_STORE_NODES = IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES
236
+ private_constant :INDEX_STORE_NODES
237
+
238
+ # The captured outer locals the body mutates in place, each mapped to its mutation sites (the nodes
239
+ # above) in source order. A site counts through every variable its receiver can evaluate to
240
+ # ({ReceiverAlias.candidates}), at any nesting depth as long as a local's read does not resolve inside a nested
241
+ # block ({.outer_local?}), and a local is excluded on exactly the terms {.writes} excludes it.
242
+ #
243
+ # Under `non_locals: true` the instance variables, class variables and globals the body mutates in place
244
+ # count too, on the terms {.writes} takes a rebound one ({.rebindable_non_local?}). Since the block-return
245
+ # threading gate threads an index write, `@cache[:first] ||= e; @cache[:first] == 2` would otherwise type
246
+ # every position from the empty entry hash, store THAT position's `e`, and fold `find` to `2` where Ruby,
247
+ # keeping the first iteration's `1`, answers `nil`; `$seen << x; n == 1` after `n = $seen.size` folded
248
+ # `find` to `nil` the same way. A class variable or global counts only as the receiver itself (parentheses
249
+ # aside), not through a branch that selects it.
250
+ #
251
+ # A local also counts through the two routes straight-line code widens it by without reading it as the
252
+ # receiver. A mutator on an element read rooted at it (`a[0] << e`, the local {ElementReadWidening}
253
+ # widens) is a site of that local. A self-call whose callee content-mutates the parameter a local is passed
254
+ # to (`add_to(a, e)`, the local `StatementEvaluator#content_mutated_arguments` reports) is a
255
+ # {UnknownStoreWidening::CalleeStore} site of it. Both were invisible here, so every position of the fold
256
+ # read the local at its entry contents: `a = [[1]]; [1, 2].map { |e| v = a[0].size; a[0] << e; v }`
257
+ # folded to `[1, 1]` where Ruby answers `[1, 2]`.
258
+ #
259
+ # @param base_scope — the call-site scope the block closes over.
260
+ # @param non_locals — also collect the instance variables, class variables and globals the body mutates
261
+ # in place.
262
+ # @return `{ name => [site, ...] }`, empty for the overwhelmingly common body that mutates nothing
263
+ # captured.
264
+ def content_mutations(block_node, base_scope, non_locals: false)
36
265
  body = block_node.body
37
- return [] if body.nil?
266
+ return NO_SITES if body.nil?
267
+
268
+ introduced = nil
269
+ body_mutation_sites(body, base_scope, non_locals) { introduced ||= introduced_locals(block_node) }
270
+ end
271
+
272
+ # Issue #1412 — false when `node` certainly holds nothing {.writes} (with `ivars: true`) or
273
+ # {.content_mutations} could collect against `base_scope`: no write to a local it binds, no instance, class or
274
+ # global variable write, no mutator or index store, and no self-call passing a local. It over-approximates
275
+ # (a write a block parameter shadows still answers true) and allocates nothing, so a caller can skip both
276
+ # scans, and whatever it would compute only to use their answers, for the common body that touches no capture.
277
+ def may_touch_capture?(node, base_scope)
278
+ return true if capture_touch?(node, base_scope)
279
+
280
+ node.rigor_each_child { |child| return true if may_touch_capture?(child, base_scope) }
281
+ false
282
+ end
283
+
284
+ def capture_touch?(node, base_scope)
285
+ klass = node.class
286
+ return base_scope.locals.key?(node.name) if LOCAL_WRITE_NODES.include?(klass)
287
+ return true if NON_LOCAL_WRITE_NODES.include?(klass) || INDEX_STORE_NODES.include?(klass)
288
+ return false unless klass == Prism::CallNode
289
+
290
+ receiver = node.receiver
291
+ return true if receiver && MutationWidening::SHAPE_MUTATORS.include?(node.name)
292
+
293
+ callee_call?(node)
294
+ end
295
+
296
+ # Issue #1412 — {.content_mutations} for a `while` / `until` / `for` body: the locals bound in `base_scope`
297
+ # (the scope the loop body first enters from) that the body mutates in place. A loop body opens no scope and
298
+ # introduces no name, so every such local counts on the same depth terms a block's capture does — a read
299
+ # that resolves inside a nested block or lambda, or inside a nested `def` or class body, is not the loop's.
300
+ #
301
+ # @return `{ name => [site, ...] }`, empty for the common body that mutates nothing.
302
+ def loop_content_mutations(statements, base_scope)
303
+ return NO_SITES if statements.nil?
304
+
305
+ body_mutation_sites(statements, base_scope, false) { NO_INTRODUCED }
306
+ end
307
+
308
+ NO_INTRODUCED = Set.new.freeze
309
+ private_constant :NO_INTRODUCED
310
+
311
+ # The mutation sites under `body` whose reads name a variable {.captured_target?} accepts, keyed by name.
312
+ # The block yields the names the enclosing construct introduces, asked only when a local site is found.
313
+ def body_mutation_sites(body, base_scope, non_locals, &)
314
+ sites = nil
315
+ evaluator = nil
316
+ Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
317
+ # A callee is resolved in the call-site scope, as the straight-line callee floor resolves it.
318
+ site = mutation_site(descendant) { evaluator ||= StatementEvaluator.new(scope: base_scope) }
319
+ next if site.nil?
320
+
321
+ site_reads(site).each do |read|
322
+ next unless captured_target?(read, ancestors, base_scope, non_locals, &)
323
+
324
+ ((sites ||= {})[read.name] ||= []) << site
325
+ end
326
+ end
327
+ sites || NO_SITES
328
+ end
329
+
330
+ # The mutation site `node` is — the node itself, or a {UnknownStoreWidening::CalleeStore} for a self-call
331
+ # whose callee content-mutates a local it is passed — or nil. A self-call is resolved by the
332
+ # `StatementEvaluator` the block yields, which the caller builds only when one is needed. A mutator name
333
+ # called on `self` (`self.store(h, k)`, `self.push(a, x)`) names no variable as its receiver, so it is asked
334
+ # as a self-call, as the straight-line callee floor asks it.
335
+ def mutation_site(node)
336
+ receiver = mutated_receiver(node)
337
+ return node if receiver && !receiver.is_a?(Prism::SelfNode)
338
+ return nil unless callee_call?(node)
339
+
340
+ arguments = yield.content_mutated_arguments(node)
341
+ arguments.empty? ? nil : UnknownStoreWidening::CalleeStore.new(node, arguments)
342
+ end
343
+
344
+ # The variable reads a mutation site reaches: a callee store's arguments, the local an element read is
345
+ # rooted at, or else {ReceiverAlias.mutated_reads} of the receiver.
346
+ def site_reads(site)
347
+ return site.arguments if site.is_a?(UnknownStoreWidening::CalleeStore)
348
+
349
+ receiver = mutated_receiver(site)
350
+ path = ElementReadWidening.element_read_path(receiver)
351
+ path ? [path.first] : ReceiverAlias.mutated_reads(receiver)
352
+ end
353
+
354
+ # A call to a method on `self` (implicit or explicit) that passes a local as an argument — the only kind
355
+ # the callee floor can report a site for, so the only kind worth resolving.
356
+ def callee_call?(node)
357
+ return false unless node.is_a?(Prism::CallNode)
358
+ return false unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
359
+
360
+ arguments = node.arguments&.arguments
361
+ !arguments.nil? && arguments.any?(Prism::LocalVariableReadNode)
362
+ end
363
+
364
+ # True when `read` names a variable {.content_mutations} collects: a {.content_target?} which, when it is a
365
+ # local, does not resolve inside a nested block and is not one the block introduces (the block yields that set,
366
+ # computed only when needed). An `it` read is never one: it is the parameter of the innermost block around
367
+ # it, so the body's own `it` is introduced and a nested block's `it` is that block's.
368
+ def captured_target?(read, ancestors, base_scope, non_locals)
369
+ return false if read.is_a?(Prism::ItLocalVariableReadNode)
370
+ return false unless content_target?(read, base_scope, non_locals)
371
+ return true unless read.is_a?(Prism::LocalVariableReadNode)
372
+
373
+ outer_local?(read, ancestors) && !yield.include?(read.name)
374
+ end
375
+
376
+ # A local bound at the call site (the block's own names are excluded by the caller), or — under
377
+ # `non_locals:` — an instance variable, class variable or global {.rebindable_non_local?} accepts.
378
+ def content_target?(read, base_scope, non_locals)
379
+ return base_scope.locals.key?(read.name) if read.is_a?(Prism::LocalVariableReadNode)
380
+
381
+ non_locals && rebindable_non_local?(base_scope, read.name)
382
+ end
383
+
384
+ # False when a local read or write resolves inside a scope nested between the body and `node`. Prism resolves a
385
+ # name in a nested block's own scope first, so `[[9]].each { |a| a << x }` mutates, and `[[9]].each { |a| a =
386
+ # x }` rebinds, that block's parameter, not the outer local that happens to share its name: its `depth` stops
387
+ # short of the body's own scope. A `def`, `class`, `module` or `class << self` body opens a scope of its own
388
+ # that sees no outer local at all, so `def helper; z = 5; end` writes the method's `z` — but only its body,
389
+ # and a `def`'s parameters, are that scope: a `def (o = x).m` receiver, a `class Foo < (s = x)` superclass or
390
+ # a `class << (t = x)` target runs in the enclosing one and is left to the depth test. {.writes} and
391
+ # {.content_mutations} both ask it, so the two sets cannot disagree about which `a` a site names. A name
392
+ # resolving in the body's own scope is left to the block-introduced exclusion: Prism puts an outer local there
393
+ # only when the parse never saw it declared, which a synthetic call-site scope can still bind.
394
+ def outer_local?(node, ancestors)
395
+ nesting = 0
396
+ ancestors.each_with_index do |ancestor, index|
397
+ return false if hard_scope_entered?(ancestor, ancestors[index + 1] || node)
398
+
399
+ nesting += 1 if NESTED_SCOPE_NODES.include?(ancestor.class)
400
+ end
401
+ node.depth >= nesting
402
+ end
403
+
404
+ # True when `child`, the next node on the path, is inside the scope `ancestor` opens.
405
+ def hard_scope_entered?(ancestor, child)
406
+ case ancestor
407
+ when Prism::DefNode then child.equal?(ancestor.body) || child.equal?(ancestor.parameters)
408
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then child.equal?(ancestor.body)
409
+ else false
410
+ end
411
+ end
38
412
 
39
- introduced = introduced_locals(block_node)
40
- outer_writes = []
41
- Source::NodeWalker.each(body) do |descendant|
42
- next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
43
- next if introduced.include?(descendant.name)
44
- next unless base_scope.locals.key?(descendant.name)
413
+ NESTED_SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
414
+ private_constant :NESTED_SCOPE_NODES
45
415
 
46
- outer_writes << descendant.name
416
+ # The index arm spells {INDEX_STORE_NODES} out rather than splatting it: `when *ARRAY` copies the array
417
+ # on every evaluation, and this runs once per node of every scanned block body, which cost `rigor check`
418
+ # about 126K allocations over the v0.3.9 `lib` ([#1502](https://github.com/rigortype/rigor/issues/1502),
419
+ # the shape `ScopeIndexer::META_CONSTANT_WRITE_NODES` records). Keep the arm and the list in step.
420
+ def mutated_receiver(node)
421
+ case node
422
+ when Prism::CallNode then node.receiver if MutationWidening::SHAPE_MUTATORS.include?(node.name)
423
+ when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode,
424
+ Prism::IndexTargetNode
425
+ node.receiver
47
426
  end
48
- outer_writes.uniq
49
427
  end
50
428
 
51
429
  # Names the block itself introduces: parameters (numbered parameters included, via