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,447 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../type"
7
+ require_relative "external_ancestor_resolution"
8
+ require_relative "fresh_frame_blocks"
9
+ require_relative "project_method_ownership"
10
+ require_relative "stored_block_call"
11
+
12
+ module Rigor
13
+ module Inference
14
+ # Issue #1429 — where a guard's narrowing of a global or constant stops holding. A guard narrows `$stdout` or
15
+ # `STDOUT` on its edge ({Narrowing}), but any Ruby code that runs between the guard and a read may rebind the
16
+ # global (`$stdout = StringIO.new`) or the constant (`const_set`), and the analysis cannot see that code. So the
17
+ # statement evaluator restores each narrowed name to the union of its pre-guard binding and its narrowed one
18
+ # ({Scope#forget_guard_narrowings}) wherever code this module counts may run:
19
+ #
20
+ # - a call that may run project, gem or unresolved code ({.call_runs_foreign_code?}): a method the project
21
+ # defines on the receiver's class or its ancestry, one whose signature is owned outside Ruby core and the
22
+ # standard library, a core method on a project or gem receiver (`Enumerable#map` runs the class's `each`),
23
+ # an unresolved callee (a `Dynamic` receiver, a name no signature declares), a call that runs code by name
24
+ # (`send`, `instance_eval`, `eval`, `require`, `load`), a call on a code object (`Proc`, `Method`,
25
+ # `Enumerator`, `Fiber`, `Thread`, a delegator), and a block-pass argument (`&blk`);
26
+ # - a literal block whose body may do either, or writes a global or constant itself;
27
+ # - `yield` and `super`, which run code the method does not show.
28
+ #
29
+ # A core or standard-library method on a core or standard-library receiver keeps the narrowing (`$sep.strip`,
30
+ # `$stdout.flush`, `File.read(path)`), and so does a method `Kernel`, `Object` or `BasicObject` owns on any
31
+ # receiver (`puts`, `format`, `obj.frozen?`), so `if $sep; $sep.strip; $sep.length; end` keeps `$sep` non-nil.
32
+ #
33
+ # The accepted gap is implicit conversion: a core method that calls back into a project method the program
34
+ # does not spell (`puts obj` runs `obj.to_s`, `hash[obj]` runs `obj.hash`, `a.sort` runs `<=>`) is read as the
35
+ # core method alone.
36
+ module GuardRebinding
37
+ # Calls that run code chosen by name or by a String.
38
+ CODE_RUNNING_NAMES = Set[
39
+ :send, :__send__, :public_send, :eval, :require, :require_relative, :load,
40
+ # These rebind a constant on any receiver (`Object.const_set(:SEP, nil)`).
41
+ :const_set, :remove_const
42
+ ].freeze
43
+ # These run their literal block, which the scan reads as any block, or a String of code, which it cannot read.
44
+ BLOCK_OR_CODE_NAMES = Set[
45
+ :instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec
46
+ ].freeze
47
+ # Methods `Kernel`, `Object` or `BasicObject` own that call another method of the receiver, which a project class
48
+ # may define: `r != 1` runs `r == 1`.
49
+ UNIVERSAL_DELEGATES = { :!= => :==, :!~ => :=~, :=== => :==, :respond_to? => :respond_to_missing? }.freeze
50
+ # Receivers whose methods run code the receiver holds: a block, a method, a generator or a delegate.
51
+ CODE_OBJECT_CLASSES = Set[
52
+ "Proc", "Method", "UnboundMethod", "Binding", "Enumerator", "Enumerator::Lazy", "Enumerator::Chain",
53
+ "Enumerator::Yielder", "Fiber", "Thread", "Delegator", "SimpleDelegator"
54
+ ].freeze
55
+ # The owners whose methods a project object answers the same way any object does.
56
+ UNIVERSAL_OWNERS = Set["Kernel", "Object", "BasicObject"].freeze
57
+ # The nodes that write a global or a constant.
58
+ WRITE_NODES = [
59
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
60
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode,
61
+ Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
62
+ Prism::ConstantOperatorWriteNode, Prism::ConstantTargetNode, Prism::ConstantPathWriteNode,
63
+ Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantPathOperatorWriteNode,
64
+ Prism::ConstantPathTargetNode
65
+ ].freeze
66
+ # Code that runs code the method does not show.
67
+ FOREIGN_NODES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
68
+ REBINDING_NODES = (WRITE_NODES + FOREIGN_NODES).to_set.freeze
69
+ # Nodes that call methods the syntax does not spell as a `CallNode`: an operator write calls its operator (`r +=
70
+ # 1` calls `r.+`), an attribute or index compound write calls the reader and the writer (`r.val ||= 1`, `r[0] +=
71
+ # 1`), and a `for` loop calls `each` on its collection.
72
+ IMPLICIT_CALL_NODES = Set[
73
+ Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
74
+ Prism::ClassVariableOperatorWriteNode, Prism::CallOperatorWriteNode, Prism::CallOrWriteNode,
75
+ Prism::CallAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
76
+ Prism::ForNode
77
+ ].freeze
78
+ VARIABLE_OPERATOR_WRITES = {
79
+ Prism::LocalVariableOperatorWriteNode => :local, Prism::InstanceVariableOperatorWriteNode => :ivar,
80
+ Prism::ClassVariableOperatorWriteNode => :cvar
81
+ }.freeze
82
+ # Calls that keep their literal block to run at a later event, after the method returns or between any two of
83
+ # its statements: an exit handler, a signal handler, a global-assignment hook, a trace hook and a finalizer.
84
+ # `nil` keys the Kernel function's spellings (`at_exit`, `self.trap`, `Kernel.at_exit`); a constant keys its
85
+ # singleton methods.
86
+ DEFERRED_BLOCK_CALLS = {
87
+ nil => %i[at_exit trap trace_var], Signal: %i[trap], TracePoint: %i[new trace],
88
+ ObjectSpace: %i[define_finalizer]
89
+ }.transform_values(&:freeze).freeze
90
+ private_constant :CODE_RUNNING_NAMES, :BLOCK_OR_CODE_NAMES, :UNIVERSAL_DELEGATES, :CODE_OBJECT_CLASSES,
91
+ :UNIVERSAL_OWNERS, :WRITE_NODES, :FOREIGN_NODES,
92
+ :REBINDING_NODES, :IMPLICIT_CALL_NODES, :VARIABLE_OPERATOR_WRITES, :DEFERRED_BLOCK_CALLS
93
+
94
+ module_function
95
+
96
+ # True when `call_node`, a statement's own call, may rebind a global or constant once its operands ran: its
97
+ # method may run foreign code, or its literal block may.
98
+ def call_may_rebind?(call_node, scope)
99
+ return true unless call_node.is_a?(Prism::CallNode)
100
+ return true if call_runs_foreign_code?(call_node, scope)
101
+
102
+ block = call_node.block
103
+ block.is_a?(Prism::BlockNode) && may_rebind?(block, ScanScope.block_parameter_scope(call_node, block, scope))
104
+ end
105
+
106
+ # True when the receiver chain or the arguments of `call_node` may rebind one; Ruby runs them before the call.
107
+ def operands_may_rebind?(call_node, scope)
108
+ may_rebind?(call_node.receiver, scope) || may_rebind?(call_node.arguments, scope) ||
109
+ (call_node.block.is_a?(Prism::BlockArgumentNode) && may_rebind?(call_node.block, scope))
110
+ end
111
+
112
+ # True when running `node` may rebind one: it writes a global or constant, yields, calls `super`, or holds a
113
+ # call, spelled or implicit ({.implicit_call_may_rebind?}), whose method may run foreign code. A `def` and a
114
+ # lambda literal run nothing where they are written. Each node is visited once: a call's literal block is
115
+ # reached as one of its children, so a nested block chain costs its size, not its depth's power.
116
+ #
117
+ # A local the scanned code writes has no binding in `scope` yet, so the scan reads it as the code writes it
118
+ # ({ScanScope.with_scanned_locals}): `copy = $sep; copy.length` is a `String` call. A literal block's parameters
119
+ # read as the method's signature yields them ({ScanScope.block_parameter_scope}).
120
+ def may_rebind?(node, scope)
121
+ return false unless node.is_a?(Prism::Node)
122
+
123
+ scan(node, ScanScope.with_scanned_locals(node, scope))
124
+ end
125
+
126
+ def scan(node, scope)
127
+ return false unless node.is_a?(Prism::Node)
128
+ return true if REBINDING_NODES.include?(node.class)
129
+ return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
130
+ return scan_call(node, scope) if node.is_a?(Prism::CallNode)
131
+ return true if IMPLICIT_CALL_NODES.include?(node.class) && implicit_call_may_rebind?(node, scope)
132
+
133
+ found = false
134
+ node.rigor_each_child { |child| found ||= scan(child, scope) }
135
+ found
136
+ end
137
+
138
+ def scan_call(node, scope)
139
+ return true if call_runs_foreign_code?(node, scope)
140
+ return true if scan(node.receiver, scope) || scan(node.arguments, scope)
141
+
142
+ block = node.block
143
+ return scan(block, scope) unless block.is_a?(Prism::BlockNode)
144
+
145
+ scan(block, ScanScope.block_scope(node, block, scope))
146
+ end
147
+
148
+ # True when `node` is a compound write or `for` loop that calls a method its syntax does not spell.
149
+ def implicit_call_node?(node)
150
+ IMPLICIT_CALL_NODES.include?(node.class)
151
+ end
152
+
153
+ # True when the method a compound write or a `for` loop calls without spelling it may run foreign code
154
+ # ({IMPLICIT_CALL_NODES}). The operator of an attribute or index operator write runs on the value the reader
155
+ # returns, typed through the dispatcher; one it cannot type counts.
156
+ def implicit_call_may_rebind?(node, scope)
157
+ kind = VARIABLE_OPERATOR_WRITES[node.class]
158
+ return type_method_foreign?(variable_type(kind, node.name, scope), node.binary_operator, scope) if kind
159
+ return type_method_foreign?(scope.type_of(node.collection), :each, scope) if node.is_a?(Prism::ForNode)
160
+
161
+ compound_write_foreign?(node, scope)
162
+ rescue StandardError
163
+ true
164
+ end
165
+
166
+ def compound_write_foreign?(node, scope)
167
+ receiver_type = compound_receiver_type(node, scope)
168
+ reader, writer = compound_accessors(node)
169
+ return true if [reader, writer].any? { |name| type_method_foreign?(receiver_type, name, scope) }
170
+ return false unless node.respond_to?(:binary_operator)
171
+
172
+ read = compound_read_type(node, receiver_type, reader, scope)
173
+ read.nil? || type_method_foreign?(read, node.binary_operator, scope)
174
+ end
175
+
176
+ def compound_receiver_type(node, scope)
177
+ return scope.type_of(node.receiver) if node.receiver
178
+
179
+ scope.self_type || Type::Combinator.nominal_of("Object")
180
+ end
181
+
182
+ def compound_accessors(node)
183
+ return %i[[] []=] if node.respond_to?(:arguments) && !node.respond_to?(:read_name)
184
+
185
+ [node.read_name, node.write_name]
186
+ end
187
+
188
+ def compound_read_type(node, receiver_type, reader, scope)
189
+ arguments = node.respond_to?(:arguments) && node.arguments ? node.arguments.arguments : []
190
+ MethodDispatcher.dispatch(receiver_type: receiver_type, method_name: reader,
191
+ arg_types: arguments.map { |argument| scope.type_of(argument) },
192
+ environment: scope.environment, scope: scope)
193
+ end
194
+
195
+ def variable_type(kind, name, scope)
196
+ case kind
197
+ when :local then scope.local(name)
198
+ when :ivar then scope.ivar(name)
199
+ else scope.cvar(name)
200
+ end || Type::Combinator.untyped
201
+ end
202
+
203
+ # True when `method_name` on a value of `type` may run foreign code ({.foreign_target?}).
204
+ def type_method_foreign?(type, method_name, scope)
205
+ targets = ProjectMethodOwnership.targets(type)
206
+ return true if targets.nil? || targets.empty?
207
+
208
+ targets.any? { |class_name, kind| foreign_target?(class_name, method_name, kind, scope) }
209
+ end
210
+
211
+ # The scope the body of `block_node` (a block, a lambda literal or an `END { }` body) enters with: `scope` with
212
+ # its guard narrowings restored where the body may run after code that rebinds them. A lambda, an `END` body,
213
+ # a block the call keeps to run later (`proc`, `define_method`) or at a later event ({DEFERRED_BLOCK_CALLS}),
214
+ # the root block of a thread or fiber, the block of a call that itself may run foreign code (`with_retry {
215
+ # $g.length }` runs after the helper's body), and a body that may rebind one itself (a later run reads what an
216
+ # earlier one wrote). A block with no owning call is left alone.
217
+ def block_entry(scope, block_node, call_node)
218
+ return scope unless scope.guard_narrowed?
219
+ return scope.forget_guard_narrowings if block_node.is_a?(Prism::LambdaNode) ||
220
+ block_node.is_a?(Prism::PostExecutionNode)
221
+ return scope unless call_node.is_a?(Prism::CallNode)
222
+
223
+ if StoredBlockCall.stores_block?(call_node) || deferred_block_call?(call_node) ||
224
+ FreshFrameBlocks.fresh_entry?(call_node, scope) || call_runs_foreign_code?(call_node, scope) ||
225
+ may_rebind?(block_node, ScanScope.block_parameter_scope(call_node, block_node, scope))
226
+ return scope.forget_guard_narrowings
227
+ end
228
+
229
+ scope
230
+ end
231
+
232
+ # True when `call_node` is one of {DEFERRED_BLOCK_CALLS}, spelled on its literal receiver.
233
+ def deferred_block_call?(call_node)
234
+ receiver = call_node.receiver
235
+ key = StoredBlockCall.kernel_spelled?(receiver) ? nil : StoredBlockCall.root_constant_name(receiver)
236
+ names = DEFERRED_BLOCK_CALLS[key]
237
+ !names.nil? && names.include?(call_node.name)
238
+ end
239
+
240
+ # True when the method `call_node` calls may run code other than Ruby core and the standard library (see the
241
+ # module comment). Its operands and literal block are not read here.
242
+ def call_runs_foreign_code?(call_node, scope)
243
+ return true if call_node.block.is_a?(Prism::BlockArgumentNode)
244
+ return true if CODE_RUNNING_NAMES.include?(call_node.name)
245
+ return true if BLOCK_OR_CODE_NAMES.include?(call_node.name) &&
246
+ !(call_node.block.is_a?(Prism::BlockNode) && call_node.arguments.nil?)
247
+
248
+ targets = receiver_targets(call_node, scope)
249
+ return true if targets.nil? || targets.empty?
250
+
251
+ targets.any? { |class_name, kind| foreign_target?(class_name, call_node.name, kind, scope) }
252
+ rescue StandardError
253
+ true
254
+ end
255
+
256
+ # The `[class_name, kind]` pairs the call dispatches on ({ProjectMethodOwnership.targets}); an implicit or
257
+ # `self.` receiver reads the scope's `self`, and the top level's `main` is an `Object`.
258
+ def receiver_targets(call_node, scope)
259
+ receiver = call_node.receiver
260
+ if receiver.nil? || receiver.is_a?(Prism::SelfNode)
261
+ self_type = scope.self_type
262
+ return [["Object", :instance]] if self_type.nil?
263
+
264
+ return ProjectMethodOwnership.targets(self_type)
265
+ end
266
+
267
+ ProjectMethodOwnership.targets(scope.type_of(receiver))
268
+ end
269
+
270
+ def foreign_target?(class_name, method_name, kind, scope)
271
+ return true if CODE_OBJECT_CLASSES.include?(class_name)
272
+ return true if ProjectMethodOwnership.defines?(class_name, method_name, kind, scope)
273
+
274
+ owner = method_owner(class_name, method_name, kind, scope)
275
+ return true if owner.nil?
276
+ return universal_delegate_foreign?(class_name, method_name, kind, scope) if UNIVERSAL_OWNERS.include?(owner)
277
+
278
+ loader = scope.environment.rbs_loader
279
+ return true if loader.nil?
280
+
281
+ !(loader.core_or_stdlib_class?(owner) && loader.core_or_stdlib_class?(class_name))
282
+ end
283
+
284
+ # A universal method is foreign only when it calls another method of the receiver the project defines
285
+ # ({UNIVERSAL_DELEGATES}).
286
+ def universal_delegate_foreign?(class_name, method_name, kind, scope)
287
+ delegate = UNIVERSAL_DELEGATES[method_name]
288
+ !delegate.nil? && ProjectMethodOwnership.defines?(class_name, delegate, kind, scope)
289
+ end
290
+
291
+ # The class or module whose signature answers `method_name` on `class_name`: its own RBS, an RBS ancestor of a
292
+ # project class ({ExternalAncestorResolution.resolve}), or `Object`'s for an instance method nothing earlier
293
+ # declares (a project class with no signature calling `puts`). nil when none does.
294
+ def method_owner(class_name, method_name, kind, scope)
295
+ definition = ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope)
296
+ if definition.nil? && kind == :instance
297
+ definition = ExternalAncestorResolution.resolve(class_name, method_name, :instance, scope: scope)&.first
298
+ definition ||= ExternalAncestorResolution.method_definition("Object", method_name, :instance, scope: scope)
299
+ end
300
+ owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
301
+ owner&.to_s&.delete_prefix("::")
302
+ end
303
+ private_class_method :scan, :scan_call, :receiver_targets, :foreign_target?, :universal_delegate_foreign?,
304
+ :method_owner, :deferred_block_call?,
305
+ :compound_write_foreign?,
306
+ :compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type,
307
+ :type_method_foreign?
308
+ end
309
+
310
+ module GuardRebinding
311
+ # The scope {GuardRebinding.may_rebind?} types a scanned node's receivers in: the locals the scanned code writes
312
+ # and a literal block's parameters, which the scope before the code does not bind.
313
+ module ScanScope
314
+ LOCAL_SCAN_BARRIERS = [Prism::DefNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode].freeze
315
+ private_constant :LOCAL_SCAN_BARRIERS
316
+
317
+ module_function
318
+
319
+ # `scope` with each local `node` writes read as the union of what it held and each value the code writes it,
320
+ # in source order, so a later write reads an earlier one. A local `scope` already binds keeps its binding in
321
+ # the union: the scanned code may run before or after its own write (a loop body, a block, a rescue clause).
322
+ # A nested `def`, lambda, class or module body is not read.
323
+ def with_scanned_locals(node, scope)
324
+ writes = []
325
+ collect_local_writes(node, writes)
326
+ writes.reduce(scope) do |acc, write|
327
+ written = acc.type_of(write.value)
328
+ existing = acc.local(write.name)
329
+ acc.with_local(write.name, existing ? Type::Combinator.union(existing, written) : written)
330
+ end
331
+ rescue StandardError
332
+ scope
333
+ end
334
+
335
+ def collect_local_writes(node, writes)
336
+ return unless node.is_a?(Prism::Node)
337
+ return if LOCAL_SCAN_BARRIERS.any? { |barrier| node.is_a?(barrier) }
338
+
339
+ writes << node if node.is_a?(Prism::LocalVariableWriteNode)
340
+ node.rigor_each_child { |child| collect_local_writes(child, writes) }
341
+ end
342
+
343
+ # The scope the body of `call_node`'s literal `block` is scanned in, from `scope`, the enclosing scan's: the
344
+ # block's parameters bound ({.block_parameter_scope}), and the locals the body writes typed again from them
345
+ # ({.with_scanned_locals}), since the enclosing scan typed those writes before the parameters were bound. A
346
+ # block without parameters keeps `scope`.
347
+ def block_scope(call_node, block, scope)
348
+ bound = block_parameter_scope(call_node, block, scope)
349
+ bound.equal?(scope) ? scope : with_scanned_locals(block, bound)
350
+ end
351
+
352
+ # `scope` with every local the block's parameter list names bound, so none reads a binding of the same name
353
+ # from outside the block. A plain required parameter reads what the method yields at its position in
354
+ # `requireds` (`MethodDispatcher.expected_block_param_types`); every other name, a destructured, optional,
355
+ # splat, post, keyword, block or `;`-block-local one, reads `Dynamic[top]`, so a call on it counts as an
356
+ # unresolved callee. `scope` itself when the body reads no parameter where the scan types it
357
+ # ({.reads_parameters?}): the bindings would change nothing.
358
+ def block_parameter_scope(call_node, block, scope)
359
+ parameters = block.parameters
360
+ return scope unless parameters.is_a?(Prism::BlockParametersNode)
361
+
362
+ names = []
363
+ collect_parameter_names(parameters, names)
364
+ return scope if names.empty? || !reads_parameters?(block.body, names)
365
+
366
+ bindings = names.to_h { |name| [name, Type::Combinator.untyped] }
367
+ requireds = parameters.parameters&.requireds || []
368
+ unless requireds.none?(Prism::RequiredParameterNode)
369
+ yielded = yielded_types(call_node, scope)
370
+ requireds.each_with_index do |parameter, index|
371
+ next unless parameter.is_a?(Prism::RequiredParameterNode)
372
+
373
+ bindings[parameter.name] = yielded[index] || Type::Combinator.untyped
374
+ end
375
+ end
376
+ bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
377
+ end
378
+
379
+ # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read.
380
+ def yielded_types(call_node, scope)
381
+ receiver = call_node.receiver ? scope.type_of(call_node.receiver) : scope.self_type
382
+ arguments = call_node.arguments&.arguments || []
383
+ MethodDispatcher.expected_block_param_types(
384
+ receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
385
+ arg_types: arguments.map { |argument| scope.type_of(argument) }
386
+ )
387
+ rescue StandardError
388
+ []
389
+ end
390
+
391
+ # The names a block's parameter list declares: every kind of parameter, a destructured one's parts included,
392
+ # and a `;`-block-local. A default value is not read.
393
+ def collect_parameter_names(node, names)
394
+ case node
395
+ when Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
396
+ Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, Prism::KeywordRestParameterNode,
397
+ Prism::BlockParameterNode, Prism::BlockLocalVariableNode
398
+ names << node.name if node.name
399
+ when Prism::Node
400
+ node.rigor_each_child { |child| collect_parameter_names(child, names) }
401
+ end
402
+ end
403
+
404
+ # True when the block body `body` reads one of `names` where the scan types it. A read the scan never types is
405
+ # a bare argument of a statement that calls a method on `self` without a literal block (`puts i`): the scan
406
+ # reads such a call by its receiver and name alone, and types none of its arguments. Any other read counts,
407
+ # and so does every read under a body that is not a plain statement list (a `rescue` in a `do` block).
408
+ def reads_parameters?(body, names)
409
+ return false if body.nil?
410
+ return names_read?(body, names) unless body.is_a?(Prism::StatementsNode)
411
+
412
+ body.body.any? { |statement| statement_reads?(statement, names) }
413
+ end
414
+
415
+ def statement_reads?(statement, names)
416
+ return names_read?(statement, names) unless self_call_without_block?(statement)
417
+
418
+ arguments = statement.arguments&.arguments || []
419
+ arguments.any? { |argument| !argument.is_a?(Prism::LocalVariableReadNode) && names_read?(argument, names) } ||
420
+ names_read?(statement.block, names)
421
+ end
422
+
423
+ def self_call_without_block?(node)
424
+ node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
425
+ !node.block.is_a?(Prism::BlockNode)
426
+ end
427
+
428
+ # True when `node` reads one of `names`: a read, or an operator write, which reads the local before it writes.
429
+ def names_read?(node, names)
430
+ case node
431
+ when Prism::LocalVariableReadNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
432
+ Prism::LocalVariableAndWriteNode
433
+ return true if names.include?(node.name)
434
+ end
435
+ return false unless node.is_a?(Prism::Node)
436
+
437
+ found = false
438
+ node.rigor_each_child { |child| found ||= names_read?(child, names) }
439
+ found
440
+ end
441
+
442
+ private_class_method :collect_local_writes, :yielded_types, :collect_parameter_names, :reads_parameters?,
443
+ :statement_reads?, :self_call_without_block?, :names_read?
444
+ end
445
+ end
446
+ end
447
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The lookup half of {MutationWidening}'s Hash seam.
8
+ #
9
+ # `Hash#default=`, `Hash#default_proc=` and `Hash#compare_by_identity` mutate the receiver without storing,
10
+ # removing or rewriting a pair. What they change is what a READ of the pairs answers, and a closed `HashShape`
11
+ # makes a claim about exactly that: a key outside `pairs` is provably missing, so `[]` reads `Constant[nil]`.
12
+ # `counts = { a: 1 }; counts.default = 0` kept that claim, and `counts[:b] + 1` drew an error-level
13
+ # `call.undefined-method` on code Ruby runs and prints 1 for.
14
+ #
15
+ # They are kept out of {MutationWidening::HASH_MUTATORS} on purpose. That table answers "the pair set changed",
16
+ # and its other readers act on that answer: the widening there replaces the shape with a `Hash[K, V]` nominal,
17
+ # which costs every present key's value for a mutation that left every pair where it was, and the `non-empty-hash`
18
+ # witness reads it as a store or a removal. This module answers the narrower question instead, so the present keys
19
+ # keep their values. The effect classifier asks only whether the receiver was written, which each of these three
20
+ # does, so it reads the two tables as one ({Effects::MutationClassifier::HASH_MUTATORS}).
21
+ module HashLookupMutation
22
+ MUTATORS = %i[default= default_proc= compare_by_identity].to_set.freeze
23
+
24
+ module_function
25
+
26
+ # The binding `shape` holds after `method_name`, or `nil` when the call changes no read of it.
27
+ #
28
+ # `default=` / `default_proc=` open the shape: a key outside `pairs` now reads the default, which the shape
29
+ # cannot state, so it reads `untyped` — the answer an open shape gives an undeclared key. A present key still
30
+ # reads its own value. A default proc can also store (`proc { |h, k| h[k] = [] }`), so a read may add a pair,
31
+ # which is what the open policy says too: `size` / `keys` / `key?` stop folding.
32
+ #
33
+ # `compare_by_identity` changes the key a read has to pass, not the answer to a miss, so it leaves a closed
34
+ # shape closed. What it can falsify is a HIT: see {.identity_sensitive?}.
35
+ def widen_shape(shape, method_name)
36
+ case method_name
37
+ when :default=, :default_proc= then open_shape(shape)
38
+ when :compare_by_identity then identity_widening(shape) if identity_sensitive?(shape)
39
+ end
40
+ end
41
+
42
+ def open_shape(shape)
43
+ return nil if shape.open?
44
+
45
+ Type::HashShape.new(
46
+ shape.pairs,
47
+ required_keys: shape.required_keys,
48
+ optional_keys: shape.optional_keys,
49
+ read_only_keys: shape.read_only_keys,
50
+ extra_keys: :open
51
+ )
52
+ end
53
+
54
+ # True when some key of `shape` stops finding its pair under `compare_by_identity`. A read spells its key as a
55
+ # new literal, and after the switch it hits only when that literal is the very object the hash stored. For a
56
+ # Symbol, `true` / `false` / `nil` and an Integer in the fixnum range it always is. A String key is the frozen
57
+ # copy the literal stored, which a later `"k"` is not unless `# frozen_string_literal: true` interns both — so
58
+ # `s = { "k" => 1 }; s.compare_by_identity; s["k"]` is `nil` in one file and `1` in the next — and a bignum or
59
+ # a heap Float is a fresh object per evaluation.
60
+ #
61
+ # A shape whose every key is identity-stable reads exactly as before; any other is widened by
62
+ # {.identity_widening}.
63
+ def identity_sensitive?(shape)
64
+ shape.pairs.each_key.any? { |key| !identity_stable_key?(key) }
65
+ end
66
+
67
+ # An identity-sensitive shape widened as a storing mutator widens it, its values unpinned, with a `Dynamic[top]`
68
+ # arm on the value side. A read of a String key may find its pair or miss it and answer `nil`, or a default set
69
+ # before or after, and the nominal cannot say which: the arm keeps that read from folding (`s["k"] == 1`) or
70
+ # drawing `possible nil receiver` in the file where the key does hit. It also keeps the answer independent of
71
+ # order — a nominal ignores a later `default=`, so without the arm `s.compare_by_identity; s.default = "x"` read
72
+ # `s["zz"]` as `Integer`.
73
+ def identity_widening(shape)
74
+ key, value = MutationWidening.widen_hash_shape(shape, values: :widen).type_args
75
+ Type::Combinator.nominal_of("Hash", type_args: [key, Type::Combinator.union(value, Type::Combinator.untyped)])
76
+ end
77
+
78
+ # `Integer#bit_length` is at most 62 for exactly CRuby's 64-bit fixnum range, `-(2**62)..(2**62 - 1)`.
79
+ def identity_stable_key?(key)
80
+ case key
81
+ when Symbol, true, false, nil then true
82
+ when Integer then key.bit_length <= 62
83
+ else false
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -17,11 +17,24 @@ module Rigor
17
17
  # `||=` / `&&=` are conditional at runtime, but a widening may only LOSE precision, so answering on the branch that
