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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../receiver_alias"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2335 — which locals a write through a receiver may reach ({AliasMutationFloor}), read off a frame body's
10
+ # {AliasMutationFloor::Facts}.
11
+ module AliasMutationFloor
12
+ module_function
13
+
14
+ # The locals aliased by the time `call_node`'s message runs, as `{ name => depth }`: those a site has handed on,
15
+ # and those linked to one ({.reach}). A call inside a closure that may run later sees every site.
16
+ def aliased_at(facts, call_node)
17
+ return ALL if facts.held.equal?(ALL)
18
+
19
+ offset = offset_of(facts, call_node)
20
+ seeds = {}
21
+ facts.held.each { |name, key| seeds[name] = FULL if key <= offset }
22
+ facts.held_part.each { |name, key| seeds[name] ||= PART if key <= offset }
23
+ reach(seeds, facts.links, offset)
24
+ end
25
+
26
+ def offset_of(facts, call_node)
27
+ offset = (call_node.message_loc || call_node.location).start_offset
28
+ facts.deferred.any? { |from, to| offset >= from && offset < to } ? Float::INFINITY : offset
29
+ end
30
+
31
+ # `seeds` closed over every link whose site may have run by `offset`. A depth says how far inside a local's object
32
+ # the mutated object may sit: 0 ({FULL}) is the object itself, 1 ({PART}) one of its elements, and so on. A copy
33
+ # keeps the depth; across an element link (`x = a[i]`, `x = a[i][j]`) the parent sits as many levels further out
34
+ # than the child as the read goes, and the child as many further in, so a mutation of the parent object itself
35
+ # (depth 0) does not reach the child.
36
+ # The least depth wins.
37
+ def reach(seeds, links, offset)
38
+ changed = !seeds.empty?
39
+ while changed
40
+ changed = false
41
+ links.each do |child, parent, key, levels|
42
+ next if key > offset
43
+
44
+ changed = true if spread(seeds, child, parent, levels) | spread(seeds, parent, child, -levels)
45
+ end
46
+ end
47
+ seeds
48
+ end
49
+
50
+ DEPTH_CAP = 8
51
+ private_constant :DEPTH_CAP
52
+
53
+ # Spreads the depth `from` reached across one link to `to`, `step` levels further out; answers whether `to`
54
+ # changed.
55
+ def spread(seeds, from, to, step) # rubocop:disable Naming/PredicateMethod
56
+ depth = seeds[from]
57
+ return false if depth.nil?
58
+
59
+ depth += step
60
+ return false if depth.negative? || depth > DEPTH_CAP
61
+ return false if seeds[to] && seeds[to] <= depth
62
+
63
+ seeds[to] = depth
64
+ true
65
+ end
66
+
67
+ # The locals a write through `receiver` may reach, as {.aliased_at} answers them: through each local it may
68
+ # evaluate to ({ReceiverAlias.candidates}: `(c ? a : b)`), through an element read's root ({.element_targets}), or
69
+ # as through any other receiver.
70
+ def receiver_targets(receiver, facts, call_node)
71
+ reads = ReceiverAlias.candidates(receiver)
72
+ aliased = aliased_at(facts, call_node)
73
+ return element_targets(receiver, facts, aliased, call_node) if reads.empty?
74
+ return targets(nil, facts, aliased, call_node) unless reads.all?(Prism::LocalVariableReadNode)
75
+
76
+ reads.map { |read| targets(read.name, facts, aliased, call_node) }.reduce { |acc, set| union(acc, set) }
77
+ end
78
+
79
+ # Through an element read rooted at a local (`c[0] << x`, `h[:k].fetch(0) << x`), the root's own links as far
80
+ # inside it as the read goes (`a = c[0]` is the mutated object itself), beside what any other receiver reaches.
81
+ def element_targets(receiver, facts, aliased, call_node)
82
+ others = targets(nil, facts, aliased, call_node)
83
+ root, depth = element_root(receiver)
84
+ return others if root.nil? || others.equal?(ALL)
85
+
86
+ union(others, reach({ root.name => depth }, facts.links, offset_of(facts, call_node)))
87
+ end
88
+
89
+ def union(left, right)
90
+ return ALL if left.equal?(ALL) || right.equal?(ALL)
91
+
92
+ merged = left.dup
93
+ right.each { |name, depth| merged[name] = [merged[name], depth].compact.min }
94
+ merged
95
+ end
96
+
97
+ # The locals a write through the local `name` (nil for any other receiver) may reach, as `{ name => depth }`:
98
+ #
99
+ # - the locals linked to it ({.reach}): a copy of it, or what it copied (`b = a; b << x`), or the local it is an
100
+ # element of (`x = a[i]`);
101
+ # - and every aliased local, through a receiver that may hold any object: an ivar, an index read, a call result,
102
+ # a block parameter, or a local some write binds to an object from elsewhere;
103
+ # - or only the aliased locals that are foreign, through a local only ever written a new object or a method
104
+ # parameter the body never rebinds, once the body has handed it on: either is an object no new-object local
105
+ # can be, and only a holder it reached can hand it to another local.
106
+ def targets(name, facts, aliased, call_node)
107
+ return ALL if aliased.equal?(ALL)
108
+ return aliased if name.nil? || NUMBERED_PARAMETER.match?(name)
109
+
110
+ group = reach({ name => FULL }, facts.links, offset_of(facts, call_node))
111
+ group.delete(name)
112
+ return union(aliased, group) if facts.foreign.include?(name) && !facts.entry.include?(name)
113
+ return group unless aliased.key?(name)
114
+
115
+ union(group, aliased.slice(*facts.foreign))
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../element_read_widening"
6
+ require_relative "../mutation_widening"
7
+ require_relative "../receiver_alias"
8
+ require_relative "../remover_holder_scan"
9
+ require_relative "../set_mutation"
10
+ require_relative "foreign_locals"
11
+
12
+ module Rigor
13
+ module Inference
14
+ module AliasMutationFloor
15
+ ALWAYS = -1
16
+ LOOPS = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode, Prism::LambdaNode].freeze
17
+ KEEPING_CALLS = (MutationWidening::SHAPE_MUTATORS | SetMutation::MUTATORS |
18
+ %i[new add add? send public_send __send__ instance_variable_set instance_exec instance_eval
19
+ class_exec module_exec define_method define_singleton_method]).freeze
20
+ private_constant :ALWAYS, :LOOPS, :KEEPING_CALLS
21
+
22
+ module_function
23
+
24
+ IMMUTABLE_CLASSES = %w[Integer Float Rational Complex Symbol NilClass TrueClass FalseClass].to_set.freeze
25
+ private_constant :IMMUTABLE_CLASSES
26
+
27
+ # Whether `type` is a container that may hold a mutable element, which a write through an element read (`x =
28
+ # a[0]; x << 1`) can change: `[[1]]` and `Array[String]` may, `[1, :a]` may not.
29
+ def mutable_parts?(type)
30
+ case type
31
+ when Type::Tuple then type.elements.any? { |element| mutable_value?(element) }
32
+ when Type::HashShape then type.pairs.values.any? { |value| mutable_value?(value) }
33
+ when Type::Nominal then type.type_args.any? { |arg| mutable_value?(arg) }
34
+ when Type::Difference then mutable_parts?(type.base)
35
+ when Type::Union then type.members.any? { |member| mutable_parts?(member) }
36
+ else true
37
+ end
38
+ end
39
+
40
+ def mutable_value?(type)
41
+ case type
42
+ when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
43
+ when Type::Nominal then !IMMUTABLE_CLASSES.include?(type.class_name)
44
+ when Type::Union then type.members.any? { |member| mutable_value?(member) }
45
+ else true
46
+ end
47
+ end
48
+
49
+ # Each local or ivar read `node` may evaluate to: through the branches of an `if` / `unless` / ternary, `case`,
50
+ # `case`/`in`, `begin` (its body, rescue and else clauses), `rescue` modifier and `||` / `&&`, the last statement
51
+ # of a body, and a call that answers its receiver (`a.tap { }`, `a.itself`, `a.freeze`, `a.to_a`); anything else
52
+ # as {ReceiverAlias.candidates} reads it. A nest deeper than {SELECTION_DEPTH} reads as no local.
53
+ def each_may_be(node, depth = 0, &)
54
+ node = RemoverHolderScan.unparenthesised(node)
55
+ return if node.nil? || depth > SELECTION_DEPTH
56
+ return each_may_be(node.receiver, depth + 1, &) if self_returning_call?(node)
57
+
58
+ branches = selection_branches(node)
59
+ return ReceiverAlias.candidates(node).each(&) if branches.nil?
60
+
61
+ branches.each { |branch| each_may_be(branch, depth + 1, &) }
62
+ end
63
+
64
+ SELECTION_DEPTH = 6
65
+
66
+ def self_returning_call?(node) = node.is_a?(Prism::CallNode) && SELF_RETURNERS.include?(node.name)
67
+
68
+ # The sub-expressions a selection's value is one of, or nil for a node that selects nothing.
69
+ def selection_branches(node)
70
+ case node
71
+ when Prism::StatementsNode then [node.body.last]
72
+ when Prism::ParenthesesNode, Prism::ElseNode then [node.respond_to?(:statements) ? node.statements : node.body]
73
+ when Prism::IfNode then [node.statements, node.subsequent]
74
+ when Prism::UnlessNode then [node.statements, node.else_clause]
75
+ when Prism::OrNode, Prism::AndNode then [node.left, node.right]
76
+ when Prism::CaseNode, Prism::CaseMatchNode
77
+ node.conditions.map(&:statements) << node.else_clause
78
+ when Prism::BeginNode then begin_branches(node)
79
+ when Prism::RescueModifierNode then [node.expression, node.rescue_expression]
80
+ end
81
+ end
82
+
83
+ def begin_branches(node)
84
+ branches = [node.statements, node.else_clause]
85
+ clause = node.rescue_clause
86
+ while clause
87
+ branches << clause.statements
88
+ clause = clause.subsequent
89
+ end
90
+ branches
91
+ end
92
+
93
+ def local_name(read) = ReceiverAlias.read_name(read)
94
+
95
+ # `[base, depth]` for an element read (`a[i]`, `a.first`, `h.fetch(:k)`, `a.dig(:k, 0)`, a chain of them): what
96
+ # it reads from and how many levels inside it, `[node, 0]` for anything else.
97
+ def element_path(node)
98
+ node = RemoverHolderScan.unparenthesised(node)
99
+ depth = 0
100
+ while node.is_a?(Prism::CallNode) && node.receiver && node.block.nil? && ELEMENT_READS.include?(node.name)
101
+ depth += node.name == :dig ? [node.arguments&.arguments&.size.to_i, 1].max : 1
102
+ node = RemoverHolderScan.unparenthesised(node.receiver)
103
+ end
104
+ [node, depth]
105
+ end
106
+
107
+ # `[root, depth]` for an element read rooted at a local, or nil.
108
+ def element_root(node)
109
+ root, depth = element_path(node)
110
+ [root, depth] if depth.positive? && root.is_a?(Prism::LocalVariableReadNode)
111
+ end
112
+
113
+ # The {RemoverHolderScan} collector, which reads every fact {.scan} needs in that one walk: for each local the
114
+ # least key of the sites that hand it on ({.handed_by}), the ranges of the closures that may run later, and the
115
+ # body's foreign and written locals ({ForeignLocals.visit}).
116
+ class SiteCollector
117
+ # Non-local reads: a local bound to one, or to an element read rooted at one (`a = @rows[0]`), is held by that
118
+ # variable. ({ReceiverAlias.candidates} answers a bare ivar read itself.)
119
+ NON_LOCAL_READS = [Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
120
+ Prism::ClassVariableReadNode].to_set.freeze
121
+ LOCAL_BINDS = [Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
122
+ Prism::LocalVariableAndWriteNode].to_set.freeze
123
+
124
+ attr_writer :site
125
+ attr_reader :held, :held_part, :links, :deferred, :foreign, :written
126
+
127
+ def initialize
128
+ @held = {}
129
+ @held_part = {}
130
+ @links = []
131
+ @site = nil
132
+ @outer = nil
133
+ @outer_owner = nil
134
+ @deferred = []
135
+ @foreign = Set.new
136
+ @written = Set.new
137
+ @literal_blocks = Set.new.compare_by_identity
138
+ @def_depth = 0
139
+ end
140
+
141
+ # A local handed on whole: a capture by a closure that may run later, a reflective receiver, a zero-argument
142
+ # `super`'s parameter.
143
+ def <<(name)
144
+ note(@held, name)
145
+ self
146
+ end
147
+
148
+ # A value position another holder keeps (an element, an argument of a keeping call, an index or attribute
149
+ # write's value): each local the value may be is handed on, and the root of an element read hands on a part.
150
+ def value_held(node)
151
+ AliasMutationFloor.each_may_be(node) do |read|
152
+ note(@held, AliasMutationFloor.local_name(read)) if local?(read)
153
+ end
154
+ root, = AliasMutationFloor.element_root(node)
155
+ note(@held_part, root.name) if root
156
+ end
157
+
158
+ # A write: a local bound to a value that may be another local's object links the two (`b = a`), an element read
159
+ # links them as a part (`x = a[i]`), and a local bound to a non-local holder's object is held by that holder
160
+ # (`a = @q`). A non-local write hands the value on as {#value_held} does.
161
+ def value_written(node)
162
+ return if node.is_a?(Prism::LocalVariableOperatorWriteNode)
163
+ return value_held(node.value) unless LOCAL_BINDS.include?(node.class)
164
+
165
+ bind(node.name, node.value)
166
+ end
167
+
168
+ # A multiple assignment or a pattern: each local target is bound as {#value_written} binds one, to each element
169
+ # of an Array literal value or to the value whole.
170
+ def value_bound(value, target_nodes)
171
+ names = []
172
+ RemoverHolderScan.local_targets(target_nodes, names)
173
+ return if names.empty?
174
+
175
+ values = value.is_a?(Prism::ArrayNode) ? value.elements : [value]
176
+ names.each { |name| values.each { |part| bind(name, part) } }
177
+ end
178
+
179
+ def enter(node, parent)
180
+ if @outer.nil? && LOOPS.any? { |klass| node.is_a?(klass) }
181
+ @outer = node.location.start_offset
182
+ @outer_owner = node
183
+ end
184
+ note_deferred(node, parent)
185
+ if node.is_a?(Prism::DefNode)
186
+ @def_depth += 1
187
+ elsif @def_depth.zero?
188
+ ForeignLocals.visit(node, @foreign, @written, @literal_blocks)
189
+ end
190
+ end
191
+
192
+ def leave(node)
193
+ if node.equal?(@outer_owner)
194
+ @outer = nil
195
+ @outer_owner = nil
196
+ end
197
+ @def_depth -= 1 if node.is_a?(Prism::DefNode)
198
+ end
199
+
200
+ # Only a call that may keep its argument hands it to another holder: a constructor, an in-place mutator (a
201
+ # container adder), and a reflective or `define_method` call. Any other call's arguments are passed, not kept.
202
+ def keeps_arguments?(call_node) = KEEPING_CALLS.include?(call_node.name)
203
+
204
+ private
205
+
206
+ def local?(read) = read.is_a?(Prism::LocalVariableReadNode) || read.is_a?(Prism::ItLocalVariableReadNode)
207
+
208
+ def key = @site ? @outer || AliasMutationFloor.handed_by(@site) : ALWAYS
209
+
210
+ def note(table, name)
211
+ site_key = key
212
+ current = table[name]
213
+ table[name] = site_key if current.nil? || site_key < current
214
+ end
215
+
216
+ def bind(name, value)
217
+ AliasMutationFloor.each_may_be(value) do |read|
218
+ next unless local?(read)
219
+
220
+ other = AliasMutationFloor.local_name(read)
221
+ @links << [name, other, key, 0] unless other == name
222
+ end
223
+ note(@held, name) if NON_LOCAL_READS.include?(AliasMutationFloor.element_path(value).first.class)
224
+ root, depth = AliasMutationFloor.element_root(value)
225
+ @links << [name, root.name, key, depth] if root && root.name != name
226
+ link_elements(name, value)
227
+ end
228
+
229
+ # A container literal's elements are the local's elements (`c = [inner]`): each local an element may be is
230
+ # linked to it as a part.
231
+ def link_elements(name, value)
232
+ value = RemoverHolderScan.unparenthesised(value)
233
+ elements = value.elements if value.is_a?(Prism::ArrayNode)
234
+ elements = value.elements.grep(Prism::AssocNode).map(&:value) if value.is_a?(Prism::HashNode)
235
+ elements&.each do |element|
236
+ AliasMutationFloor.each_may_be(element) do |read|
237
+ @links << [AliasMutationFloor.local_name(read), name, key, 1] if local?(read)
238
+ end
239
+ end
240
+ end
241
+
242
+ def note_deferred(node, parent)
243
+ return unless node.is_a?(Prism::LambdaNode) ||
244
+ (node.is_a?(Prism::BlockNode) && RemoverHolderScan.deferred_block?(parent))
245
+
246
+ start = node.location.start_offset
247
+ @deferred << [start, start + node.location.length]
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end