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
@@ -2,9 +2,11 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- require_relative "../run_scoped_slots"
5
+ require_relative "../typing_state"
6
+ require_relative "../source/node_children"
6
7
  require_relative "closure_escape_analyzer"
7
8
  require_relative "construction_escape"
9
+ require_relative "../memo_audit"
8
10
 
9
11
  module Rigor
10
12
  module Inference
@@ -55,11 +57,17 @@ module Rigor
55
57
  Prism::LocalVariableAndWriteNode
56
58
  ].freeze
57
59
  ALL = :all
58
- # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so no verdict outlives the body it read.
59
- UNSAFE_CACHE_KEY = RunScopedSlots.register(:__rigor_remover_narrowing_unsafe)
60
+ NO_ARGUMENTS = [].freeze
61
+ # ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so no verdict outlives the body it read: keyed by
62
+ # body node, and the unsafe names are a pure function of the body and its frame's parameters.
63
+ UNSAFE_CACHE = TypingState.memo(
64
+ :remover_narrowing_unsafe,
65
+ run_scoped: true, audit: "remover narrowing unsafe",
66
+ reason: "keyed by the body node; the unsafe local names are a pure function of the body"
67
+ )
60
68
  private_constant :REFLECTIVE_CALLS, :SELF_CONVERSIONS, :PURE_READERS, :IMMEDIATE_BLOCK_CALLS, :FRAME_CALLS,
61
69
  :ANY_RECEIVER_FRAME_CALLS, :NON_WRITER_OPERATORS, :CORE_COLLECTIONS,
62
- :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :UNSAFE_CACHE_KEY
70
+ :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :NO_ARGUMENTS, :UNSAFE_CACHE
63
71
 
64
72
  module_function
65
73
 
@@ -70,30 +78,54 @@ module Rigor
70
78
  body = frame&.body
71
79
  return true if body.nil?
72
80
 