18
18
  # does not store is safe.
19
19
  #
20
- # `Prism::IndexTargetNode` is deliberately absent: it is a multi-assign TARGET, and the `MultiWriteNode` that owns
21
- # it is where that write is observed.
20
+ # `Prism::IndexTargetNode` stores through `[]=` too, but it is absent from {NODE_CLASSES}: it is a TARGET, and the
21
+ # value it stores comes from the construct that owns it — a multi-assign slot (`h[:a], z = 1, 2`), a `for` index
22
+ # (`for h[:a] in xs`, alone, splatted or in a multi-target) or a rescue reference (`rescue => h[:e]`). Only that
23
+ # construct can type the value, so `StatementEvaluator#eval_multi_write`, `#bind_for_index` and
24
+ # `#bind_rescue_reference` observe the straight-line write, passing each target to {.widen} with the slot
25
+ # {MultiTargetBinder} decomposed for it, the element or the rescued exception. {CONTENT_WRITE_NODE_CLASSES} adds
26
+ # it for the nested-block write-back, and `ScopeIndexer`'s pre-pass, which needs no stored value, names the class
27
+ # next to {NODE_CLASSES}.
22
28
  module IndexWriteWidening
23
29
  NODE_CLASSES = [Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode].freeze
24
30
 
31
+ # Every node that stores through `[]=` without being a `[]=` call: {NODE_CLASSES} plus the index TARGET a
32
+ # multi-assign, `rescue =>` or `for` writes through. `StatementEvaluator`'s captured-local write-back widens
33
+ # a receiver a nested block stores into through any of them, `ExpressionTyper`'s block-return threading
34
+ # gate predicts that widening, and `CapturedLocals.content_mutations` finds the per-element fold's in-place
35
+ # captures by it, so all three read this one list.
36
+ CONTENT_WRITE_NODE_CLASSES = [*NODE_CLASSES, Prism::IndexTargetNode].freeze
37
+
25
38
  # The method these forms store through — the name the mutator tables are keyed on.
26
39
  MUTATOR = :[]=
27
40
 
@@ -35,7 +48,7 @@ module Rigor
35
48
  # the widening joins the stored value into the carrier's content evidence exactly as a real `[]=` does
36
49
  # (issue #560). Empty means "no evidence" and widens without joining.
37
50
  #
38
- # @param node — one of {NODE_CLASSES}
51
+ # @param node — one of {NODE_CLASSES}, or a `Prism::IndexTargetNode`
39
52
  def widen(node:, current_scope:, arg_types: MutationWidening::NO_ARG_TYPES)
40
53
  MutationWidening.widen_receiver_aliases(node.receiver, MUTATOR, current_scope, arg_types: arg_types)
41
54
  end