rigortype 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The calls that keep their literal block to run later instead of running it before they return: the Kernel
8
+ # functions that wrap it in a Proc (`lambda`, `proc`), the definers that install it as a method body
9
+ # (`define_method`, `define_singleton_method`, on any receiver), and the constructors that keep it as a proc,
10
+ # a thread or fiber body, an enumerator's generator or a hash's default proc.
11
+ #
12
+ # A `break` in such a block never becomes the call's value — it returns from the lambda or the defined method
13
+ # when that is called, or raises `LocalJumpError` — so the #853 break-arm union
14
+ # (`ExpressionTyper#call_break_arm_types`) MUST NOT read it. Unioning the arm typed
15
+ # `lambda do |t| break if flag; t end` as `Proc | nil`, and the lambda's next `.call` drew
16
+ # `call.possible-nil-receiver`.
17
+ #
18
+ # A project redefinition of one of these names is not excluded: dropping the arm there loses a `break`
19
+ # value, never invents one.
20
+ module StoredBlockCall
21
+ KERNEL_WRAPPERS = %i[lambda proc].freeze
22
+ DEFINERS = %i[define_method define_singleton_method].freeze
23
+ CONSTRUCTORS = {
24
+ Proc: %i[new], Thread: %i[new start fork], Fiber: %i[new], Enumerator: %i[new], Hash: %i[new]
25
+ }.transform_values(&:freeze).freeze
26
+ NO_NAMES = [].freeze
27
+ private_constant :KERNEL_WRAPPERS, :DEFINERS, :CONSTRUCTORS, :NO_NAMES
28
+
29
+ module_function
30
+
31
+ def stores_block?(call_node)
32
+ name = call_node.name
33
+ return kernel_spelled?(call_node.receiver) if KERNEL_WRAPPERS.include?(name)
34
+ return true if DEFINERS.include?(name)
35
+
36
+ constant = root_constant_name(call_node.receiver)
37
+ !constant.nil? && CONSTRUCTORS.fetch(constant, NO_NAMES).include?(name)
38
+ end
39
+
40
+ # Implicit self, `self.`, or `Kernel.` / `::Kernel.` — the spellings that reach Kernel's function.
41
+ def kernel_spelled?(receiver)
42
+ receiver.nil? || receiver.is_a?(Prism::SelfNode) || root_constant_name(receiver) == :Kernel
43
+ end
44
+
45
+ # `Foo` or `::Foo`'s name, else nil.
46
+ def root_constant_name(receiver)
47
+ case receiver
48
+ when Prism::ConstantReadNode then receiver.name
49
+ when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -17,21 +17,39 @@ module Rigor
17
17
  module StringMutation
18
18
  # Receiver-mutating String methods only. The non-bang siblings (`upcase` vs `upcase!`, `sub` vs `sub!`)
19
19
  # return a new String and leave the receiver's value intact, so they stay precise.
20
+ #
21
+ # This is the one String table: `MutationWidening::SHAPE_MUTATORS` unions it with the Array and Hash ones, and
22
+ # the effect classifier and the effect catalogue's `mutators: string` cite it rather than keep a list of their
23
+ # own (ADR-103 WD3). A spec holds it to every bang method `String` defines plus the non-bang mutators reflection
24
+ # cannot find by name.
20
25
  MUTATORS = %i[
21
26
  << concat insert prepend replace clear []= slice!
27
+ setbyte bytesplice append_as_bytes force_encoding
22
28
  sub! gsub! tr! tr_s! delete! squeeze! succ! next!
23
29
  upcase! downcase! capitalize! swapcase! reverse!
24
- strip! lstrip! rstrip! chomp! chop! force_encoding
30
+ strip! lstrip! rstrip! chomp! chop!
31
+ delete_prefix! delete_suffix! encode! scrub! unicode_normalize!
25
32
  ].to_set.freeze
26
33
 