73
- table = (Thread.current[UNSAFE_CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
74
- names = table[body] ||= unsafe_names(body, frame.parameters)
81
+ names = memoised_unsafe_names(body, frame.parameters)
75
82
  names.equal?(ALL) || names.include?(name.to_sym)
76
83
  end
77
84
 
78
- def unsafe_names(body, parameters = nil)
79
- names = Set.new
85
+ # {.unsafe_names} of `body`, memoised for the run.
86
+ def memoised_unsafe_names(body, parameters)
87
+ return unsafe_names(body, parameters) if MemoAudit.enabled? && MemoAudit.suspended?
88
+
89
+ table = TypingState.current.weak_key_map(UNSAFE_CACHE)
90
+ stored = table[body]
91
+ if stored.nil?
92
+ return table[body] = MemoAudit.key_audit("remover narrowing unsafe") { unsafe_names(body, parameters) }
93
+ end
94
+
95
+ MemoAudit.check("remover narrowing unsafe", stored, body) { unsafe_names(body, parameters) }
96
+ end
97
+
98
+ # `names` collects the locals; a collector that answers `site=` ({AliasMutationFloor}) is told, before each name
99
+ # it is given, the node that hands the local on, or nil when no single node does (a capture by a closure that may
100
+ # run later, a zero-argument `super`), reads value positions itself when it answers `value_held` ({.precise?}),
101
+ # and is told when the walk enters (`enter(node, parent)`) and leaves
102
+ # (`leave(node)`) each node. One that answers `keeps_arguments?` says which calls' plain arguments it counts as
103
+ # held.
104
+ def unsafe_names(body, parameters = nil, names = Set.new)
105
+ sited = names.respond_to?(:site=)
80
106
  catch(ALL) do
81
- collect_unsafe(body, false, names)
107
+ collect_unsafe(body, false, names, sited:)
108
+ names.site = nil if sited
82
109
  parameter_names(parameters, names) if forwards_parameters?(body)
83
110
  return names.freeze
84
111
  end
85
112
  ALL
86
113
  end
87
114
 
88
- def collect_unsafe(node, in_closure, names)
115
+ def collect_unsafe(node, in_closure, names, sited: false, parent: nil)
89
116
  in_closure ||= node.is_a?(Prism::LambdaNode)
117
+ if sited
118
+ names.enter(node, parent)
119
+ names.site = in_closure ? nil : node
120
+ end
90
121
  names << node.name if in_closure && LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
91
122
  record_node(node, names)
92
123
 
93
- node.compact_child_nodes.each do |child|
124
+ node.rigor_each_child do |child|
94
125
  child_in_closure = in_closure || (child.is_a?(Prism::BlockNode) && deferred_block?(node))
95
- collect_unsafe(child, child_in_closure, names)
126
+ collect_unsafe(child, child_in_closure, names, sited:, parent: node)
96
127
  end
128
+ names.leave(node) if sited
97
129
  end
98
130
 
99
131
  def record_node(node, names) # rubocop:disable Metrics/CyclomaticComplexity
@@ -101,7 +133,9 @@ module Rigor
101
133
  when Prism::CallNode then record_call(node, names)
102
134
  when Prism::YieldNode, Prism::SuperNode then node.arguments&.arguments&.each { |a| record_held(a, names) }
103
135
  when Prism::ArrayNode then node.elements.each { |element| record_held(element, names) }
104
- when Prism::AssocNode then [node.key, node.value].each { |part| record_held(part, names) }
136
+ when Prism::AssocNode
137
+ record_held(node.key, names)
138
+ record_held(node.value, names)
105
139
  when Prism::SplatNode, Prism::BlockArgumentNode then record_held(node.expression, names)
106
140
  when Prism::CaseMatchNode then record_pattern(node.predicate, node.conditions, names)
107
141
  when Prism::MatchRequiredNode, Prism::MatchPredicateNode then record_pattern(node.value, [node.pattern], names)
@@ -115,27 +149,42 @@ module Rigor
115
149
 
116
150
  receiver = unparenthesised(node.receiver)
117
151
  names << receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && REFLECTIVE_CALLS.include?(node.name)
118
- arguments = node.arguments&.arguments || []
152
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
119
153
  if writer_call?(node) && !arguments.empty?
120
- *keys, value = arguments
121
- keys.each { |key| record_held(key, names) }
122
- every_read(value, names)
123
- else
154
+ record_writer_call(arguments, names)
155
+ elsif keeps_arguments?(node, names)
124
156
  arguments.each { |argument| record_held(argument, names) }
125
157
  end
126
158
  end
127
159
 
160
+ def record_writer_call(arguments, names)
161
+ *keys, value = arguments
162
+ keys.each { |key| record_held(key, names) }
163
+ precise?(names) ? names.value_held(value) : every_read(value, names)
164
+ end
165
+
166
+ def keeps_arguments?(node, names) = !names.respond_to?(:keeps_arguments?) || names.keeps_arguments?(node)
167
+
168
+ # A collector that reads a value position itself ({AliasMutationFloor}): `value_held(node)` for a value handed
169
+ # to another holder (an element, an argument, an index or attribute write's value), `value_written(node)` for a
170
+ # write node, and `value_bound(value, targets)` for a multiple assignment or a pattern. The default collector
171
+ # reads every local inside the value instead.
172
+ def precise?(names) = names.respond_to?(:value_held)
173
+
128
174
  # A zero-argument `super` passes every parameter of the method on, implicitly. (A `...` forward passes only the
129
175
  # anonymous parameters, which name no local.)
130
176
  def forwards_parameters?(node)
131
- node.is_a?(Prism::ForwardingSuperNode) || node.compact_child_nodes.any? { |child| forwards_parameters?(child) }
177
+ return true if node.is_a?(Prism::ForwardingSuperNode)
178
+
179
+ node.rigor_each_child { |child| return true if forwards_parameters?(child) }
180
+ false
132
181
  end
133
182
 
134
183
  def parameter_names(node, names)
135
184
  return if node.nil?
136
185
 
137
186
  names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
138
- node.compact_child_nodes.each { |child| parameter_names(child, names) }
187
+ node.rigor_each_child { |child| parameter_names(child, names) }
139
188
  end
140
189
 
141
190
  def frame_call?(node)
@@ -156,6 +205,7 @@ module Rigor
156
205
  # A write's value hands on every local read anywhere inside it, and the local it binds holds what it read.
157
206
  def record_write(node, names)
158
207
  return unless node.class.name.end_with?("WriteNode") && node.respond_to?(:value)
208
+ return names.value_written(node) if precise?(names)
159
209
  return unless every_read(node.value, names)
160
210
 
161
211
  names << node.name if LOCAL_WRITES.any? { |k| node.is_a?(k) } && !fresh_value?(node.value)
@@ -178,12 +228,14 @@ module Rigor
178
228
  end
179
229
 
180
230
  def record_multi_write(node, names)
231
+ return names.value_bound(node.value, [*node.lefts, node.rest, *node.rights]) if precise?(names)
181
232
  return unless every_read(node.value, names)
182
233
 
183
234
  local_targets([*node.lefts, node.rest, *node.rights], names)
184
235
  end
185
236
 
186
237
  def record_pattern(subject, patterns, names)
238
+ return names.value_bound(subject, patterns) if precise?(names)
187
239
  return unless every_read(subject, names)
188
240
 
189
241
  local_targets(patterns, names)
@@ -195,7 +247,8 @@ module Rigor
195
247
 
196
248
  found = node.is_a?(Prism::LocalVariableReadNode)
197
249
  names << node.name if found
198
- node.compact_child_nodes.reduce(found) { |acc, child| every_read(child, names) || acc }
250
+ node.rigor_each_child { |child| found = true if every_read(child, names) }
251
+ found
199
252
  end
200
253
 
201
254
  def local_targets(nodes, names)
@@ -208,10 +261,11 @@ module Rigor
208
261
  # A held position: every local read inside it but the receiver of a {PURE_READERS} call.
209
262
  def record_held(node, names)
210
263
  return if node.nil?
264
+ return names.value_held(node) if precise?(names)
211
265
  return if pure_read?(node)
212
266
 
213
267
  names << node.name if node.is_a?(Prism::LocalVariableReadNode)
214
- node.compact_child_nodes.each { |child| record_held(child, names) }
268
+ node.rigor_each_child { |child| record_held(child, names) }
215
269
  end
216
270
 
217
271
  # A {PURE_READERS} call on a local hands the local on to nothing. Its own arguments are held positions of the
@@ -6,8 +6,9 @@ module Rigor
6
6
  module Inference
7
7
  module ScopeIndexer
8
8
  # The `class_cvars` discovery table (slice 7 phase 6) as a {DeclarationWalk} collector: every
9
- # `@@x = …` a `def` body writes, typed under the census scope and unioned per class. Ruby cvars are
10
- # shared by the instance and singleton facets, so every `def` counts, `def self.x` included.
9
+ # `@@x = …` a `def` body writes, typed under the census scope and unioned per class, joined by what a
10
+ # `class` / `module` body writes to the same cvar. Ruby cvars are shared by the instance and singleton
11
+ # facets, so every `def` counts, `def self.x` included.
11
12
  class ClassCvarsCollector
12
13
  include DeclarationWalk::Collector
13
14
 
@@ -15,6 +16,14 @@ module Rigor
15
16
  @table = {}
16
17
  end
17
18
 
19
+ # Issue #3204 — a `class` / `module` body's own writes, outside its `def`s, under the body's context;
20
+ # each only joins an entry a `def` body's write creates. An unnameable cref (a header below
21
+ # `class << expr`) leaves the prefix empty, and the gather skips it.
22
+ def on_declaration(node, _context, body)
23
+ ScopeIndexer.collect_body_cvar_writes(node.body, body.prefix, body.scope, @table)
24
+ DeclarationWalk::DESCEND
25
+ end
26
+
18
27
  # `@@x` in a `def` resolves through the LEXICAL cref: `Module.nesting` is the same inside a meta-new or
19
28
  # eval block, so the context's `prefix` keys the write and its rebound `self` never does —
20
29
  # `K = Class.new { def m = @@x }` inside `class C` writes `C::@@x`. The walk goes no further in: the
@@ -25,10 +34,8 @@ module Rigor
25
34
  DeclarationWalk::DECLINE
26
35
  end
27
36
 
28
- # The table as the discovery index holds it: each class's entry frozen, then the whole.
29
- def table
30
- @table.transform_values(&:freeze).freeze
31
- end
37
+ # The table as the discovery index holds it ({ScopeIndexer#finished_cvar_census}).
38
+ def table = ScopeIndexer.finished_cvar_census(@table)
32
39
  end
33
40
  end
34
41
  end
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../../reflection"
6
6
  require_relative "../method_parameter_binder"
7
+ require_relative "../../typing_state"
7
8
  require_relative "../global_write_census"
8
9
  require_relative "unseen_ivar_writers"
9
10
  require_relative "derived_param_value"
@@ -58,27 +59,27 @@ module Rigor
58
59
  module_exec].to_set.freeze
59
60
  private_constant :OPAQUE_EVAL_CALLS
60
61
 
61
- WRITER_SCOPE_KEY = :__rigor_declared_param_writer_scope__
62
- private_constant :WRITER_SCOPE_KEY
62
+ # ADR-129 WD1 — a Frame: the scope is installed around a census and restored on the way out.
63
+ WRITER_SCOPE = TypingState.frame(
64
+ :declared_param_writer_scope,
65
+ reason: "the scope whose project tables the writer gates read, installed per census",
66
+ # G1′: the census types its defaults under the Frame, but only the writer gates, outside `type_of`, read it.
67
+ typing_never_reads: true
68
+ )
69
+ private_constant :WRITER_SCOPE
63
70
 
64
71
  module_function
65
72
 
66
73
  # Runs the block with `scope` as the one whose project tables the unseen-writer and single-file gates read, in
67
74
  # place of the census's own scope. `rigor sig-gen` indexes each file over the project tables less the member
68
75
  # layouts; this scope carries all of them, so another file's writer, ancestor or reopening is seen.
69
- def with_writer_scope(scope)
76
+ def with_writer_scope(scope, &)
70
77
  return yield if scope.nil?
71
78
 
72
- previous = Thread.current[WRITER_SCOPE_KEY]
73
- Thread.current[WRITER_SCOPE_KEY] = scope
74
- begin
75
- yield
76
- ensure
77
- Thread.current[WRITER_SCOPE_KEY] = previous
78
- end
79
+ TypingState.current.with(WRITER_SCOPE, scope, &)
79
80
  end
80
81
 
81
- def writer_scope(scope) = Thread.current[WRITER_SCOPE_KEY] || scope
82
+ def writer_scope(scope) = TypingState.current[WRITER_SCOPE] || scope
82
83
 
83
84
  # `{InstanceVariableWriteNode => Rigor::Type}` for `def_node`'s creditable writes, or {EMPTY}. The caller
84
85
  # passes only an instance `def`: a singleton body's `@x` is the class object's.
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../analysis/dependency_recorder"
4
- require_relative "../../run_scoped_slots"
4
+ require_relative "../../typing_state"
5
+ require_relative "../../memo_audit"
5
6
  require_relative "../../type"
6
- require_relative "../refined_dispatch/own_method"
7
+ require_relative "../census_foreign_block"
8
+ require_relative "../ivar_writer_census"
7
9
 
8
10
  module Rigor
9
11
  module Inference
@@ -13,10 +15,10 @@ module Rigor
13
15
  #
14
16
  # - the writes another file's reopening of `K` makes;
15
17
  # - the writes of every project module and superclass on `K`'s instance resolution chain (`include`,
16
- # `prepend`, transitively, and `<`), from this file's own census of that ancestor where it declares one
17
- # (`writes`, taken before the read-before-write `nil` is added, so an ancestor's `initialize` exempts nothing
18
- # and its read-before-write adds nothing, and only for an ivar an instance body of the ancestor writes in this
19
- # file) and from every other file's entries for it.
18
+ # `prepend`, transitively, and `<`; for a module `K`, the modules it includes or prepends), from this file's
19
+ # own census of that ancestor where it declares one (`writes`, taken before the read-before-write `nil` is
20
+ # added, so an ancestor's `initialize` exempts nothing and its read-before-write adds nothing, and only for an
21
+ # ivar an instance body of the ancestor writes in this file) and from every other file's entries for it.
20
22
  #
21
23
  # Another file's write comes from its `[:own_write_type, owner, ivar, value, path]` census entry
22
24
  # ({IvarWriterCensus}): a literal joins as its `Constant`, anything else as `Dynamic[top]`. An ivar the census
@@ -29,9 +31,23 @@ module Rigor
29
31
  # incremental session fires when a file's entries for the pair appear, vanish or change value; and the chain
30
32
  # files its class edges ({Scope::ResolutionChain#record}), so an `include` added or removed re-checks the reader.
31
33
  module IvarContributions
32
- TABLE_KEY = RunScopedSlots.register(:__rigor_ivar_contributions_table__)
34
+ # ADR-129 WD1 — a run-scoped Memo: the table is a pure function of the census it is keyed on by identity.
35
+ TABLE = TypingState.memo(
36
+ :ivar_contributions_table,
37
+ run_scoped: true, audit: "ivar contributions table",
38
+ reason: "the contribution table of one census; keyed on the census's identity"
39
+ )
33
40
  EMPTY = {}.freeze
34
- private_constant :TABLE_KEY, :EMPTY
41
+ NONE = [].freeze
42
+ BINDINGS = :bindings
43
+ CANDIDATE_KEYS = :candidate_keys
44
+ ANCESTOR_ROOTS = CensusForeignBlock::ANCESTOR_ROOTS.to_a.freeze
45
+ # The one implicit root a class whose superclass chain stops at `BasicObject` keeps.
46
+ BASIC_ROOTS = %w[BasicObject].freeze
47
+ SEPARATOR = CensusForeignBlock::CANDIDATE_SEPARATOR
48
+ OBJECT_MARK = CensusForeignBlock::OBJECT_MARK
49
+ private_constant :TABLE, :EMPTY, :NONE, :BINDINGS, :CANDIDATE_KEYS, :ANCESTOR_ROOTS, :BASIC_ROOTS, :SEPARATOR,
50
+ :OBJECT_MARK
35
51
 
36
52
  module_function
37
53
 
@@ -42,34 +58,148 @@ module Rigor
42
58
  path = canonical(path)
43
59
 
44
60
  table = table(scope.discovered_global_write_census)
61
+ resolution = nil
45
62
  accumulator.each do |class_name, entries|
46
63
  next unless class_name.is_a?(String) && !entries.empty?
47
64
 
48
- owners = chain_owners(scope, class_name)
65
+ resolution ||= memoized_resolution(scope, table)
66
+ owners = chain_owners(scope, class_name, resolution)
67
+ any = any_ivar_types(owners, table)
49
68
  entries.each_key do |ivar|
50
69
  joined = contributed(owners, class_name, ivar, table, writes, path)
70
+ joined = joined ? Type::Combinator.union(joined, any) : any if any
51
71
  entries[ivar] = Type::Combinator.union(entries[ivar], joined) if joined
52
72
  end
53
73
  end
54
74
  end
55
75
 
56
76
  # `K` and the project classes and modules on its instance chain, in chain order, with their class edges filed.
57
- # A module's own census takes no chain: its ancestors' writes land on whatever includes it.
58
- def chain_owners(scope, class_name)
59
- return [class_name] if RefinedDispatch::OwnMethod.module_name?(scope, class_name)
77
+ # A module's chain holds the modules it includes or prepends (#2487): each is on the chain of every class that
78
+ # includes the module, so its writes land on every object a read in the module's own methods sees. `extend`
79
+ # is no instance chain entry, for a module as for a class.
80
+ #
81
+ # Issue #2983 — the chain is read under the `:constants` flavor, so a project class or module with no method of
82
+ # its own (`class Anc; end`, whose writer a `class_eval` installs) is an owner too. {ANCESTOR_ROOTS}
83
+ # (`Object`, `BasicObject`, `Kernel`) follow, as every instance has them, except that a class whose superclass
84
+ # chain stops at `BasicObject` keeps `BasicObject` alone; then each owner's alias and instance constants
85
+ # (`[:binds_class, …]` entries), and every candidate key that resolves to an owner ({.resolution}), under which
86
+ # a `class_eval` rooted at that constant files its code.
87
+ def chain_owners(scope, class_name, resolution)
88
+ ancestors, reaches_object = ancestor_names(scope, class_name)
89
+ names = [class_name, *ancestors, *(reaches_object ? ANCESTOR_ROOTS : BASIC_ROOTS)].uniq
90
+ aliases, bound = resolution
91
+ bound_names = names.flat_map do |name|
92
+ segment = name.split("::").last
93
+ Analysis::DependencyRecorder.read_name(:binds_class, segment) if Analysis::DependencyRecorder.active?
94
+ bound[name] || NONE
95
+ end
96
+ (names + bound_names).uniq.flat_map { |owner| [owner, *(aliases[owner] || NONE)] }.uniq
97
+ end
60
98
 
61
- chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
99
+ # The project ancestors on `class_name`'s instance chain, and whether the chain reaches `Object`: false when a
100
+ # superclass edge names `BasicObject`, which has neither `Object` nor `Kernel` among its ancestors.
101
+ def ancestor_names(scope, class_name)
102
+ chain = Scope::ResolutionChain.for(scope, class_name, :instance, :constants)
62
103
  # A guard: the class body's own `include` / `<` resolution already files these edges; kept so the read
63
104
  # states its own dependency.
64
105
  chain.record(scope) if Analysis::DependencyRecorder.active?
65
- ancestors = chain.entries.filter_map do |entry|
106
+ names = chain.entries.filter_map do |entry|
66
107
  # An external entry has no name, so `filter_map` drops it. The side check is a guard: an instance chain
67
108
  # holds only instance-side entries.
68
109
  entry.name if entry.side == :instance && entry.name != class_name
69
110
  end
70
- [class_name, *ancestors.uniq]
111
+ [names, chain.entries.none? { |entry| basic_object_edge?(entry) }]
112
+ rescue StandardError
113
+ [NONE, true]
114
+ end
115
+
116
+ def basic_object_edge?(entry)
117
+ entry.external? && entry.superclass_edge &&
118
+ (entry.raw.to_s.delete_prefix("::") == "BasicObject" || entry.candidates.include?("BasicObject"))
119
+ end
120
+
121
+ # Review round 1 of #3001 — the candidate keys ({CensusForeignBlock.candidate_key}) resolved against the
122
+ # project: each to its first candidate the project declares as a class or module, or binds as a constant, else
123
+ # to its last (top-level) spelling, as Ruby's lexical lookup finds the nearest. Answers `[aliases, bound]`:
124
+ # `{ resolved owner => [candidate key, ...] }` over the census's owners, and `{ resolved class => [bound
125
+ # constant, ...] }` over its `[:binds_class, …]` entries.
126
+ #
127
+ # Review round 2: a candidate a signature declares (`sig/`, a gem's RBS) counts as existing too, so a nearer
128
+ # `N::Anc` only RBS knows wins over a top-level project `Anc` and credits no project class; and a key carrying
129
+ # {OBJECT_MARK} joins only when it resolves to an instance binding.
130
+ def resolution(scope, table)
131
+ bindings = table[BINDINGS] || NONE
132
+ constants = bindings.to_set { |_, name, _| name }
133
+ instances = bindings.filter_map { |_, name, kind| name if kind == :instance }.to_set
134
+ bound = {}
135
+ bindings.each { |key, name, _| (bound[resolve(scope, key, constants)] ||= []) << name }
136
+ aliases = {}
137
+ (table[CANDIDATE_KEYS] || NONE).each do |key|
138
+ resolved = resolve(scope, key.delete_prefix(OBJECT_MARK), constants)
139
+ next if key.start_with?(OBJECT_MARK) && (!instances.include?(resolved) || module?(scope, resolved))
140
+
141
+ (aliases[resolved] ||= []) << key
142
+ end
143
+ [aliases, bound]
144
+ end
145
+
146
+ def resolve(scope, key, constants)
147
+ candidates = key.split(SEPARATOR)
148
+ candidates.find do |name|
149
+ constants.include?(name) || scope.discovered_classes.key?(name) ||
150
+ scope.discovered_superclasses.key?(name) || scope.discovered_includes.key?(name) || declared?(scope, name)
151
+ end || candidates.last
152
+ end
153
+
154
+ # Review round 3 of #3001 — {.resolution} for `table`, computed once per run: it reads only the census and the
155
+ # project-wide declaration tables and signatures, which no file changes within a run. The value rides the
156
+ # {TABLE} slot beside the table it was computed from, keyed on the discovery index it read.
157
+ def memoized_resolution(scope, table)
158
+ slot = TypingState.current[TABLE]
159
+ return resolution(scope, table) unless slot && slot[1].equal?(table)
160
+
161
+ discovery = scope.discovery
162
+ cached = slot[2]
163
+ return cached[1] if cached && cached[0].equal?(discovery)
164
+
165
+ value = resolution(scope, table)
166
+ slot[2] = [discovery, value]
167
+ value
168
+ end
169
+
170
+ # Review round 3 of #3001 — whether `name` is a class or module the project or a signature declares, whose
171
+ # `.class` / `.singleton_class` no instance of it runs, whatever a constructor binding says. A guard beside
172
+ # {CensusForeignBlock.binding_entry}'s meta-class check, which records no such binding to begin with.
173
+ def module?(scope, name)
174
+ scope.discovered_classes.key?(name) || scope.discovered_superclasses.key?(name) ||
175
+ scope.discovered_includes.key?(name) || scope.environment&.rbs_loader&.class_known?(name) || false
71
176
  rescue StandardError
72
- [class_name]
177
+ true
178
+ end
179
+
180
+ # Whether a loaded signature declares `name` as a class, module or constant.
181
+ def declared?(scope, name)
182
+ loader = scope.environment&.rbs_loader
183
+ return false if loader.nil?
184
+
185
+ loader.class_known?(name) || !loader.constant_declaration(name).nil?
186
+ rescue StandardError
187
+ false
188
+ end
189
+
190
+ # Issue #2983 — what the owners' string `class_eval`s contribute to every ivar (`[:own_write_type, owner, :*,
191
+ # :dynamic]`), from every file, this one included: the per-file census reads no String either.
192
+ def any_ivar_types(owners, table)
193
+ types = []
194
+ owners.each do |owner|
195
+ # A guard: a String `class_eval` also gives its module the discovery index's dynamic mark, whose edge
196
+ # re-checks the reader when one comes or goes; kept so the read states its own dependency.
197
+ if Analysis::DependencyRecorder.active?
198
+ Analysis::DependencyRecorder.read_name(:own_write_type, "#{owner}##{IvarWriterCensus::ANY_IVAR}")
199
+ end
200
+ add_entry_types(types, table.dig(owner, IvarWriterCensus::ANY_IVAR) || EMPTY, nil)
201
+ end
202
+ types.empty? ? nil : Type::Combinator.union(*types)
73
203
  end
74
204
 
75
205
  # The union of what the owners contribute to `ivar`, or nil for nothing.
@@ -80,19 +210,25 @@ module Rigor
80
210
  Analysis::DependencyRecorder.read_name(:own_write_type, "#{owner}##{ivar}")
81
211
  end
82
212
  by_path = table.dig(owner, ivar) || EMPTY
83
- # This file's census of an ancestor counts only an ivar an instance body of it writes here: the census
84
- # also holds a `def self.` write of a module (#1958), which runs on the module object.
213
+ # This file's census of an ancestor counts only an ivar an instance body of it writes here. `writes` leaves
214
+ # out an ivar a `def self.` of the ancestor also writes (the census files that write under it, #1958, and it
215
+ # runs on the module object); this file's census entries for it join instead, as in another file (#2931).
85
216
  own = writes.dig(owner, ivar) if owner != class_name && by_path.key?(path)
86
217
  types << own if own
87
- by_path.each do |value_path, values|
88
- next if value_path == path
89
-
90
- values.each { |value| types << value_type(value) }
91
- end
218
+ add_entry_types(types, by_path, own || owner == class_name ? path : nil)
92
219
  end
93
220
  types.empty? ? nil : Type::Combinator.union(*types)
94
221
  end
95
222
 
223
+ # Each census entry's value type, except those of `skipped_path` (the file whose census already holds them).
224
+ def add_entry_types(types, by_path, skipped_path)
225
+ by_path.each do |value_path, values|
226
+ next if value_path == skipped_path
227
+
228
+ values.each { |value| types << value_type(value) }
229
+ end
230
+ end
231
+
96
232
  # A String literal joins as `String`, not its `Constant`: a write elsewhere cannot see an in-place mutation of
97
233
  # the String (`@s << "abc"`) in this file, in another or in an ancestor, so the value would be pinned too
98
234
  # narrowly. Every other literal kind is immutable and joins as its `Constant`.
@@ -114,21 +250,45 @@ module Rigor
114
250
  # `{ owner => { ivar => { path => [value, ...] } } }` over `census`'s path-tagged `:own_write_type` entries,
115
251
  # built once per census and kept in one run-scoped slot keyed by its identity.
116
252
  def table(census)
117
- slot = Thread.current[TABLE_KEY]
118
- return slot[1] if slot && slot[0].equal?(census)
253
+ return build_table(census) if MemoAudit.enabled? && MemoAudit.suspended?
254
+
255
+ state = TypingState.current
256
+ slot = state[TABLE]
257
+ if slot && slot[0].equal?(census)
258
+ return MemoAudit.check("ivar contributions table", slot[1], census.size) { build_table(census) }
259
+ end
119
260
 
261
+ table = MemoAudit.key_audit("ivar contributions table") { build_table(census) }
262
+ state[TABLE] = [census, table]
263
+ table
264
+ end
265
+
266
+ # Issue #2983 — `table[BINDINGS]` lists `[class candidate key, constant name, kind]` over the census's
267
+ # `[:binds_class, …]` entries, and `table[CANDIDATE_KEYS]` the owners that are candidate keys; no class is named
268
+ # by a Symbol.
269
+ def build_table(census)
120
270
  table = {}
121
271
  canonical_paths = {}
122
272
  census.each do |entry|
273
+ next (table[BINDINGS] ||= []) << entry.drop(1) if entry[0] == :binds_class
123
274
  next unless entry.size == 5 && entry[0] == :own_write_type
124
275
 
125
- by_path = ((table[entry[1]] ||= {})[entry[2]] ||= {})
276
+ by_path = (owner_slot(table, entry[1])[entry[2]] ||= {})
126
277
  (by_path[canonical_paths[entry[4]] ||= canonical(entry[4])] ||= []) << entry[3]
127
278
  end
128
- Thread.current[TABLE_KEY] = [census, table]
129
279
  table
130
280
  end
131
- private_class_method :chain_owners, :contributed, :value_type, :canonical, :table
281
+
282
+ def owner_slot(table, owner)
283
+ table[owner] ||= begin
284
+ (table[CANDIDATE_KEYS] ||= []) << owner if owner.include?(SEPARATOR) || owner.start_with?(OBJECT_MARK)
285
+ {}
286
+ end
287
+ end
288
+
289
+ private_class_method :chain_owners, :ancestor_names, :basic_object_edge?, :resolution, :resolve, :declared?,
290
+ :memoized_resolution, :module?, :any_ivar_types, :contributed, :add_entry_types,
291
+ :value_type, :canonical, :table, :build_table, :owner_slot
132
292
  end
133
293
  end
134
294
  end