rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../typing_state"
6
+ require_relative "../source/node_children"
7
+ require_relative "closure_escape_analyzer"
8
+ require_relative "element_read_widening"
9
+ require_relative "mutation_widening"
10
+ require_relative "receiver_alias"
11
+ require_relative "remover_holder_scan"
12
+ require_relative "set_mutation"
13
+ require_relative "string_mutation"
14
+ require_relative "unknown_store_widening"
15
+ require_relative "alias_mutation_floor/foreign_locals"
16
+ require_relative "alias_mutation_floor/memos"
17
+ require_relative "alias_mutation_floor/site_collector"
18
+ require_relative "alias_mutation_floor/reach"
19
+
20
+ module Rigor
21
+ module Inference
22
+ # Issue #2335 — a local's literal shape describes its object, and a mutation through another path to that object
23
+ # makes it stale: `y = x; y << "s"`, `a.each { |x| x << "s" }` on an Array holding `inner`, `@a = a; @a << "s"`,
24
+ # `h[:k] = a; h[:k] << "s"`, `Hash.new(a)[:k] << "s"`, `[acc].each { |a| poke(a) }`. The named-binding widening
25
+ # ({MutationWidening.widen_after_call}) and the callee floor only rebind the binding a receiver or argument names,
26
+ # so every other local holding the object kept its literal and a later read reported on correct code.
27
+ #
28
+ # One scan per frame body, in {RemoverHolderScan}'s walk ({SiteCollector}), answers:
29
+ #
30
+ # - the locals handed whole to another holder: an element of a container literal, an argument of a call that may
31
+ # keep it, an ivar, global, index or attribute write's value, a capture by a closure that may run later, a
32
+ # reflective receiver, a local bound to an ivar, global or class variable (`a = @q`); every local under
33
+ # `binding` / `eval`. A value counts only where it may evaluate to the local itself ({.each_may_be}: the local,
34
+ # a selection of it, a call that answers its receiver), so `n = a.size`, `b = a.dup` and `[a.first]` hand
35
+ # nothing on whole;
36
+ # - the locals one of whose elements is handed on (`[a.first]`), and the links between locals: a copy (`b = a`)
37
+ # and an element read (`x = a[i]`);
38
+ # - the *foreign* locals, whose object may have come from elsewhere ({ForeignLocals}).
39
+ #
40
+ # A local is *aliased* at a call once a site that hands it on may have run before the call's message (a site
41
+ # earlier in the body, or one inside a loop or block that also holds the call; a capture by a closure that may run
42
+ # later counts everywhere, and a call inside such a closure sees every site), or it is linked to such a local. A
43
+ # local reached only through an element link is a *part*, floored only when its binding can hold a mutable
44
+ # element ({.mutable_parts?}).
45
+ #
46
+ # A mutating call whose receiver may be another local's object ({.targets}: the locals linked to the receiver, and
47
+ # the aliased ones when the receiver may hold any object) gives each local it may reach the
48
+ # unknown store {UnknownStoreWidening.widen_store} gives a site whose stored values nothing typed: the same
49
+ # widening the named path applies, fed `Dynamic[top]` arguments, with its gradual arm. A callee that writes such
50
+ # an argument, and a call on one to a method that mutates its instance variables, floor them the same way. The
51
+ # bindings the receiver itself names are left to the named path, so its precise answer stands.
52
+ #
53
+ # A body where no local is handed on, and a receiver only ever written a new object that is not aliased, change
54
+ # nothing.
55
+ module AliasMutationFloor
56
+ # `held` maps each local handed to another holder to the least key of those sites ({.handed_by}), or is {ALL};
57
+ # `held_part` does the same for a local an element of which was handed on (`[a.first]`); `links` lists
58
+ # `[child, parent, key, levels]` for each write that binds a local to another local's object (`b = a`, 0 levels)
59
+ # or to an element of it (`x = a[i]`, 1; `x = a[i][j]`, 2; `c = [x]` links `x` as `c`'s element). `deferred`
60
+ # lists the `[start, end)` ranges of the closures that may run later. `memo` and `named_memo` keep each call's and
61
+ # each read's targets for the run: a loop or block body is evaluated more than once.
62
+ Facts = Data.define(:held, :held_part, :links, :foreign, :entry, :deferred, :memo, :named_memo)
63
+ # Depths ({.reach}): the mutated object is the local's object itself, or one of its elements.
64
+ FULL = 0
65
+ PART = 1
66
+ NO_KEYS = {}.freeze
67
+ NO_LINKS = [].freeze
68
+ # Calls whose result is their receiver (or, for `to_a` / `to_h` / `to_s`, may be on one core class), as
69
+ # {CalleeWriteCensus} reads them.
70
+ SELF_RETURNERS = %i[tap itself freeze to_a to_h to_s].to_set.freeze
71
+
72
+ ALL = :all
73
+ NUMBERED_PARAMETER = /\A_[1-9]\z/
74
+ SEND_NAMES = %i[send public_send __send__].to_set.freeze
75
+ ELEMENT_READS = %i[[] first last fetch dig].to_set.freeze
76
+ private_constant :FULL, :PART, :NO_KEYS, :NO_LINKS, :SELF_RETURNERS, :ALL, :NUMBERED_PARAMETER,
77
+ :SEND_NAMES, :ELEMENT_READS
78
+ private_constant :Memos
79
+
80
+ module_function
81
+
82
+ # `scope` after `call_node`, a shape mutator ({.candidate?}), ran on a receiver that may be another local's
83
+ # object. The block answers the locals the receiver itself names, whose own widening (the named path's)
84
+ # stands; it runs only once the call has targets.
85
+ def floor_after_call(call_node, scope)
86
+ return scope unless local_besides?(scope, nil) { |type| UnknownStoreWidening.callee_floorable?(type) }
87
+
88
+ facts = facts_for(scope)
89
+ return scope if facts.nil?
90
+
91
+ targets = call_targets(facts, call_node) { receiver_targets(call_node.receiver, facts, call_node) }
92
+ return scope if targets.empty?
93
+
94
+ whole = !targets.equal?(ALL)
95
+ floor_targets(scope, targets, yield) do |type, kind|
96
+ unknown_store(call_node, type, removed_keys: whole && kind == FULL)
97
+ end
98
+ end
99
+
100
+ # Whether `call_node` is a shape mutator on an explicit receiver, the only call {.floor_after_call} floors after.
101
+ def candidate?(call_node)
102
+ !call_node.receiver.nil? && MutationWidening::SHAPE_MUTATORS.include?(call_node.name)
103
+ end
104
+
105
+ # `scope` after `call_node` wrote the object the local `read` reads (a callee that writes its argument or mutates
106
+ # the ivars of an object holding it): every local that may be that object and holds a literal shape
107
+ # ({.literal_shape?}) takes one unknown store ({UnknownStoreWidening.callee_block_floor}). A method parameter the
108
+ # body never rebinds floors nothing here: the callee census reads every opaque hand-off as a write, and flooring
109
+ # each local a parameter was copied into at every such call cost precision across the corpus for no finding (a
110
+ # known limit of #2335).
111
+ def floor_after_written_local(scope, read, call_node)
112
+ name = read.name
113
+ return scope unless local_besides?(scope, name) { |type| literal_shape?(type) }
114
+
115
+ facts = facts_for(scope)
116
+ return scope if facts.nil?
117
+ return scope if facts.entry.include?(name)
118
+
119
+ targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
120
+ return scope if targets.empty?
121
+
122
+ floor_targets(scope, targets, name) { |type| callee_floor(type) }
123
+ end
124
+
125
+ # `type` after `call_node`'s unknown store ({UnknownStoreWidening.widen_store}), memoised per call site for the
126
+ # run as {.callee_floor} is.
127
+ def unknown_store(call_node, type, removed_keys:)
128
+ Memos.unknown_store(call_node, removed_keys, type) do
129
+ method_name, arg_types = UnknownStoreWidening.unknown_store(call_node)
130
+ (removed_keys && removed_keys_shape(type, method_name)) ||
131
+ UnknownStoreWidening.widen_store(type, method_name, arg_types)
132
+ end
133
+ end
134
+
135
+ # A `HashShape` under a remover (`delete`, `clear`, `reject!`, ...) through another holder: the same shape with
136
+ # every key optional. Nothing is stored, so every value stands, and a read of a key the remover may have taken
137
+ # still answers its `nil` (`g = h; g.delete(k); h[k].upcase` raises in Ruby), which the nominal `Hash` the unknown
138
+ # store widens to would not. Only for a target that may be the mutated object itself, and not under `binding` /
139
+ # `eval`: a remover on an element (`x = h[:k]; x.delete(:a)`) must not mark `h`'s own keys optional and leave the
140
+ # element's removed key required. Nil for anything else.
141
+ def removed_keys_shape(type, method_name)
142
+ return nil unless type.is_a?(Type::HashShape) && UnknownStoreWidening::VALUE_PRESERVING.include?(method_name)
143
+
144
+ Type::HashShape.new(type.pairs, required_keys: [], optional_keys: type.pairs.keys,
145
+ read_only_keys: type.read_only_keys, extra_keys: type.extra_keys)
146
+ end
147
+
148
+ # {UnknownStoreWidening.callee_block_floor} of a literal shape, nil for anything else, memoised for the run: the
149
+ # same shape is floored again on each pass over a loop or block body and by each callee channel.
150
+ def callee_floor(type)
151
+ return nil unless literal_shape?(type)
152
+
153
+ Memos.callee_floor(type) { UnknownStoreWidening.callee_block_floor(type) }
154
+ end
155
+
156
+ # Whether {.floor_after_written_local} may floor anything for the local `read` reads at `call_node`: a target
157
+ # bound to a literal shape.
158
+ def reaches_other_locals?(scope, read, call_node)
159
+ name = read.name
160
+ return false unless local_besides?(scope, name) { |type| literal_shape?(type) }
161
+
162
+ facts = facts_for(scope)
163
+ return false if facts.nil? || facts.entry.include?(name)
164
+
165
+ targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
166
+ return local_besides?(scope, name) { |type| literal_shape?(type) } if targets.equal?(ALL)
167
+
168
+ targets.any? do |target, kind|
169
+ type = scope.local(target)
170
+ target != name && literal_shape?(type) && (kind == FULL || mutable_parts?(type))
171
+ end
172
+ end
173
+
174
+ # Whether a call that may mutate a holder's object through `self` ({.floor_after_holder_call}) reaches an aliased
175
+ # local holding a literal shape.
176
+ def holder_call_reaches?(scope, call_node)
177
+ return false unless local_besides?(scope, nil) { |type| literal_shape?(type) }
178
+
179
+ facts = facts_for(scope)
180
+ return false if facts.nil?
181
+
182
+ targets = holder_targets(facts, call_node)
183
+ return true if targets.equal?(ALL)
184
+
185
+ targets.any? do |target, kind|
186
+ type = scope.local(target)
187
+ literal_shape?(type) && (kind == FULL || mutable_parts?(type))
188
+ end
189
+ end
190
+
191
+ # `scope` after a call on `self` whose method mutates one of its ivars: every aliased local holding a literal
192
+ # shape takes one unknown store, as {.floor_after_written_local} gives it.
193
+ def floor_after_holder_call(scope, call_node)
194
+ facts = facts_for(scope)
195
+ return scope if facts.nil?
196
+
197
+ floor_targets(scope, holder_targets(facts, call_node), nil) { |type| callee_floor(type) }
198
+ end
199
+
200
+ def holder_targets(facts, call_node)
201
+ call_targets(facts, call_node) { targets(nil, facts, aliased_at(facts, call_node), call_node) }
202
+ end
203
+
204
+ # A literal shape a store can make stale: a `Tuple`, a `HashShape`, a literal String or Set, or an empty-witness
205
+ # refinement, alone or in a union. The callee paths floor only these: a nominal `Array[String]` is a claim the
206
+ # named path's unknown store leaves as it is too.
207
+ def literal_shape?(type)
208
+ case type
209
+ when Type::Tuple, Type::HashShape then true
210
+ when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
211
+ when Type::Difference then type.removes_empty_witness?
212
+ when Type::Union then type.members.any? { |member| literal_shape?(member) }
213
+ else false
214
+ end
215
+ end
216
+
217
+ # Whether the body of `def_node` runs a shape mutator on one of its own instance variables or an element of one
218
+ # (`@items << v`, `@h[k] = v`, `@rows[0] << v`): a call to it on an object that holds another local's object
219
+ # (`box = Box.new(a)`, whose `initialize` keeps `@items = a`) may mutate that object. A nested def is not entered.
220
+ def mutates_ivar?(def_node)
221
+ return false if def_node.nil?
222
+
223
+ Memos.ivar_mutator(def_node) { ivar_mutation?(def_node.body) }
224
+ end
225
+
226
+ def ivar_mutation?(node)
227
+ return false if node.nil? || node.is_a?(Prism::DefNode)
228
+ return true if node.is_a?(Prism::CallNode) && MutationWidening::SHAPE_MUTATORS.include?(node.name) &&
229
+ ivar_rooted?(node.receiver)
230
+
231
+ node.rigor_each_child { |child| return true if ivar_mutation?(child) }
232
+ false
233
+ end
234
+
235
+ def ivar_rooted?(receiver)
236
+ receiver = RemoverHolderScan.unparenthesised(receiver)
237
+ receiver = receiver.receiver while receiver.is_a?(Prism::CallNode) && ELEMENT_READS.include?(receiver.name)
238
+ receiver.is_a?(Prism::InstanceVariableReadNode)
239
+ end
240
+
241
+ # A `send` / `public_send` / `__send__` with a literal Symbol or String name as the call it makes (`a.send(:push,
242
+ # x)` and `a.send("push", x)` are `a.push(x)`), or nil. A name that is no valid string names no method.
243
+ def literal_send_target(call_node)
244
+ return nil unless SEND_NAMES.include?(call_node.name) && call_node.block.nil?
245
+
246
+ arguments = call_node.arguments&.arguments
247
+ target = arguments&.first
248
+ return nil unless target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode)
249
+ return nil unless target.unescaped.valid_encoding?
250
+
251
+ rest = arguments.drop(1)
252
+ call_node.copy(name: target.unescaped.to_sym,
253
+ arguments: rest.empty? ? nil : call_node.arguments.copy(arguments: rest))
254
+ end
255
+
256
+ def facts_for(scope)
257
+ frame = scope.match_frame
258
+ body = frame&.body
259
+ return nil if body.nil?
260
+
261
+ facts = Memos.facts(body) { scan(body, frame.parameters) }
262
+ facts.held.empty? && facts.held_part.empty? && facts.links.empty? ? nil : facts
263
+ end
264
+
265
+ # One walk ({SiteCollector}) answers the facts. A body that calls `binding` or `eval` stops it early: every local
266
+ # is aliased, and the foreign locals are read in a walk of their own.
267
+ def scan(body, parameters)
268
+ collector = SiteCollector.new
269
+ result = RemoverHolderScan.unsafe_names(body, parameters, collector)
270
+ if result.equal?(ALL)
271
+ foreign, entry = ForeignLocals.collect(body, parameters)
272
+ return Facts.new(held: ALL, held_part: NO_KEYS, links: NO_LINKS, foreign:, entry:, deferred: NO_LINKS,
273
+ memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
274
+ end
275
+
276
+ foreign, entry = ForeignLocals.finish(parameters, collector.foreign, collector.written)
277
+ Facts.new(held: collector.held.freeze, held_part: collector.held_part.freeze, links: collector.links.freeze,
278
+ foreign:, entry:, deferred: collector.deferred.freeze,
279
+ memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
280
+ end
281
+
282
+ # A site's key ({SiteCollector}) is the start of the outermost loop or block that holds it (it may run again after
283
+ # any later point inside that loop), else where it has handed its locals on: its end (the receiver of a call has
284
+ # handed its locals on before the call's message runs, while the call's own arguments are handed on by the call
285
+ # itself), or its block's start. A local's key is its least site's; a capture by a closure that may run later is
286
+ # {ALWAYS}.
287
+ # Where a site has handed its locals on: a call's block may receive them as soon as it starts.
288
+ def handed_by(node)
289
+ block = node.block if node.is_a?(Prism::CallNode)
290
+ block.is_a?(Prism::BlockNode) ? block.location.start_offset : node.location.end_offset
291
+ end
292
+
293
+ # The targets of `call_node` through its receiver, computed once per call site.
294
+ def call_targets(facts, call_node)
295
+ facts.memo.fetch(call_node) { facts.memo[call_node] = yield }
296
+ end
297
+
298
+ # The targets of a write through the local `read` reads (a written argument, a receiver), computed once per read.
299
+ # The read is the call's own operand, so its call's offset is fixed with it.
300
+ def read_targets(facts, read)
301
+ facts.named_memo.fetch(read) { facts.named_memo[read] = yield }
302
+ end
303
+
304
+ # Whether `scope` binds a local other than `except` (a name, a list of names, or nil) whose type the block
305
+ # accepts: the cheap gate every trigger asks before the body scan, since a floor changes only such a binding.
306
+ def local_besides?(scope, except)
307
+ scope.locals.each { |name, type| return true if !excepted?(except, name) && yield(type) }
308
+ false
309
+ end
310
+
311
+ def excepted?(except, name)
312
+ except.is_a?(Symbol) ? except == name : except&.include?(name)
313
+ end
314
+
315
+ def each_target(scope, targets, &)
316
+ return scope.locals.each_key { |name| yield name, FULL } if targets.equal?(ALL)
317
+
318
+ targets.each(&)
319
+ end
320
+
321
+ # Each target but `except` takes what the block answers for its binding and the depth it was reached at. A target
322
+ # reached only below its own object (a depth past {FULL}) is floored only when its binding can hold a mutable
323
+ # element.
324
+ def floor_targets(scope, targets, except)
325
+ acc = scope
326
+ each_target(scope, targets) do |name, kind|
327
+ next if excepted?(except, name)
328
+
329
+ current = acc.local(name)
330
+ next if current.nil? || (kind > FULL && !mutable_parts?(current))
331
+
332
+ floored = yield(current, kind)
333
+ acc = acc.with_mutated_local(name, floored) unless floored.nil? || floored == current
334
+ end
335
+ acc
336
+ end
337
+ end
338
+ end
339
+ end
@@ -62,11 +62,29 @@ module Rigor
62
62
  receiver_name = block_form_receiver(node)
63
63
  return nil if receiver_name.nil?
64
64
 
65
+ named = struct_class_name(node) if receiver_name == :Struct
66
+ return named if named
67
+
65
68
  location = node.location
66
69
  key = [source_path, location.start_line, location.start_column].compact.join(":")
67
70
  Type::AnonymousClassName.build(receiver_name, key)
68
71
  end
69
72
 
73
+ # Issue #2968 — `Struct.new("Named", :a)` names its class `Struct::Named`: a leading String argument that spells a
74
+ # constant (Ruby raises `NameError` for any other) is the class's name under `Struct`, so a block body is that
75
+ # class's and a reopening `class Struct::Named` in another file meets the same table entry. Nil for any other
76
+ # first argument. `node` is a `Struct.new` call.
77
+ def struct_class_name(node)
78
+ first = node.arguments&.arguments&.first
79
+ return nil unless first.is_a?(Prism::StringNode)
80
+
81
+ name = first.unescaped
82
+ "Struct::#{name}" if name.match?(STRUCT_CLASS_NAME)
83
+ end
84
+
85
+ STRUCT_CLASS_NAME = /\A[A-Z][A-Za-z0-9_]*\z/
86
+ private_constant :STRUCT_CLASS_NAME
87
+
70
88
  # The bare-constant receiver's name (`Class`, `::Class`), or nil for every other receiver shape.
71
89
  def literal_constant_name(node)
72
90
  case node
@@ -194,7 +194,7 @@ module Rigor
194
194
  softened = []
195
195
  joined = Array.new(tables.map(&:size).max) do |i|
196
196
  column = tables.map { |table| table[i] }
197
- next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
197
+ next Type::Combinator.untyped if column.any? { |t| t.nil? || Type::Combinator.untyped?(t) }
198
198
 
199
199
  firm = column.reject { |t| nil_literal?(t) }
200
200
  next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "block_call_timing"
6
6
  require_relative "closure_escape_analyzer"
7
+ require_relative "../memo_audit"
7
8
  require_relative "project_method_ownership"
8
9
 
9
10
  module Rigor
@@ -50,9 +51,15 @@ module Rigor
50
51
  # one, asks the same callee.
51
52
  def may_repeat?(def_node, scope)
52
53
  memo = ProjectMethodOwnership.memo(scope)[:block_runs]
53
- return memo[def_node] if memo.key?(def_node)
54
+ if memo.key?(def_node)
55
+ return MemoAudit.check("block runs", memo[def_node], def_node) do
56
+ runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
57
+ end
58
+ end
54
59
 
55
- memo[def_node] = runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
60
+ memo[def_node] = MemoAudit.key_audit("block runs") do
61
+ runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
62
+ end
56
63
  end
57
64
 
58
65
  def runs(node, block_name)
@@ -25,11 +25,11 @@ module Rigor
25
25
  # Methods that yield (block-dependent) — classifier
26
26
  # may mark them leaf when the block call is gated:
27
27
  :each, :each_with_index, :each_index, :each_slice,
28
- :each_cons, :each_with_object,
28
+ :each_cons, :each_with_object, :reverse_each,
29
29
  # Identity/comparison methods that take a block too
30
30
  :max, :min, :max_by, :min_by, :minmax, :minmax_by,
31
31
  :sort_by, :group_by, :partition, :all?, :any?, :none?,
32
- :one?, :find, :detect, :find_all, :find_index,
32
+ :one?, :find, :detect, :rfind, :find_all, :find_index, :index, :rindex,
33
33
  :reduce, :inject, :flat_map, :collect_concat,
34
34
  :zip, :product, :combination, :permutation,
35
35
  :chunk_while, :slice_when, :tally
@@ -109,7 +109,7 @@ module Rigor
109
109
  end
110
110
  end
111
111
 
112
- by_name[name] = compute_successors(def_node, name, owner, edges)
112
+ by_name[name] = MemoAudit.key_audit("successor") { compute_successors(def_node, name, owner, edges) }
113
113
  end
114
114
 
115
115
  def compute_successors(def_node, name, owner, edges)
@@ -272,6 +272,8 @@ module Rigor
272
272
 
273
273
  def ivar_name?(name) = variable_kind(name) == :ivar
274
274
 
275
+ def local_name?(name) = variable_kind(name) == :local
276
+
275
277
  # Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
276
278
  # `@`, and a local with none of them.
277
279
  def variable_kind(name)