27
- # The {MUTATORS} that can leave the receiver EMPTY, and so retract a `non-empty-string` refinement. The
28
- # rest are appenders or same-length rewrites: `s << x` / `s.prepend(x)` / `s.upcase!` cannot empty a
29
- # string that already holds a character, so the refinement is still provable afterwards.
30
- EMPTYING_MUTATORS = %i[
31
- replace clear slice! []=
32
- sub! gsub! delete! squeeze! strip! lstrip! rstrip! chomp! chop!
34
+ # The {MUTATORS} that CANNOT leave a non-empty receiver empty, and so keep a `non-empty-string` refinement:
35
+ # appenders (`<<`, `concat`, `insert`, `prepend`, `append_as_bytes`), and rewrites that keep at least one
36
+ # character (`setbyte`, `force_encoding`, the case mappings, `reverse!`, `succ!` / `next!`, `squeeze!`,
37
+ # `unicode_normalize!`). A whitelist, as `RefinementMutation::EMPTY_PRESERVING` is for Array and Hash: a mutator
38
+ # missing here retracts the witness, which costs precision, where a mutator missing from a list of emptiers kept
39
+ # the witness of a string it had just emptied — `tr!` and `tr_s!` did, since `"a".tr!("a", "")` is `""`.
40
+ EMPTY_PRESERVING = %i[
41
+ << concat insert prepend append_as_bytes
42
+ setbyte force_encoding
43
+ upcase! downcase! capitalize! swapcase! reverse! succ! next! squeeze! unicode_normalize!
33
44
  ].to_set.freeze
34
45
 
46
+ # `tr!` / `tr_s!` map every character they match to one character of the replacement (padded with its last), so
47
+ # they empty the buffer only through an EMPTY replacement: `"a".tr!("a", "")` is `""`, `"a".tr!("a", "_")` is
48
+ # `"_"`. They keep the witness when their second argument is provably a non-empty String.
49
+ TRANSLATORS = %i[tr! tr_s!].to_set.freeze
50
+
51
+ NO_ARG_TYPES = [].freeze
52
+
35
53
  module_function
36
54
 
37
55
  # `Constant["ab"]` under a String mutator → the bare `String` nominal. The pinned value is what the
@@ -55,6 +73,25 @@ module Rigor
55
73
  def constant?(type)
56
74
  type.is_a?(Type::Constant) && type.value.is_a?(String)
57
75
  end
76
+
77
+ # Whether `method_name`, called with `arg_types` (the call's positional argument types, possibly none), can leave
78
+ # a non-empty receiver empty, and so retract a `non-empty-string` refinement. Every mutator outside
79
+ # {EMPTY_PRESERVING} can, except a {TRANSLATORS} call whose replacement is provably non-empty.
80
+ def may_empty?(method_name, arg_types = NO_ARG_TYPES)
81
+ return false if EMPTY_PRESERVING.include?(method_name)
82
+ return true unless TRANSLATORS.include?(method_name)
83
+
84
+ !non_empty_string?(arg_types[1])
85
+ end
86
+
87
+ def non_empty_string?(type)
88
+ case type
89
+ when Type::Constant then constant?(type) && !type.value.empty?
90
+ when Type::Difference then type.removes_empty_witness? && type.base.class_name == "String"
91
+ when Type::Union then type.members.all? { |member| non_empty_string?(member) }
92
+ else false
93
+ end
94
+ end
58
95
  end
59
96
  end
60
97
  end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "element_read_widening"
7
+ require_relative "mutation_widening"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # What a binding can hold after each of a set of in-place mutation sites — the nodes
12
+ # {CapturedLocals.content_mutations} collects — has run any number of times, storing values nothing typed.
13
+ #
14
+ # Each site takes its own straight-line widening ({MutationWidening.widen_for_mutator}), fed one
15
+ # `Dynamic[top]` per argument, and every site that can STORE a value then gives each type argument of the
16
+ # widened `Array` / `Hash` a `Dynamic[top]` arm: `{ a: 0 }` under `h[k] += 1` reads
17
+ # `Hash[Dynamic[top] | Symbol, Dynamic[top] | Integer]`. The second step is not redundant. The widening
18
+ # joins argument evidence only for the plain adders (`<<`, `push`, `[]=`, `store`, …); a site whose stored
19
+ # values its arguments do not describe — `map!`, a block-form `fill`, `transform_values!`, `merge!`,
20
+ # `flatten!` — comes back with the seed's element types alone, which is a wrong precise answer once the
21
+ # block has run: `a = [1]` under `a.map!(&:to_s)` would read `Array[Integer]`. Only a site that removes or
22
+ # reorders ({VALUE_PRESERVING}) keeps its content as the widening left it, since it cannot put a value
23
+ # there that was not there before — unless it is the site that CLOSES a literal carrier. A `Tuple` or
24
+ # `HashShape` knows which slots and keys exist, and the nominal it widens to cannot say that any may be
25
+ # missing, so it takes the gradual arm too: `{ a: 0 }` under a lone `h.delete(:a)` read `Hash[Symbol, 0]`,
26
+ # whose `h[:b]` answered `0` where Ruby answers `nil`, and `s = [0]` under `p = s.pop; s.push(x)` closed to
27
+ # `Array[0]`, a precise nominal the `push` after it then declined, so `p` stayed `0?`. With the arm the
28
+ # answer for a `Tuple` / `HashShape` seed (alone or in a `Union`) no longer depends on the order the sites are
29
+ # written in; a refinement seed (`non-empty-array[0]`) still can, since its remover does not close a literal.
30
+ #
31
+ # A site whose widening declines leaves the binding as it is, exactly as the straight-line seam does: a
32
+ # precise nominal (a declared or inferred `Array[String]` is a claim this seam may not grow), a receiver
33
+ # that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a method
34
+ # its class does not define). The caller MUST therefore not read an unchanged binding as describing later
35
+ # iterations. An empty-witness refinement under a storing mutator that keeps the witness is not such a decline:
36
+ # the straight-line widening grows it (issue #936), and gives a site whose arguments describe nothing it stores
37
+ # (`non-empty-array[String]` under `map!`) the gradual arm itself ({RewriteMutation}),
38
+ # so the site widens like any other storing site.
39
+ #
40
+ # The per-element block fold is the consumer: it types every position from one entry scope, so the binding
41
+ # it lays under each position has to hold whatever earlier iterations stored. Unknown evidence is the point,
42
+ # not a shortcut — a stored value typed in that same entry scope (`h[k] = h[k] + 1`) is itself a
43
+ # first-iteration answer.
44
+ #
45
+ # Two more kinds of site change a binding's contents without naming it as the receiver, and each is widened
46
+ # the way straight-line code widens it:
47
+ #
48
+ # - A mutator on an ELEMENT of the binding (`a[0] << e`, `a.first.push(e)`): the site's receiver is an element
49
+ # read rooted at the variable ({ElementReadWidening.element_read_path}), and the same widening runs on the
50
+ # element the path selects, rebuilding the carrier around it. The other slots keep their types, and a path
51
+ # {ElementReadWidening} cannot follow (a `HashShape` slot) declines, as it does on straight-line code.
52
+ # - A self-call whose callee content-mutates the parameter the binding is passed to (`add_to(a, e)`, a
53
+ # {CalleeStore}): nothing at the call says what the callee stores or removes, so each collection the binding
54
+ # can hold is floored to its bare carrier ({.content_floor}), as ADR-57's callee floor does after the call.
55
+ module UnknownStoreWidening
56
+ # Mutators that only remove or reorder what the receiver already holds.
57
+ VALUE_PRESERVING = %i[
58
+ pop shift delete delete_at delete_if reject! select! filter! keep_if clear compact! uniq! slice!
59
+ sort! sort_by! reverse! rotate! shuffle!
60
+ ].to_set.freeze
61
+
62
+ # A site that mutates a binding through a callee: `call` passes the variable, read by one of `arguments`, to
63
+ # a parameter the callee content-mutates.
64
+ CalleeStore = Data.define(:call, :arguments)
65
+
66
+ COLLECTION_CLASSES = %w[Array Hash].freeze
67
+ FLOORED_CLASSES = %w[Array Hash String].freeze
68
+
69
+ NO_ARG_NODES = [].freeze
70
+ private_constant :COLLECTION_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
71
+
72
+ module_function
73
+
74
+ # @param sites — in-place mutation nodes whose receiver, or an element read their receiver starts from, can
75
+ # evaluate to the binding `type` describes, and {CalleeStore}s whose call passes that binding.
76
+ # @return `type` widened through every site; `type` itself when no site's widening applies.
77
+ def widen(type, sites)
78
+ sites.reduce(type) { |acc, site| widen_site(acc, site) || acc }
79
+ end
80
+
81
+ # `type` widened through one site, or `nil` when its widening declines.
82
+ def widen_site(type, site)
83
+ return content_floor(type) if site.is_a?(CalleeStore)
84
+
85
+ method_name, arg_types = unknown_store(site)
86
+ path = ElementReadWidening.element_read_path(site.receiver)
87
+ return widen_store(type, method_name, arg_types) if path.nil?
88
+
89
+ ElementReadWidening.widen_through_path(type, path.last, method_name, arg_types) do |element|
90
+ widen_store(element, method_name, arg_types)
91
+ end
92
+ end
93
+
94
+ # The straight-line widening of `type` under `method_name`, with the gradual arm on every site that can store.
95
+ def widen_store(type, method_name, arg_types)
96
+ widened = MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types)
97
+ return nil if widened.nil?
98
+
99
+ VALUE_PRESERVING.include?(method_name) && !literal_carrier?(type) ? widened : gradual_content(widened)
100
+ end
101
+
102
+ # Every `Array` / `Hash` / `String` carrier `type` can be as its bare carrier: `Array[Dynamic[top]]`,
103
+ # `Hash[Dynamic[top], Dynamic[top]]`, `String`. A literal, a nominal (a precise one included) and a refinement
104
+ # over one ({.carrier_class}) all floor; so `non-empty-string`, which a callee can empty, reads `String`.
105
+ # Unlike the escaping floor (`StatementEvaluator#content_floor_for`), which answers one carrier for a whole
106
+ # `Union`, this floors a `Union` member by member, so a member no callee can fill (`nil`) stays: the binding
107
+ # stands where the entry binding stood, and dropping a member would narrow it. Anything else is returned
108
+ # untouched.
109
+ def content_floor(type)
110
+ return Type::Combinator.union(*type.members.map { |member| content_floor(member) }) if type.is_a?(Type::Union)
111
+
112
+ carrier = carrier_class(type)
113
+ carrier ? carrier_floor(carrier) : type
114
+ end
115
+
116
+ # `"Array"`, `"Hash"` or `"String"` when `type` is a form of that carrier — a literal, a nominal, or a
117
+ # difference, refinement or intersection over one (`non-empty-array[Integer]`, `decimal-int-string`,
118
+ # `non-empty-uppercase-string`) — else nil.
119
+ def carrier_class(type)
120
+ case type
121
+ when Type::Tuple then "Array"
122
+ when Type::HashShape then "Hash"
123
+ when Type::Constant then "String" if type.value.is_a?(String)
124
+ when Type::Nominal then type.class_name if FLOORED_CLASSES.include?(type.class_name)
125
+ when Type::Difference, Type::Refined then carrier_class(type.base)
126
+ when Type::Intersection then intersection_carrier_class(type)
127
+ end
128
+ end
129
+
130
+ def intersection_carrier_class(type)
131
+ type.members.each do |member|
132
+ carrier = carrier_class(member)
133
+ return carrier if carrier
134
+ end
135
+ nil
136
+ end
137
+
138
+ def carrier_floor(class_name)
139
+ return Type::Combinator.nominal_of("String") if class_name == "String"
140
+
141
+ arity = class_name == "Hash" ? 2 : 1
142
+ Type::Combinator.nominal_of(class_name, type_args: Array.new(arity) { Type::Combinator.untyped })
143
+ end
144
+
145
+ # A carrier that still records its slots or keys: a `Tuple` / `HashShape`, alone or as a `Union` member.
146
+ def literal_carrier?(type)
147
+ case type
148
+ when Type::Tuple, Type::HashShape then true
149
+ when Type::Union then type.members.any? { |member| literal_carrier?(member) }
150
+ else false
151
+ end
152
+ end
153
+
154
+ # `[mutator, arg_types]` for one site: a call stores through its own name and arguments, an index write
155
+ # through `[]=` with its index arguments ahead of the value. A splat stays the `nil` arity marker the
156
+ # `[]=` splice classifier reads.
157
+ def unknown_store(site)
158
+ arguments = site.arguments&.arguments || NO_ARG_NODES
159
+ unknown = arguments.map { |arg| arg.is_a?(Prism::SplatNode) ? nil : Type::Combinator.untyped }
160
+ return [site.name, unknown] if site.is_a?(Prism::CallNode)
161
+
162
+ [:[]=, unknown + [Type::Combinator.untyped]]
163
+ end
164
+
165
+ # An `Array` / `Hash` nominal (alone, as a `Union` member, or as a refinement's base) with a value-pinned type
166
+ # argument. `Type::Combinator.widen_value_pinned` does not look inside type arguments, so each one is asked on
167
+ # its own. A `bool` or a literal union a signature declared (`Array[:a | :b]`) counts as pinned too; the arm it
168
+ # takes can only quiet a report, which is the accepted cost of reading every such binding past one iteration.
169
+ # A class-level argument (`Array[Integer]`) is not pinned: a store of the same class leaves it true, which is
170
+ # the claim this seam may not grow.
171
+ def value_pinned_collection?(type)
172
+ case type
173
+ when Type::Union then type.members.any? { |member| value_pinned_collection?(member) }
174
+ when Type::Difference then value_pinned_collection?(type.base)
175
+ when Type::Nominal
176
+ COLLECTION_CLASSES.include?(type.class_name) &&
177
+ type.type_args.any? { |arg| Type::Combinator.widen_value_pinned(arg) != arg }
178
+ else false
179
+ end
180
+ end
181
+
182
+ # Every type argument of an `Array` / `Hash` carrier joined with `Dynamic[top]`, member-wise through a
183
+ # `Union` and through a `Difference`'s base. Anything else is returned untouched.
184
+ def gradual_content(type)
185
+ case type
186
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| gradual_content(member) })
187
+ when Type::Difference then Type::Combinator.difference(gradual_content(type.base), type.removed)
188
+ when Type::Nominal
189
+ return type unless COLLECTION_CLASSES.include?(type.class_name) && !type.type_args.empty?
190
+
191
+ Type::Combinator.nominal_of(
192
+ type.class_name,
193
+ type_args: type.type_args.map { |arg| Type::Combinator.union(arg, Type::Combinator.untyped) }
194
+ )
195
+ else type
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_walker"
6
+ require_relative "captured_locals"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # The names of a {CapturedLocals.writes} set that a block body rebinds somewhere `StatementEvaluator` does
11
+ # not carry into the body's exit scope.
12
+ #
13
+ # The per-element fold's issue #587 (b) fixpoint reads each pass's exit binding out of that evaluator, so a
14
+ # rebind the exit never sees leaves the fixpoint on a binding the program has already moved past, and every
15
+ # position of the fold re-reads it. Issue #617's unmoved-pin floor caught such a name only while nothing else
16
+ # moved its binding: `seen = nil; [1, 2].find { |e| seen ||= 0; (seen += 1) == 2 }` threads the `||=`, so the
17
+ # fixpoint left the seed, and every predicate read `0 + 1 == 2`. A name this scan returns takes the floor
18
+ # whatever the fixpoint converged to.
19
+ #
20
+ # The scan admits a write from these positions only, each one the evaluator threads, measured against it
21
+ # rather than read off it:
22
+ #
23
+ # - a statement, and the body of `(…)`, `begin` / `rescue` / `else` / `ensure`;
24
+ # - an assignment's value — every variable-write form, and an index `||=` / `&&=` / `op=`, but not its
25
+ # receiver or index arguments;
26
+ # - an `if` / `unless` / `case` predicate and every branch, but not a `when` condition or an `in` guard;
27
+ # - both operands of `&&` / `||`;
28
+ # - a `while` / `until` predicate and body, a `for` index, collection and body;
29
+ # - a multi-assign value and its local and instance-variable targets, and the LOCAL a `rescue => e`
30
+ # reference, a `for` index, a pattern capture or `=~`'s named captures binds — the evaluator binds no
31
+ # class variable or global through any of them, and no instance variable outside a multi-assign;
32
+ # - a nested block or lambda body, for a local or an instance variable: the call's write-back fixpoint, which
33
+ # joins the block's `next` and `break` paths, or the escaping-block floor covers both, and nothing covers
34
+ # a class variable or global there.
35
+ #
36
+ # Every other position counts as unthreaded: a call's receiver and arguments, an array or hash literal, an
37
+ # interpolation, a `return` / `next` / `break` value, `rescue` modifier, `self.w =` (which rebinds `@w` with
38
+ # no write node at all), a `def` or class body. The list is a whitelist, so a position the evaluator gains
39
+ # later floors a name needlessly rather than letting a pin through. Issue #1223 is such a gain: the evaluator
40
+ # threads a call's receiver and arguments, the literals, the interpolations and a `rescue` modifier
41
+ # ({StatementEvaluator#eval_call}), and the scan does not admit them yet.
42
+ #
43
+ # A threaded rebind can still miss the exit binding when a jump leaves after it. The fold's pass and a
44
+ # nested block's write-back join every block-level `next` (and the write-back every `break`) into the exit
45
+ # ({StatementEvaluator#evaluate_invocation}), but a `while` / `until` / `for` loop's continuation joins its
46
+ # `next` and `break` paths only in part ({StatementEvaluator#loop_iteration}): a `for` body runs one pass,
47
+ # and a name outside the `while` / `until` rebind fixpoint (an instance variable) sees the first pass's
48
+ # `next` exits and no `break` — so the rebind such a branch made can still drop out — and `redo` / `retry`
49
+ # re-enter code with bindings no join sees. A rebind that textually precedes such a jump is therefore
50
+ # unthreaded too; the body runs forward-only, so a rebind after every jump cannot be on a jump's path, and
51
+ # a jump that is its construct's final statement leaves with the exit scope itself. A rebind in an `ensure`
52
+ # runs after any jump in its `begin`, so under any such jump it counts as preceding one.
53
+ module UnthreadedRebinds
54
+ VARIABLE_WRITE_NODES = (CapturedLocals::LOCAL_WRITE_NODES | CapturedLocals::NON_LOCAL_WRITE_NODES).freeze
55
+ private_constant :VARIABLE_WRITE_NODES
56
+
57
+ TARGET_NODES = Set[
58
+ Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
59
+ Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
60
+ ].freeze
61
+ private_constant :TARGET_NODES
62
+
63
+ VALUE_ONLY = { value: :code }.freeze
64
+ private_constant :VALUE_ONLY
65
+
66
+ # Parent class => the child fields the evaluator threads, each with how it is entered: `:code` threads
67
+ # writes, `:binding` threads only the targets the construct binds ({#binding_target_threaded?}),
68
+ # `:ensure` is an `ensure` body, `:loop` a loop body, `:nested` a call's block and `:lambda` a lambda's
69
+ # body.
70
+ THREADED_FIELDS = {
71
+ Prism::StatementsNode => { body: :code },
72
+ Prism::ParenthesesNode => { body: :code },
73
+ Prism::BeginNode => { statements: :code, rescue_clause: :code, else_clause: :code, ensure_clause: :code },
74
+ Prism::RescueNode => { reference: :binding, statements: :code, subsequent: :code },
75
+ Prism::EnsureNode => { statements: :ensure },
76
+ Prism::ElseNode => { statements: :code },
77
+ Prism::IfNode => { predicate: :code, statements: :code, subsequent: :code },
78
+ Prism::UnlessNode => { predicate: :code, statements: :code, else_clause: :code },
79
+ Prism::CaseNode => { predicate: :code, conditions: :code, else_clause: :code },
80
+ Prism::WhenNode => { statements: :code },
81
+ Prism::CaseMatchNode => { predicate: :code, conditions: :code, else_clause: :code },
82
+ Prism::InNode => { pattern: :binding, statements: :code },
83
+ Prism::AndNode => { left: :code, right: :code },
84
+ Prism::OrNode => { left: :code, right: :code },
85
+ Prism::MatchPredicateNode => { value: :code, pattern: :binding },
86
+ Prism::MatchRequiredNode => { value: :code, pattern: :binding },
87
+ Prism::MatchWriteNode => { targets: :binding },
88
+ Prism::MultiWriteNode => { lefts: :binding, rest: :binding, rights: :binding, value: :code },
89
+ Prism::WhileNode => { predicate: :code, statements: :loop },
90
+ Prism::UntilNode => { predicate: :code, statements: :loop },
91
+ Prism::ForNode => { index: :binding, collection: :code, statements: :loop },
92
+ Prism::CallNode => { block: :nested },
93
+ Prism::BlockNode => { body: :code },
94
+ Prism::LambdaNode => { body: :lambda },
95
+ Prism::IndexOrWriteNode => VALUE_ONLY,
96
+ Prism::IndexAndWriteNode => VALUE_ONLY,
97
+ Prism::IndexOperatorWriteNode => VALUE_ONLY,
98
+ **(VARIABLE_WRITE_NODES - TARGET_NODES).to_h { |klass| [klass, VALUE_ONLY] }
99
+ }.transform_values(&:freeze).freeze
100
+ private_constant :THREADED_FIELDS
101
+
102
+ # The jumps whose path the exit binding misses, per construct. A block's `next` and `break` are joined
103
+ # ({StatementEvaluator#evaluate_invocation}, the write-back), and a `break` out of the fold's own call ends
104
+ # it, so no later iteration reads what it carried; `redo` and `retry` are joined nowhere. A loop joins
105
+ # its `next` and `break` paths only in part, so both still count.
106
+ BLOCK_JUMPS = Set[Prism::RedoNode, Prism::RetryNode].freeze
107
+ LOOP_JUMPS = (BLOCK_JUMPS | [Prism::NextNode, Prism::BreakNode]).freeze
108
+ private_constant :BLOCK_JUMPS, :LOOP_JUMPS
109
+
110
+ # The constructs that own the jumps inside them.
111
+ JUMP_BOUNDARY_NODES = Set[
112
+ Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
113
+ Prism::WhileNode, Prism::UntilNode, Prism::ForNode
114
+ ].freeze
115
+ private_constant :JUMP_BOUNDARY_NODES
116
+
117
+ NO_JUMP = -1
118
+ private_constant :NO_JUMP
119
+
120
+ EMPTY = Set.new.freeze
121
+ private_constant :EMPTY
122
+
123
+ # How a walk position reaches the exit scope: whether a local rebind there is threaded, whether an
124
+ # instance variable's is, whether a class variable's or global's is, the offset below which a rebind
125
+ # precedes a jump,
126
+ # whether the position binds targets only (`:multi_assign` under a multi-assign, `:capture` under any
127
+ # other binding construct, false otherwise), and the locals a nested block shadows.
128
+ Position = Data.define(:local, :ivar, :other, :horizon, :binding, :shadowed)
129
+ private_constant :Position
130
+
131
+ module_function
132
+
133
+ # @param names — the rebound names from {CapturedLocals.writes}, sigil and all.
134
+ # @return the subset of `names` the body rebinds somewhere the exit scope misses.
135
+ def names(block_node, names)
136
+ body = block_node.body
137
+ return EMPTY if body.nil? || names.empty?
138
+
139
+ wanted = names.to_set
140
+ found = Set.new
141
+ horizon = last_jump_offset(body, BLOCK_JUMPS)
142
+ start = Position.new(local: true, ivar: true, other: true, horizon: horizon, binding: false, shadowed: EMPTY)
143
+ visit(body, start, wanted, found)
144
+ found
145
+ end
146
+
147
+ def visit(node, position, wanted, found)
148
+ return unless node.is_a?(Prism::Node)
149
+ return if node.is_a?(Prism::DefinedNode)
150
+
151
+ check(node, position, wanted, found)
152
+ return node.rigor_each_child { |child| visit(child, position, wanted, found) } if position.binding
153
+
154
+ fields = THREADED_FIELDS[node.class]
155
+ return node.rigor_each_child { |child| visit_unthreaded(child, wanted, found) } if fields.nil?
156
+
157
+ threaded = threaded_children(node, fields)
158
+ node.rigor_each_child do |child|
159
+ mode = threaded[child]
160
+ if mode
161
+ visit(child, enter(node, child, mode, position), wanted, found)
162
+ else
163
+ visit_unthreaded(child, wanted, found)
164
+ end
165
+ end
166
+ end
167
+
168
+ def threaded_children(node, fields)
169
+ threaded = {}.compare_by_identity
170
+ fields.each do |field, mode|
171
+ value = node.public_send(field)
172
+ if value.is_a?(Array)
173
+ value.each { |child| threaded[child] = mode }
174
+ elsif value
175
+ threaded[value] = mode
176
+ end
177
+ end
178
+ threaded
179
+ end
180
+
181
+ # Every rebind under an unthreaded position misses the exit scope, whatever the position beneath it.
182
+ def visit_unthreaded(node, wanted, found)
183
+ Source::NodeWalker.each(node) do |descendant|
184
+ name = rebound_name(descendant)
185
+ found << name if name && wanted.include?(name)
186
+ end
187
+ end
188
+
189
+ def enter(parent, child, mode, position)
190
+ case mode
191
+ when :binding then position.with(binding: parent.is_a?(Prism::MultiWriteNode) ? :multi_assign : :capture)
192
+ when :ensure then position.with(horizon: position.horizon == NO_JUMP ? NO_JUMP : Float::INFINITY)
193
+ when :loop then extend_horizon(position, child, LOOP_JUMPS)
194
+ when :nested then nested_block(child, position)
195
+ when :lambda then nested_body(parent, child, position)
196
+ else position
197
+ end
198
+ end
199
+
200
+ def extend_horizon(position, body, kinds)
201
+ position.with(horizon: [position.horizon, last_jump_offset(body, kinds)].max)
202
+ end
203
+
204
+ # A call's block. A block-pass argument (`&blk`) carries no body the call's write-back reads.
205
+ def nested_block(child, position)
206
+ return position.with(local: false, ivar: false, other: false) unless child.is_a?(Prism::BlockNode)
207
+
208
+ nested_body(child, child.body, position)
209
+ end
210
+
211
+ # A block or lambda body: its parameters and block-locals shadow, its own jumps join the horizon, and a
212
+ # class variable's or global's rebind is no longer carried out.
213
+ def nested_body(owner, body, position)
214
+ extend_horizon(position, body, BLOCK_JUMPS).with(
215
+ other: false, shadowed: position.shadowed | CapturedLocals.introduced_locals(owner)
216
+ )
217
+ end
218
+
219
+ def check(node, position, wanted, found)
220
+ name = rebound_name(node)
221
+ return if name.nil? || !wanted.include?(name)
222
+ return if position.shadowed.include?(name)
223
+
224
+ found << name unless threaded_rebind?(node, name, position)
225
+ end
226
+
227
+ def threaded_rebind?(node, name, position)
228
+ return false unless VARIABLE_WRITE_NODES.include?(node.class)
229
+ return false unless TARGET_NODES.include?(node.class) == (position.binding ? true : false)
230
+
231
+ kind = CapturedLocals.variable_kind(name)
232
+ return false if position.binding && !binding_target_threaded?(kind, position.binding)
233
+ return false unless kind_threaded?(kind, position)
234
+
235
+ node.location.start_offset >= position.horizon
236
+ end
237
+
238
+ def kind_threaded?(kind, position)
239
+ case kind
240
+ when :local then position.local
241
+ when :ivar then position.ivar
242
+ else position.other
243
+ end
244
+ end
245
+
246
+ # A binding construct's target reaches the exit scope only for the kinds the evaluator binds there: a
247
+ # multi-assign binds locals and instance variables, and a `rescue =>` reference, a `for` index, a pattern
248
+ # capture or a named capture binds locals only. `a, $g = 1, :z` leaves `$g` where it was.
249
+ def binding_target_threaded?(kind, binding)
250
+ kind == :local || (kind == :ivar && binding == :multi_assign)
251
+ end
252
+
253
+ def rebound_name(node)
254
+ return node.name if VARIABLE_WRITE_NODES.include?(node.class)
255
+
256
+ CapturedLocals.setter_ivar_name(node)
257
+ end
258
+
259
+ # The start offset of the last `kinds` jump that belongs to the construct whose body is `body`, or
260
+ # {NO_JUMP}. A jump that is the body's final statement is skipped: it leaves with the exit scope itself.
261
+ def last_jump_offset(body, kinds)
262
+ return NO_JUMP if body.nil?
263
+
264
+ final = body.body.last if body.is_a?(Prism::StatementsNode)
265
+ offset = NO_JUMP
266
+ each_owned_jump(body, kinds) do |jump|
267
+ offset = [offset, jump.location.start_offset].max unless jump.equal?(final)
268
+ end
269
+ offset
270
+ end
271
+
272
+ def each_owned_jump(node, kinds, &)
273
+ yield node if kinds.include?(node.class)
274
+ return if node.is_a?(Prism::DefinedNode)
275
+
276
+ node.rigor_each_child do |child|
277
+ each_owned_jump(child, kinds, &) unless JUMP_BOUNDARY_NODES.include?(child.class)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -68,7 +68,6 @@ module Rigor
68
68
  end
