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,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_walker"
6
+ require_relative "../type"
7
+ require_relative "block_parameter_binder"
8
+ require_relative "captured_locals"
9
+ require_relative "index_write_widening"
10
+ require_relative "mutation_widening"
11
+ require_relative "unknown_store_widening"
12
+
13
+ module Rigor
14
+ module Inference
15
+ # The index `||=` sites of a repeating block body whose slot an EARLIER run of the body may already have
16
+ # filled. `StatementEvaluator#index_compound_write_value` reads a `||=` whose slot types as a lone `Dynamic`
17
+ # as the memoization idiom and answers the rvalue, on the ADR-5 reading that nothing the analyzer saw set
18
+ # the slot. A block-return pass types every run of the body from one entry scope, so under it that reading
19
+ # is wrong for a slot an earlier run stored into: the per-element fold's second position of
20
+ # `cache = {}; [1, 2].find { |e| (cache[:first] ||= e) == 2 }` answered its own `2`, where Ruby keeps the
21
+ # first iteration's `1`, folded `find` to `2` and reported `found == 2` always-truthy. The pass marks the
22
+ # sites this module returns (`Scope#with_repeated_or_writes`), and the reading is withheld at a marked site.
23
+ #
24
+ # The mark is per site, not per receiver, because nothing about the receiver's type can say that its slot
25
+ # was filled: `{}` widens to `Hash[Dynamic[top], Dynamic[top]]`, a bare `Hash.new` is not widened at all, and
26
+ # a constant, class variable, attribute reader or nested memo (`(cache[:a] ||= {})[:b] ||= e`) is no binding
27
+ # the fold rebinds. A narrowing or a rebind of the receiver's variable leaves the mark where it is.
28
+ #
29
+ # A site whose receiver is fresh at every run — a hash or array literal, a `.new` call on a constant, or the
30
+ # own slots of a local only the body binds, always to such an object — is never marked. Otherwise:
31
+ #
32
+ # - The generic block-return pass types the rvalue from the signature's parameter type, which covers every
33
+ # run's store, so it marks a site only when the site is not ISOLATED ({.isolated?}): some other store in the
34
+ # body may fill its slot with something else. `words.map { |w| pool[w] ||= w }` keeps `Array[String]`.
35
+ # - The per-element fold types each position's rvalue on its own, so it marks per position ({Marks}). The
36
+ # first position has no earlier one and is never marked. A later one is marked unless the site is isolated,
37
+ # sits at the body's own level (not in a nested block or loop), and its key differs from every earlier
38
+ # position's ({FreshKeys.positions}): `pool = {}; %w[a b].map { |s| pool[s] ||= s.upcase }` keeps `["A", "B"]`,
39
+ # and `%w[a a b].find { |s| (pool[s] ||= s) == "b" }` marks only the second position.
40
+ module RepeatedOrWrites
41
+ NO_SITES = [].freeze
42
+
43
+ # The sites a pass marks at each run it types on its own. `shared` is marked at every run (the generic pass);
44
+ # `positional[i]` at the per-element fold's position `i` only. {#at} with a nil position — a caller that
45
+ # does not type one position — answers every site.
46
+ Marks = Data.define(:shared, :positional) do
47
+ def at(position)
48
+ return shared if positional.empty?
49
+ return positional.flatten.uniq if position.nil?
50
+
51
+ positional.fetch(position, NO_SITES)
52
+ end
53
+
54
+ def empty? = shared.empty? && positional.all?(&:empty?)
55
+ end
56
+
57
+ NO_MARKS = Marks.new(shared: NO_SITES, positional: NO_SITES)
58
+
59
+ # A store in the body and the object it stores into, as `[root, depth]`: the variable the receiver
60
+ # evaluates from (`[:ivar, :@cache]`) and how many element reads lie between the two (`@cache[k][j] = v`
61
+ # stores into depth 1). `path` is nil for an object the store cannot name. `installs_shared` is whether it
62
+ # can put an object an earlier run may have filled into a slot — anything but a fresh object, and nothing
63
+ # for a store that only removes or reorders.
64
+ Store = Data.define(:node, :path, :installs_shared)
65
+
66
+ # What the body holds: the `||=` sites to consider, the ones at the body's own level, how many stores that
67
+ # may reach an object an earlier run saw there are (those sites included) and how many reach each path,
68
+ # whether some store's object is unknown, the paths a store may put a shared object into, and every local
69
+ # name the body writes.
70
+ Scan = Data.define(:or_writes, :top_level, :store_count, :path_counts, :unattributable, :installs,
71
+ :written_locals)
72
+
73
+ # The walk's accumulators. `writes` maps each variable the body writes, as `[kind, name]`, to whether every
74
+ # write stores a fresh object into it.
75
+ Walk = Struct.new(:or_writes, :top_level, :stores, :written_locals, :writes)
76
+
77
+ # The node forms and source patterns the scan recognises.
78
+ module Forms
79
+ STORE_NODES = IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES
80
+ INDEX_WRITE_NODES = IndexWriteWidening::NODE_CLASSES
81
+
82
+ # A call that can store through any method name, so into any slot of its receiver.
83
+ DYNAMIC_SEND = Set[:send, :public_send, :__send__].freeze
84
+
85
+ # A call that hands out a method as an object, which a store can then be called through.
86
+ METHOD_HANDLES = Set[:method, :public_method].freeze
87
+
88
+ # A heredoc opener: its text lies outside its node's location, so a `||=` in it escapes the source slice.
89
+ HEREDOC_OPENER = /<<[~-]?["'`]?[A-Za-z_]/
90
+
91
+ # The nodes whose children run more than once for each run of the body that contains them.
92
+ REPEATING_NODES = Set[
93
+ Prism::BlockNode, Prism::LambdaNode, Prism::WhileNode, Prism::UntilNode, Prism::ForNode
94
+ ].freeze
95
+
96
+ # Every variable read and write form, with the kind of variable it names.
97
+ VARIABLE_KINDS = {
98
+ local: [Prism::LocalVariableReadNode, *CapturedLocals::LOCAL_WRITE_NODES],
99
+ ivar: [
100
+ Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
101
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
102
+ ],
103
+ cvar: [
104
+ Prism::ClassVariableReadNode, Prism::ClassVariableWriteNode, Prism::ClassVariableOperatorWriteNode,
105
+ Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableTargetNode
106
+ ],
107
+ gvar: [
108
+ Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode,
109
+ Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
110
+ ],
111
+ const: [
112
+ Prism::ConstantReadNode, Prism::ConstantWriteNode, Prism::ConstantOperatorWriteNode,
113
+ Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantTargetNode
114
+ ]
115
+ }.flat_map { |kind, classes| classes.map { |node_class| [node_class, kind] } }.to_h.freeze
116
+
117
+ READ_NODES = Set[
118
+ Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
119
+ Prism::GlobalVariableReadNode, Prism::ConstantReadNode
120
+ ].freeze
121
+
122
+ # The writes that bind their value as it is: a plain write, and an `||=` (which keeps an object already set).
123
+ VALUE_WRITE_NODES = Set[
124
+ Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
125
+ Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode,
126
+ Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode, Prism::ClassVariableOrWriteNode,
127
+ Prism::GlobalVariableOrWriteNode, Prism::ConstantOrWriteNode
128
+ ].freeze
129
+ end
130
+ private_constant :Forms
131
+
132
+ module_function
133
+
134
+ # @param block — the repeating block.
135
+ # @param stores — {CapturedLocals.content_mutations} of the block (with `non_locals: true`): the captured
136
+ # variables its body mutates in place, each with its sites; its callee stores count as stores here.
137
+ # @param scope — the call-site scope, in which a per-element fold's positions bind their parameters.
138
+ # @param element_types — the per-element fold's position types, one per position; nil for the generic
139
+ # block-return pass.
140
+ # @return the {Marks} to lay, {NO_MARKS} for the overwhelmingly common body with no such site.
141
+ def sites(block, stores, scope, element_types: nil)
142
+ body = block.body
143
+ return NO_MARKS unless body && may_hold_or_write?(body)
144
+
145
+ found = scan(block, stores, scope)
146
+ return NO_MARKS if found.or_writes.empty?
147
+ return generic_marks(found) if element_types.nil?
148
+
149
+ positional_marks(block, found, stores, scope, element_types)
150
+ end
151
+
152
+ # One source slice is far cheaper than the walk, and a body that spells no `||=` holds no such site. A
153
+ # heredoc's text lies outside its node's location, so a body that opens one is walked regardless.
154
+ def may_hold_or_write?(body)
155
+ source = body.slice
156
+ source.include?("||=") || source.match?(Forms::HEREDOC_OPENER)
157
+ end
158
+
159
+ def generic_marks(found)
160
+ Marks.new(shared: found.or_writes.reject { |node| isolated?(node, found) }, positional: NO_SITES)
161
+ end
162
+
163
+ def positional_marks(block, found, stores, scope, element_types)
164
+ positional = Array.new(element_types.size) { [] }
165
+ parameters = nil
166
+ found.or_writes.each do |node|
167
+ fresh = exempt_candidate?(node, found) &&
168
+ FreshKeys.positions(block, node, found.written_locals, stores, scope, element_types,
169
+ parameters ||= CapturedLocals.introduced_locals(block))
170
+ (1...element_types.size).each { |index| positional[index] << node unless fresh && fresh[index] }
171
+ end
172
+ Marks.new(shared: NO_SITES, positional: positional)
173
+ end
174
+
175
+ def exempt_candidate?(node, found) = found.top_level.key?(node) && isolated?(node, found)
176
+
177
+ # Walks the body, then settles each store's object ({.settle}), and drops a site on a body-local fresh at
178
+ # every run ({.fresh_roots}), whose own slots no earlier run saw.
179
+ def scan(block, stores, scope)
180
+ walked = Walk.new([], {}.compare_by_identity, [], Set.new, {})
181
+ walk(block.body, true, walked)
182
+ fresh = fresh_roots(block, walked.writes, scope)
183
+ settled = (walked.stores + callee_stores(stores)).filter_map do |store|
184
+ settle(store, walked.writes, fresh, scope)
185
+ end
186
+ or_writes = walked.or_writes.reject { |node| on_fresh_slot?(receiver_path(node.receiver, 0), fresh) }
187
+ summary(walked, or_writes, settled)
188
+ end
189
+
190
+ def summary(walked, or_writes, settled)
191
+ Scan.new(or_writes: or_writes, top_level: walked.top_level, store_count: settled.size,
192
+ path_counts: settled.map(&:path).tally, unattributable: settled.any? { |store| store.path.nil? },
193
+ installs: settled.select(&:installs_shared).map(&:path), written_locals: walked.written_locals)
194
+ end
195
+
196
+ def on_fresh_slot?(path, fresh) = !path.nil? && path.last.zero? && fresh.include?(path.first)
197
+
198
+ # The locals the body alone binds — not captured, not a block parameter or block-local — whose every write
199
+ # stores a fresh object.
200
+ def fresh_roots(block, writes, scope)
201
+ introduced = nil
202
+ writes.each_with_object(Set.new) do |((kind, name), all_fresh), roots|
203
+ next unless all_fresh && kind == :local && !scope.locals.key?(name)
204
+
205
+ introduced ||= CapturedLocals.introduced_locals(block)
206
+ roots << [kind, name] unless introduced.include?(name)
207
+ end
208
+ end
209
+
210
+ # The store as the isolation test reads it; nil for one that reaches nothing an earlier run saw. A store is
211
+ # made unattributable when its variable may name some other object: one the body binds from anything but a
212
+ # fresh object (`other = pool`), or a local that is neither captured nor written by the body — a block
213
+ # parameter, `it`, `_1` — which holds whatever it is handed. A body-local fresh at every run holds a new
214
+ # object, so a store into its own slots is dropped; but that object may hold an old one (`box = [pool]`), so
215
+ # a store deeper in is unattributable.
216
+ def settle(store, writes, fresh, scope)
217
+ root = store.path&.first
218
+ return store if root.nil?
219
+
220
+ if writes.key?(root)
221
+ return nil if on_fresh_slot?(store.path, fresh)
222
+ return store if writes[root] && !fresh.include?(root)
223
+ elsif root.first != :local || scope.locals.key?(root.last)
224
+ return store
225
+ end
226
+ Store.new(node: store.node, path: nil, installs_shared: true)
227
+ end
228
+
229
+ def walk(node, top_level, found)
230
+ return unless node.is_a?(Prism::Node)
231
+ return if node.is_a?(Prism::DefNode)
232
+
233
+ record_write(node, found)
234
+ record_store(node, top_level, found)
235
+ return if node.is_a?(Prism::DefinedNode)
236
+
237
+ nested_level = top_level && !Forms::REPEATING_NODES.include?(node.class)
238
+ node.rigor_each_child { |child| walk(child, nested_level, found) }
239
+ end
240
+
241
+ def record_write(node, found)
242
+ kind = Forms::VARIABLE_KINDS[node.class]
243
+ return if kind.nil? || Forms::READ_NODES.include?(node.class)
244
+
245
+ found.written_locals << node.name if kind == :local
246
+ root = [kind, node.name]
247
+ fresh = Forms::VALUE_WRITE_NODES.include?(node.class) && fresh_object?(node.value)
248
+ found.writes[root] = found.writes.fetch(root, true) && fresh
249
+ end
250
+
251
+ def record_store(node, top_level, found)
252
+ if dynamic_store?(node)
253
+ found.stores << Store.new(node: node, path: nil, installs_shared: true)
254
+ return
255
+ end
256
+ return unless store_node?(node)
257
+ return if fresh_object?(node.receiver)
258
+
259
+ found.stores << Store.new(node: node, path: store_path(node), installs_shared: installs_shared?(node))
260
+ return unless node.is_a?(Prism::IndexOrWriteNode)
261
+
262
+ found.or_writes << node
263
+ found.top_level[node] = true if top_level
264
+ end
265
+
266
+ def store_node?(node)
267
+ return true if Forms::STORE_NODES.include?(node.class)
268
+
269
+ node.is_a?(Prism::CallNode) && store_method?(node.name)
270
+ end
271
+
272
+ def store_method?(name) = name == :[]= || MutationWidening::SHAPE_MUTATORS.include?(name)
273
+
274
+ # A call that can store through a method it does not name — `send` and its kin — or that hands out a store
275
+ # method to be called later (`pool.method(:[]=)`).
276
+ def dynamic_store?(node)
277
+ return false unless node.is_a?(Prism::CallNode)
278
+ return true if Forms::DYNAMIC_SEND.include?(node.name)
279
+ return false unless Forms::METHOD_HANDLES.include?(node.name)
280
+
281
+ name = node.arguments&.arguments&.first
282
+ name.is_a?(Prism::SymbolNode) && store_method?(name.unescaped.to_sym)
283
+ end
284
+
285
+ # Whether the store `node` can put an object an earlier run may have filled into a slot: an index write of
286
+ # anything but a fresh object, a mutator handed one, or any other store form; not a mutator that only
287
+ # removes or reorders.
288
+ def installs_shared?(node)
289
+ case node
290
+ when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode then !fresh_object?(node.value)
291
+ when Prism::CallNode
292
+ return false if UnknownStoreWidening::VALUE_PRESERVING.include?(node.name)
293
+
294
+ arguments = node.arguments&.arguments || NO_SITES
295
+ stored = node.name == :[]= ? arguments.last(1) : arguments
296
+ stored.any? { |argument| !fresh_object?(argument) }
297
+ else true
298
+ end
299
+ end
300
+
301
+ # A call with no receiver stores into `self`.
302
+ def store_path(node)
303
+ return [[:self], 0] if node.receiver.nil?
304
+
305
+ receiver_path(node.receiver, 0)
306
+ end
307
+
308
+ # `[root, depth]` for the object `node` evaluates to, or nil when no variable roots it.
309
+ def receiver_path(node, depth)
310
+ case node
311
+ when Prism::ParenthesesNode then parenthesized_path(node, depth)
312
+ when Prism::SelfNode then [[:self], depth]
313
+ when Prism::ItLocalVariableReadNode then [%i[local it], depth]
314
+ when Prism::CallNode then element_read_path(node, depth)
315
+ when *Forms::INDEX_WRITE_NODES then receiver_path(node.receiver, depth + 1)
316
+ else variable_path(node, depth)
317
+ end
318
+ end
319
+
320
+ def parenthesized_path(node, depth)
321
+ statements = node.body
322
+ return nil unless statements.is_a?(Prism::StatementsNode) && statements.body.size == 1
323
+
324
+ receiver_path(statements.body.first, depth)
325
+ end
326
+
327
+ def element_read_path(node, depth)
328
+ return nil unless node.name == :[] && node.receiver && !node.safe_navigation?
329
+
330
+ receiver_path(node.receiver, depth + 1)
331
+ end
332
+
333
+ def variable_path(node, depth)
334
+ kind = Forms::VARIABLE_KINDS[node.class]
335
+ kind && [[kind, node.name], depth]
336
+ end
337
+
338
+ # {CapturedLocals.content_mutations}'s callee stores — a self-call passing a captured local to a parameter
339
+ # its callee mutates — as stores into that local.
340
+ def callee_stores(stores)
341
+ stores.flat_map do |name, sites|
342
+ sites.grep(UnknownStoreWidening::CalleeStore).map do |site|
343
+ Store.new(node: site.call, path: [[:local, name.to_sym], 0], installs_shared: true)
344
+ end
345
+ end
346
+ end
347
+
348
+ # An expression that evaluates to a new object at every run: a hash or array literal, or `.new` called on a
349
+ # constant.
350
+ def fresh_object?(node)
351
+ case node
352
+ when Prism::HashNode, Prism::ArrayNode then true
353
+ when Prism::CallNode
354
+ node.name == :new &&
355
+ (node.receiver.is_a?(Prism::ConstantReadNode) || node.receiver.is_a?(Prism::ConstantPathNode))
356
+ else false
357
+ end
358
+ end
359
+
360
+ # True when no other store in the body can fill `node`'s slot: no other store reaches the object `node` stores
361
+ # into (the same variable, at the same element depth), none can put a shared object into a container above it
362
+ # on the way (`pool[:a] = shared` above `pool[:a][s] ||= s`), and every store names the object it reaches
363
+ # ({.settle}), the site's own included. A site whose receiver no variable roots is isolated only as the
364
+ # body's sole store.
365
+ def isolated?(node, found)
366
+ path = receiver_path(node.receiver, 0)
367
+ return found.store_count == 1 if path.nil?
368
+ return false if found.unattributable || found.path_counts[path] != 1
369
+
370
+ root, depth = path
371
+ found.installs.none? { |(other_root, other_depth)| other_root == root && other_depth < depth }
372
+ end
373
+
374
+ # Whether an index `||=` site's key differs, position by position, from every earlier position's key under
375
+ # the per-element fold.
376
+ module FreshKeys
377
+ # The nodes a key expression may not contain; see {.fixed_key?}.
378
+ KEY_UNFIXED_NODES = Set[
379
+ Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode,
380
+ Prism::BlockNode, Prism::LambdaNode, *CapturedLocals::LOCAL_WRITE_NODES, *CapturedLocals::NON_LOCAL_WRITE_NODES
381
+ ].freeze
382
+
383
+ HASH_KEY_CLASSES = [Symbol, String, Integer].freeze
384
+ private_constant :KEY_UNFIXED_NODES, :HASH_KEY_CLASSES
385
+
386
+ module_function
387
+
388
+ # For each position, whether `node`'s key there is provably a value no earlier position's key equals; nil
389
+ # when the key cannot be typed position by position. The key must be a single index argument that reads no
390
+ # variable but ones the body never changes ({.fixed_key?}), so typing it under each position's parameter
391
+ # binding is typing what the position evaluates. The keys up to a position must be `Constant`s its receiver
392
+ # tells apart ({.distinguishable?}), and its own must equal none before it. A key that fails to type is not
393
+ # shown fresh anywhere, so the site stays marked, the wider answer.
394
+ def positions(block, node, written_locals, stores, scope, element_types, parameters)
395
+ key = sole_key(node)
396
+ return nil if key.nil? || !fixed_key?(key, parameters, written_locals, stores)
397
+
398
+ typed = element_types.map do |element_type|
399
+ position = BlockParameterBinder.new(expected_param_types: [element_type]).bind_onto(block, scope)
400
+ [position.type_of(key), receiver_kind(position.type_of(node.receiver))]
401
+ end
402
+ fresh_by_position(typed)
403
+ rescue StandardError
404
+ nil
405
+ end
406
+
407
+ def fresh_by_position(typed)
408
+ values = []
409
+ typed.each_with_index.map do |(key_type, receiver_kind), index|
410
+ return typed.map { false } unless key_type.is_a?(Type::Constant)
411
+
412
+ value = key_type.value
413
+ unseen = values.none? { |earlier| earlier.eql?(value) }
414
+ values << value
415
+ index.zero? || (unseen && distinguishable?(values, receiver_kind))
416
+ end
417
+ end
418
+
419
+ # A core `Hash` tells apart any two `Symbol`, `String` or `Integer` keys `eql?` does, and a core `Array` any two
420
+ # non-negative indices. Any other receiver's `[]` may normalise a key (`with_indifferent_access`, a
421
+ # case-insensitive hash), so only non-negative `Integer`s, or keys all of one class, `Symbol` or `String`, pass.
422
+ def distinguishable?(values, receiver_kind)
423
+ case receiver_kind
424
+ when :hash then values.all? { |value| HASH_KEY_CLASSES.any? { |key_class| value.is_a?(key_class) } }
425
+ when :array then values.all? { |value| value.is_a?(Integer) && !value.negative? }
426
+ else
427
+ values.all? { |value| value.is_a?(Integer) && !value.negative? } ||
428
+ values.all?(Symbol) || values.all?(String)
429
+ end
430
+ end
431
+
432
+ def receiver_kind(type)
433
+ case type
434
+ when Type::HashShape then :hash
435
+ when Type::Tuple then :array
436
+ when Type::Nominal then { "Hash" => :hash, "Array" => :array }[type.class_name]
437
+ end
438
+ end
439
+
440
+ def sole_key(node)
441
+ arguments = node.arguments&.arguments
442
+ return nil unless arguments&.size == 1 && node.block.nil?
443
+
444
+ key = arguments.first
445
+ key.is_a?(Prism::SplatNode) || key.is_a?(Prism::KeywordHashNode) ? nil : key
446
+ end
447
+
448
+ # True when `key` reads no variable the body can change between positions: every local it reads is one of
449
+ # the block's `parameters` or a captured local, which the body neither writes nor mutates in place, and it
450
+ # contains no instance-, class- or global-variable read, no write, and no block or lambda.
451
+ def fixed_key?(key, parameters, written_locals, stores)
452
+ Source::NodeWalker.each(key) do |node|
453
+ return false if KEY_UNFIXED_NODES.include?(node.class)
454
+ next unless node.is_a?(Prism::LocalVariableReadNode)
455
+ return false if written_locals.include?(node.name)
456
+ return false unless node.depth.zero? ? parameters.include?(node.name) : !stores.key?(node.name)
457
+ end
458
+ true
459
+ end
460
+ end
461
+ end
462
+ end
463
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The constructs whose body `return` leaves only that body, not the enclosing method. A `return` anywhere else —
8
+ # an ordinary block included — exits the method (control-flow-analysis.md § "Non-local exits").
9
+ # `StatementEvaluator`'s return sink and `DefReturnTyper`'s return collection both stop here; each used to carry
10
+ # its own list, and sig-gen's treated every block as a barrier (issue #1382).
11
+ module ReturnBarrier
12
+ # A nested `def` and a `->` literal: the body is its own method or lambda.
13
+ NODES = Set[Prism::DefNode, Prism::LambdaNode].freeze
14
+
15
+ # The block calls whose body `return` leaves only the block: the method a `define_method` /
16
+ # `define_singleton_method` block defines, on whatever receiver (`klass.define_method(:m) { … }`), and
17
+ # `Kernel#lambda` — called bare, on `self`, or on `Kernel`. Each also counts through `send`
18
+ # (`klass.send(:define_method, :m) { … }`). A `lambda` on any other receiver is some other method.
19
+ DEFINE_CALLS = %i[define_method define_singleton_method].to_set.freeze
20
+ SEND_CALLS = %i[send public_send __send__].to_set.freeze
21
+ private_constant :DEFINE_CALLS, :SEND_CALLS
22
+
23
+ module_function
24
+
25
+ # True when `node` is a nested `def` or a lambda literal.
26
+ def node?(node)
27
+ NODES.include?(node.class)
28
+ end
29
+
30
+ # True when the block `call_node` carries is a return barrier.
31
+ def block_call?(call_node)
32
+ name = call_node.name
33
+ if SEND_CALLS.include?(name)
34
+ sent = call_node.arguments&.arguments&.first
35
+ return false unless sent.is_a?(Prism::SymbolNode)
36
+
37
+ name = sent.unescaped.to_sym
38
+ end
39
+ DEFINE_CALLS.include?(name) || (name == :lambda && kernel_receiver?(call_node.receiver))
40
+ end
41
+
42
+ # True when a `lambda` call on `receiver` reaches `Kernel#lambda`: no receiver, `self`, `Kernel` or `::Kernel`.
43
+ def kernel_receiver?(receiver)
44
+ case receiver
45
+ when nil, Prism::SelfNode then true
46
+ when Prism::ConstantReadNode then receiver.name == :Kernel
47
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
48
+ else false
49
+ end
50
+ end
51
+ private_class_method :kernel_receiver?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "refinement_mutation"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The in-place mutators whose ARGUMENTS do not describe what they store — the other half of what
9
+ # {MutationWidening} joins. A block computes the value (`map!`, `transform_values!`, a block-form `fill`), the
10
+ # argument is a collection whose contents {ContentJoin} does not read for that name (`merge!`, `Hash#replace`),
11
+ # or the stored values are the receiver's own elements taken apart (`flatten!`).
12
+ #
13
+ # The widening kept the SEED's types for such a site, because nothing it joins describes the new values:
14
+ # `a = [1]; a.map!(&:to_s)` read `Array[Integer]`, and `a.first.upcase` drew a false
15
+ # `undefined method 'upcase' for Integer`. The seam cannot see the values, so the answer at each position the
16
+ # mutator writes is gradual — {REPLACED} or {JOINED}, depending on whether the old values can survive it.
17
+ module RewriteMutation
18
+ # Mutators that rewrite EVERY value at the listed type-argument positions, keyed by carrier class: nothing the
19
+ # seed held there is known to survive, so the position becomes `Dynamic[top]` outright. Keeping the seed beside
20
+ # the gradual arm would leave a closed claim about values that are gone — `a = [1, 2]; a.map!(&:to_s)` read
21
+ # `Array[1 | 2 | Dynamic[top]]`, which a hand-written `-> Array[String]` rejects on correct code, and the kept
22
+ # `1` gated a later `a << "x"` out of the re-join as foreign evidence. `Array#replace` is listed although its
23
+ # argument IS its new content: {MutationWidening#join_added_elements} still joins that evidence after the
24
+ # replacement, and the seed it would otherwise keep beside it is gone.
25
+ REPLACED = Ractor.make_shareable({
26
+ "Array" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] },
27
+ "Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] }
28
+ })
29
+
30
+ # Mutators that store values nothing describes BESIDE values they may keep: `fill(x, start)` leaves the slots
31
+ # before `start`, and `merge!` / `update` leave every key the argument lacks. The listed positions join
32
+ # `Dynamic[top]` and keep the seed's own arms.
33
+ #
34
+ # `fill` is listed although its no-block form's argument IS its value: the widening cannot see the block, and
35
+ # the arm costs the argument form nothing, since {MutationWidening#join_added_elements} already floors every
36
+ # straight-line store it joins. `merge!` on a `Hash.new(0)` counter opens the value side the default proved
37
+ # (`Dynamic[top] | Integer`), since the seam cannot tell a merging block or a foreign argument from neither.
38
+ JOINED = Ractor.make_shareable({
39
+ "Array" => { fill: [0] },
40
+ "Hash" => { merge!: [0, 1], update: [0, 1] }
41
+ })
42
+
43
+ module_function
44
+
45
+ # True when `method_name` is a rewriter for some carrier class.
46
+ def rewriter?(method_name)
47
+ [REPLACED, JOINED].any? { |table| table.each_value.any? { |names| names.key?(method_name) } }
48
+ end
49
+
50
+ # `carrier` with each position `method_name` rewrites made gradual — replaced for a {REPLACED} name, joined
51
+ # for a {JOINED} one; any carrier other than an `Array` / `Hash` nominal, or a name neither table lists for its
52
+ # class, is returned untouched. Idempotent, so a path that reaches it twice answers the same.
53
+ def arm(carrier, method_name)
54
+ return carrier unless carrier.is_a?(Type::Nominal) && !carrier.type_args.empty?
55
+
56
+ replaced = REPLACED.dig(carrier.class_name, method_name) || []
57
+ joined = JOINED.dig(carrier.class_name, method_name) || []
58
+ return carrier if replaced.empty? && joined.empty?
59
+
60
+ args = carrier.type_args.each_with_index.map do |arg, i|
61
+ if replaced.include?(i) then Type::Combinator.untyped
62
+ elsif joined.include?(i) then Type::Combinator.union(arg, Type::Combinator.untyped)
63
+ else arg
64
+ end
65
+ end
66
+ Type::Combinator.nominal_of(carrier.class_name, type_args: args)
67
+ end
68
+
69
+ # {.arm} through a `Union` (member by member) and an empty-witness refinement (its base, keeping the witness
70
+ # only when the mutator cannot empty the receiver — {RefinementMutation.preserves_witness?}). For a carrier
71
+ # the ADR-56 slice-C join rebuilt from its seed, which knows the adders' content but not the rewrite.
72
+ def arm_through(type, method_name)
73
+ case type
74
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| arm_through(member, method_name) })
75
+ when Type::Difference then arm_refinement(type, method_name)
76
+ else arm(type, method_name)
77
+ end
78
+ end
79
+
80
+ def arm_refinement(difference, method_name)
81
+ base = difference.base
82
+ return difference unless difference.removes_empty_witness? && base.is_a?(Type::Nominal)
83
+
84
+ armed = arm(base, method_name)
85
+ return difference if armed.equal?(base)
86
+
87
+ kept = RefinementMutation.preserves_witness?(base.class_name, method_name)
88
+ kept ? Type::Combinator.difference(armed, difference.removed) : armed
89
+ end
90
+
91
+ # `values`, or `:keep` when `method_name` rewrites the carrier's value position (an `Array`'s element, a
92
+ # `Hash`'s value) — the pre-state widening's pinning mode.
93
+ #
94
+ # Erasing the pinning there buys nothing and costs the #561 guarantee. The erasure exists to stop a stale
95
+ # constant fold (issue #560), and a union carrying `Dynamic` never folds; but the erased form is a CLOSED
96
+ # class a {JOINED} arm then sits beside, and one a hand-written signature pinning the seed rejects — haml's
97
+ # `-> Array[:multi]` accepts `Array[:multi | Dynamic[top]]`, not `Array[Dynamic[top] | Symbol]`. A {REPLACED}
98
+ # position discards the pinning either way.
99
+ #
100
+ # `type` is the widening's pre-state; a `Union` answers `values` and each member asks again as it widens.
101
+ def values_mode(type, method_name, values)
102
+ class_name = value_carrier_class(type)
103
+ return values if class_name.nil?
104
+
105
+ value_position = class_name == "Hash" ? 1 : 0
106
+ rewritten = [REPLACED, JOINED].any? { |table| table.dig(class_name, method_name)&.include?(value_position) }
107
+ rewritten ? :keep : values
108
+ end
109
+
110
+ def value_carrier_class(type)
111
+ case type
112
+ when Type::Tuple then "Array"
113
+ when Type::HashShape then "Hash"
114
+ when Type::Nominal then type.class_name
115
+ end
116
+ end
117
+ private_class_method :value_carrier_class, :arm_refinement
118
+ end
119
+ end
120
+ end