69
69
  end
70
70
 
71
- # @return number of currently-pending tasks.
72
71
  def pending_size
73
72
  @mutex.synchronize { @tasks.size }
74
73
  end
@@ -268,8 +268,9 @@ module Rigor
268
268
 
269
269
  # Runs `Analysis::Runner` with a `BufferBinding` so the buffer bytes (instead of the on-disk file) drive
270
270
  # the parse. The `Rigor::Analysis::ProjectScan` cached on the ProjectContext is passed through
271
- # `prebuilt:` so plugin `#prepare`, the dependency-source walker, and the synthetic-method /
272
- # project-patched scanners do not re-run per publish. The snapshot rebuilds only when
271
+ # `prebuilt:` so plugin `#prepare`, the dependency-source walker, the synthetic-method /
272
+ # project-patched scanners and (#1038) the template-unit transforms do not re-run per publish — the
273
+ # last of those is an Erubi compile of every view for an ERB project. The snapshot rebuilds only when
273
274
  # `ProjectContext#invalidate!` fires (watched-file or configuration change). Returns the LSP-shaped
274
275
  # Diagnostic Array, ready to serialize into the notification's `params.diagnostics` field.
275
276
  def run_analysis(path:, bytes:)
@@ -83,9 +83,9 @@ module Rigor
83
83
  )
84
84
  end
85
85
 
86
- # @return , nil] the resolved method definition, the receiver
87
- # class name, and the dispatch kind (`:instance` or `:singleton`); nil when the receiver shape isn't
88
- # yet supported or the method doesn't resolve through the RBS env.
86
+ # @return `[definition, receiver_class_name, kind]` — the resolved method definition (nil when the
87
+ # method doesn't resolve through the RBS env), the receiver class name, and the dispatch kind
88
+ # (`:instance` or `:singleton`); `[nil, nil, nil]` when the receiver shape isn't yet supported.
89
89
  def lookup_method(receiver_type, method_name, scope)
90
90
  case receiver_type
91
91
  when Type::Singleton