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,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "anonymous_meta_class"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2490 — a block, inside an instance body of a class `K`, whose `self` is provably not an instance of `K`, so
10
+ # an `@x` write directly in it writes some other object's ivar and the class-ivar censuses must not credit it to
11
+ # `K`. Read syntactically, before any type is known, by both the per-file census ({ScopeIndexer}) and the cross-file
12
+ # {IvarWriterCensus}. False positives come first, so a receiver that may hold a `K` instance keeps the credit:
13
+ #
14
+ # - `instance_eval` / `instance_exec` on a receiver that is provably no `K` instance ({.provably_other?}): a
15
+ # constant or `self.class` (a class object), `X.new` / `X.allocate` on a constant `X` not spelling `K`, a
16
+ # literal. Any other receiver (a parameter, a local, `dup`, `itself`) may be a `K` and still counts;
17
+ # - `class_eval` / `module_eval` / `class_exec` / `module_exec` on an explicit receiver: `self` is the receiver
18
+ # module, so `@x` directly in the block is that module object's own ivar;
19
+ # - `define_method` on a constant not spelling `K` (its block runs on that class's instances), and
20
+ # `define_singleton_method` on a class object or a literal;
21
+ # - a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block: `self` is the new class.
22
+ #
23
+ # `class << obj` inside an instance body is one more barred body; the callers bar it themselves, since it is no
24
+ # call. Only a barred body's own statements are dropped: a `define_method` block nested in a `class_eval` or
25
+ # `class << obj` body whose module may be `K` or an instance's singleton class runs on a `K` instance and still
26
+ # counts, and so does a receiver-less `def` nested in either (#2941, {.reentry_blocks}).
27
+ # `def.ivar-write-mismatch`'s collector reads the same rule (#2943, {.census_children}).
28
+ module CensusForeignBlock
29
+ INSTANCE_EVALS = %i[instance_eval instance_exec].to_set.freeze
30
+ MODULE_EVALS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
31
+ DEFINERS = %i[define_method define_singleton_method].to_set.freeze
32
+ SEND_CALLS = %i[send public_send __send__].to_set.freeze
33
+ # The constructors whose receiver names the class of the object they answer.
34
+ CONSTRUCTORS = %i[new allocate].to_set.freeze
35
+ # The literal receivers, none of them an instance of a project class.
36
+ LITERALS = [Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode, Prism::IntegerNode,
37
+ Prism::FloatNode, Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::RegularExpressionNode,
38
+ Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::LambdaNode].freeze
39
+ # The calls that answer a module of their receiver.
40
+ MODULE_OF_INSTANCE = %i[class singleton_class].to_set.freeze
41
+ # The constants naming a module every class has among its ancestors, whose methods an instance runs.
42
+ ANCESTOR_ROOTS = %w[Object BasicObject Kernel].to_set.freeze
43
+ # The calls whose answer is foreign when their receiver is: the object itself, a copy, a new instance, its class.
44
+ FOREIGN_PRESERVING = %i[new allocate dup clone itself class singleton_class].to_set.freeze
45
+ # Issue #2983 — the scalar literals, whose blockless calls answer no instance of a project class, unlike a
46
+ # container's (`[k].first`) or a lambda's.
47
+ SCALAR_LITERALS = (LITERALS - [Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode]).freeze
48
+ # A String or Symbol literal that spells a constant path, which a call may resolve to the class
49
+ # (`"K".constantize`).
50
+ CONSTANT_SPELLING = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
51
+ # Separates the qualifications of a {.candidate_key}; no constant name holds it.
52
+ CANDIDATE_SEPARATOR = "|"
53
+ # Marks a candidate key whose code runs on the constant's own value's module ({.root_constant}).
54
+ OBJECT_MARK = "@"
55
+ COPIES = %i[dup clone itself].to_set.freeze
56
+ BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
57
+ NONE = [].freeze
58
+ # Every name {.foreign?} can answer true for, so any other call returns at one Set probe.
59
+ CALLS = (INSTANCE_EVALS | MODULE_EVALS | DEFINERS | %i[new define]).freeze
60
+
61
+ module_function
62
+
63
+ # Whether `node`'s literal block runs with a `self` that is provably not an instance of `class_name`.
64
+ def foreign?(node, class_name)
65
+ unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name) && node.block.is_a?(Prism::BlockNode)
66
+ return false
67
+ end
68
+ return true if AnonymousMetaClass.block_form_receiver(node)
69
+
70
+ name = node.name
71
+ receiver = node.receiver
72
+ return false if self_node?(receiver)
73
+ return true if MODULE_EVALS.include?(name)
74
+ return provably_other?(receiver, class_name) if INSTANCE_EVALS.include?(name)
75
+ return provably_other?(receiver, class_name) if name == :define_singleton_method
76
+
77
+ foreign_constant?(receiver, class_name)
78
+ end
79
+
80
+ # A receiver that is provably no instance of `class_name`: a constant or `self.class` (a class object, `K`
81
+ # itself included), `X.new` / `X.allocate` on a constant `X` not spelling `class_name`, or a literal.
82
+ def provably_other?(receiver, class_name)
83
+ case receiver
84
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then true
85
+ when Prism::CallNode
86
+ return receiver.arguments.nil? if receiver.name == :class
87
+
88
+ CONSTRUCTORS.include?(receiver.name) && foreign_constant?(receiver.receiver, class_name)
89
+ else LITERALS.any? { |klass| receiver.is_a?(klass) }
90
+ end
91
+ end
92
+
93
+ # A constant that does not spell `class_name`.
94
+ def foreign_constant?(node, class_name)
95
+ (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) && !own_class?(node, class_name)
96
+ end
97
+
98
+ # The children of `node` a census walk for `class_name` still descends into when `node` runs code whose `self`
99
+ # is another object, or nil for an ordinary node: a {.foreign?} call keeps its receiver and arguments, and a
100
+ # `class << obj` body its expression; both keep the code nested in them that runs on a `class_name` instance
101
+ # again ({.reentry_blocks}) unless `reentry` is false, as it is in a singleton-side `def`, whose `self.class`
102
+ # and `singleton_class` are no module of an instance. A reentered `def` comes back as the {Prism::DefNode},
103
+ # which a walk that stops at a `def` skips and the class-ivar census walks into (#2941).
104
+ def census_children(node, class_name, reentry: true)
105
+ case node
106
+ when Prism::SingletonClassNode
107
+ return [node.expression] unless reentry
108
+
109
+ [node.expression, *reentry_blocks(node, class_name)]
110
+ when Prism::CallNode
111
+ return nil unless foreign?(node, class_name)
112
+ return [node.receiver, node.arguments].compact unless reentry
113
+
114
+ [node.receiver, node.arguments, *reentry_blocks(node, class_name)].compact
115
+ end
116
+ end
117
+
118
+ # The code inside barred code whose `self` may again be an instance of `class_name`, in a `class_eval`-family
119
+ # block whose module may be `class_name` or the singleton class of one of its instances ({.foreign_module?}) or in
120
+ # a `class << obj` body on an operand {.provably_other?} does not name:
121
+ #
122
+ # - a `define_method` block on the implicit or `self` receiver, called directly or through `send` /
123
+ # `public_send` / `__send__` with the literal name;
124
+ # - a receiver-less `def` (#2941), which defines an instance method of that module, or a singleton method of
125
+ # the `class << obj` operand, just as the `define_method` does.
126
+ #
127
+ # Ruby runs each on a `K` instance (a `K` method, or a singleton method of the instance), so its writes are
128
+ # `K`'s; an untyped receiver keeps the credit, as {.foreign?} keeps it for `instance_eval`.
129
+ def reentry_blocks(node, class_name)
130
+ body =
131
+ case node
132
+ when Prism::SingletonClassNode then node.body unless provably_other?(node.expression, class_name)
133
+ when Prism::CallNode
134
+ node.block.body if MODULE_EVALS.include?(node.name) && !foreign_module?(node.receiver, class_name)
135
+ end
136
+ return NONE if body.nil?
137
+
138
+ found = []
139
+ collect_define_method_blocks(body, found)
140
+ found
141
+ end
142
+
143
+ # A `class_eval` receiver whose module is provably neither `class_name`, an ancestor of it nor an instance's
144
+ # singleton class, read from inside an instance body. A receiver chain is foreign only when it is:
145
+ #
146
+ # - rooted at a constant other than `class_name`'s own spelling and {ANCESTOR_ROOTS} (`Other.class_eval`,
147
+ # `Other.dup`), a `X.new` or `Class.new` on such a constant among them, or rooted at a literal, through
148
+ # {FOREIGN_PRESERVING} calls only;
149
+ # - `.class` / `.singleton_class` on a receiver {.provably_other?} names (`Other.new.class`, `"s".class`,
150
+ # `self.class.class`);
151
+ # - rooted at a scalar literal through blockless calls of any name (`"s".upcase.class`, #2983), unless the
152
+ # literal is a String or Symbol that spells a constant (`"K".constantize`).
153
+ #
154
+ # A `superclass` call may answer an ancestor (`Other.superclass` is `Object`), a `const_get` or registry call may
155
+ # answer the class (`Other.const_get(:K)`), and every other receiver
156
+ # (`self.class`, `singleton_class`, a parameter's `o.class`, `self.class.base_class`) may name `class_name`, so
157
+ # they keep the credit. Parentheses are looked through. The rule is syntactic: a constant naming another
158
+ # ancestor of the class (`Base.class_eval` with `K < Base`), an alias of it or one of its instances reads as
159
+ # foreign here, and the cross-file census files the barred block's code under that constant instead
160
+ # ({.root_constant}), whose entries the class's census joins (#2983).
161
+ def foreign_module?(receiver, class_name)
162
+ receiver = unwrap(receiver)
163
+ return true if scalar_literal_rooted?(receiver)
164
+
165
+ case receiver
166
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
167
+ foreign_constant?(receiver, class_name) && !ANCESTOR_ROOTS.include?(receiver.slice.delete_prefix("::"))
168
+ when Prism::CallNode then foreign_module_call?(receiver, class_name)
169
+ else LITERALS.any? { |klass| receiver.is_a?(klass) }
170
+ end
171
+ end
172
+
173
+ # A call in the chain keeps it foreign only when it answers its receiver's own object, a copy, a new instance
174
+ # or its class ({FOREIGN_PRESERVING}); any other call (`const_get`, a registry's `fetch`, `superclass`) may
175
+ # answer the class, so the receiver may be it.
176
+ def foreign_module_call?(call, class_name)
177
+ if MODULE_OF_INSTANCE.include?(call.name) && call.arguments.nil? &&
178
+ provably_other?(unwrap(call.receiver), class_name)
179
+ return true
180
+ end
181
+ return false unless FOREIGN_PRESERVING.include?(call.name)
182
+
183
+ foreign_module?(call.receiver, class_name)
184
+ end
185
+
186
+ # Issue #2983 — whether `receiver` is a chain of blockless calls rooted at a scalar literal that spells no
187
+ # constant: whatever the calls are, Ruby's methods on such a literal answer no project class or its instance.
188
+ def scalar_literal_rooted?(receiver)
189
+ node = receiver
190
+ while node.is_a?(Prism::CallNode)
191
+ return false if node.block
192
+
193
+ node = unwrap(node.receiver)
194
+ end
195
+ return false if node.equal?(receiver) || SCALAR_LITERALS.none? { |klass| node.is_a?(klass) }
196
+
197
+ !((node.is_a?(Prism::StringNode) || node.is_a?(Prism::SymbolNode)) && CONSTANT_SPELLING.match?(node.unescaped))
198
+ end
199
+
200
+ # Issue #2983 — the constant a `class_eval` receiver chain is rooted at through {FOREIGN_PRESERVING} calls
201
+ # (`Anc`, `INST.class`, `AliasK.new.class`, `Other.singleton_class`), as its candidate key under `nesting`
202
+ # ({.candidate_key}), or nil. The module such a chain names is the constant's, its class's or its singleton's,
203
+ # so the code a barred block reenters runs on the constant's instances or on the object it holds; the
204
+ # cross-file census files it under the key, and a class whose census joins the constant the key resolves to (an
205
+ # ancestor, an alias, an instance constant of it) sees the write.
206
+ #
207
+ # Review round 2 of #3001 — a `.class` or `.singleton_class` taken of the constant's own value (`INST.class`,
208
+ # `X.singleton_class`, through `dup` / `clone` / `itself`) names the module of that object, which is a class
209
+ # instance only when the constant binds one; the key then carries {OBJECT_MARK}, and the join credits it only
210
+ # when the constant resolves to an instance binding (`INST = K.new`). One taken of a class object (`X.class` of a
211
+ # `.class`, `X.new.class.singleton_class`) names `Class` or a metaclass, whose methods no instance runs: nil.
212
+ def root_constant(receiver, nesting)
213
+ node = unwrap(receiver)
214
+ object = false
215
+ while node.is_a?(Prism::CallNode) && FOREIGN_PRESERVING.include?(node.name)
216
+ inner = unwrap(node.receiver)
217
+ if MODULE_OF_INSTANCE.include?(node.name)
218
+ value = through_copies(inner)
219
+ return if value.is_a?(Prism::CallNode) && MODULE_OF_INSTANCE.include?(value.name)
220
+
221
+ object = true if constant_node?(value)
222
+ end
223
+ node = inner
224
+ end
225
+ return unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
226
+
227
+ key = candidate_key(node.slice, nesting)
228
+ object ? "#{OBJECT_MARK}#{key}" : key
229
+ end
230
+
231
+ # `node` with `dup`, `clone` and `itself` calls looked through: the object they answer has the same class.
232
+ def through_copies(node)
233
+ node = unwrap(node.receiver) while node.is_a?(Prism::CallNode) && COPIES.include?(node.name)
234
+ node
235
+ end
236
+
237
+ def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
238
+
239
+ # Review round 1 of #3001 — the qualifications a constant spelled `spelling` may have under the lexical
240
+ # `nesting` (the enclosing class and module names), nearest first as Ruby's lookup tries them, joined by
241
+ # {CANDIDATE_SEPARATOR}: `Anc` under `N::K` is `"N::K::Anc|N::Anc|Anc"`. A rooted `::Anc`, or one at the top
242
+ # level, has the one candidate `"Anc"`. `ScopeIndexer::IvarContributions` resolves the key to the first
243
+ # candidate the project declares.
244
+ def candidate_key(spelling, nesting)
245
+ return spelling.delete_prefix("::") if spelling.start_with?("::") || nesting.empty?
246
+
247
+ nesting.size.downto(0).map { |depth| [*nesting.first(depth), spelling].join("::") }.join(CANDIDATE_SEPARATOR)
248
+ end
249
+
250
+ # Review round 3 of #3001 — whether `call` is `Class.new`, `Module.new`, `Struct.new` or `Data.define` on the
251
+ # bare constant, whose answer is a class.
252
+ def meta_new?(call)
253
+ !AnonymousMetaClass::META_NEW_SELECTORS[AnonymousMetaClass.literal_constant_name(call.receiver)].nil?
254
+ end
255
+
256
+ # Issue #2983 — for a constant write `C = K` or `C = K.new` / `K.allocate` under the lexical `nesting`, the
257
+ # census entry `[:binds_class, key, name, kind]`: `K`'s {.candidate_key}, `C`'s qualified name (a path write's
258
+ # as written) and `:instance` for a constructor or `:alias`, else nil. `C` is an alias or an instance of the
259
+ # class `K` resolves to.
260
+ def binding_entry(node, nesting)
261
+ value = unwrap(node.value)
262
+ instance = value.is_a?(Prism::CallNode) && CONSTRUCTORS.include?(value.name) && value.block.nil?
263
+ # Review round 3 of #3001: `Struct.new(:a)`, `Class.new(Base)` and `Module.new` answer a class, not an instance.
264
+ # A guard beside `IvarContributions.module?`, which drops the same credit once the constant is a declared class.
265
+ return if instance && meta_new?(value)
266
+
267
+ value = value.receiver if instance
268
+ return unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
269
+
270
+ name = node.is_a?(Prism::ConstantWriteNode) ? [*nesting, node.name].join("::") : node.target.slice
271
+ [:binds_class, candidate_key(value.slice, nesting), name.delete_prefix("::"),
272
+ instance ? :instance : :alias].freeze
273
+ end
274
+
275
+ # Issue #2983 — the module a String `class_eval` / `module_eval` may define methods on, for
276
+ # `IvarWriterCensus#record_string_eval`: `owner`, the class owning a class body, for an implicit or `self`
277
+ # receiver there (one in an instance body raises); `instance`, the instance body's class, for a receiver
278
+ # {.foreign_module?} does not bar; otherwise the receiver chain's root constant ({.root_constant}); or nil.
279
+ def string_eval_target(node, instance, owner)
280
+ return (owner unless instance) if self_node?(node.receiver)
281
+ return instance if instance && !foreign_module?(node.receiver, instance)
282
+
283
+ nesting = instance || owner
284
+ root_constant(node.receiver, nesting ? nesting.split("::") : NONE)
285
+ end
286
+
287
+ # For a barred `class_eval`-family block whose receiver {.foreign_module?} takes as another module and whose
288
+ # chain is rooted at a constant ({.root_constant}), that constant's candidate key and the code in the block that
289
+ # runs on its module's instances, as {.reentry_blocks} collects it; else nil.
290
+ def rooted_reentry(node, class_name)
291
+ return unless MODULE_EVALS.include?(node.name) && foreign_module?(node.receiver, class_name)
292
+
293
+ root = root_constant(node.receiver, class_name.split("::"))
294
+ return if root.nil?
295
+
296
+ found = []
297
+ collect_define_method_blocks(node.block.body, found)
298
+ [root, found]
299
+ end
300
+
301
+ # `node` with any parentheses around a single expression looked through.
302
+ def unwrap(node)
303
+ while node.is_a?(Prism::ParenthesesNode)
304
+ body = node.body
305
+ body = body.body.first if body.is_a?(Prism::StatementsNode) && body.body.size == 1
306
+ break if body.nil? || body.is_a?(Prism::StatementsNode)
307
+
308
+ node = body
309
+ end
310
+ node
311
+ end
312
+
313
+ def collect_define_method_blocks(node, found)
314
+ return unless node.is_a?(Prism::Node)
315
+ return found << node if node.is_a?(Prism::DefNode) && node.receiver.nil?
316
+ return if BARRIERS.any? { |klass| node.is_a?(klass) }
317
+
318
+ if node.is_a?(Prism::CallNode)
319
+ return found << node.block if define_method_call?(node)
320
+ return if node.block.is_a?(Prism::BlockNode) && CALLS.include?(node.name)
321
+ end
322
+
323
+ node.rigor_each_child { |child| collect_define_method_blocks(child, found) }
324
+ end
325
+
326
+ # A `define_method` with a literal block on the implicit or `self` receiver, called directly or through a `send`
327
+ # whose first argument is the literal name.
328
+ def define_method_call?(node)
329
+ return false unless node.is_a?(Prism::CallNode) && self_node?(node.receiver)
330
+ return false unless node.block.is_a?(Prism::BlockNode)
331
+ return true if node.name == :define_method
332
+ return false unless SEND_CALLS.include?(node.name)
333
+
334
+ first = node.arguments&.arguments&.first
335
+ (first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)) && first.unescaped == "define_method"
336
+ end
337
+
338
+ # `self.class`, or a constant spelling `class_name`.
339
+ def own_class?(node, class_name)
340
+ case node
341
+ when Prism::CallNode
342
+ node.name == :class && self_node?(node.receiver) && node.arguments.nil?
343
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
344
+ spelled = node.slice.delete_prefix("::")
345
+ spelled == class_name || spelled == class_name.split("::").last
346
+ else
347
+ false
348
+ end
349
+ end
350
+
351
+ def self_node?(node) = node.nil? || node.is_a?(Prism::SelfNode)
352
+ end
353
+ end
354
+ end
@@ -57,7 +57,7 @@ module Rigor
57
57
  widened = names.reduce(scope) do |acc, name|
58
58
  ivar = name.start_with?("@")
59
59
  current = ivar ? scope.ivar(name) : scope.local(name)
60
- next acc if current.nil? || current == untyped
60
+ next acc if current.nil? || Type::Combinator.untyped?(current)
61
61
 
62
62
  ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
63
63
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "../type"
4
4
  require_relative "../reflection"
5
5
  require_relative "external_ancestor_resolution"
6
+ require_relative "../memo_audit"
6
7
  require_relative "project_method_ownership"
7
8
 
8
9
  module Rigor
@@ -185,9 +186,15 @@ module Rigor
185
186
  return false unless scope.known_user_class?(class_name)
186
187
 
187
188
  by_method = (ProjectMethodOwnership.memo(scope)[:ancestry][class_name] ||= {})
188
- return by_method[method_sym] if by_method.key?(method_sym)
189
+ if by_method.key?(method_sym)
190
+ return MemoAudit.check("ancestry non-escaping", by_method[method_sym], class_name, method_sym) do
191
+ compute_ancestry_non_escaping?(class_name, method_sym, scope)
192
+ end
193
+ end
189
194
 
190
- by_method[method_sym] = compute_ancestry_non_escaping?(class_name, method_sym, scope)
195
+ by_method[method_sym] = MemoAudit.key_audit("ancestry non-escaping") do
196
+ compute_ancestry_non_escaping?(class_name, method_sym, scope)
197
+ end
191
198
  end
192
199
 
193
200
  def compute_ancestry_non_escaping?(class_name, method_sym, scope)
@@ -236,7 +243,7 @@ module Rigor
236
243
  OBJECT_NON_ESCAPING = %i[tap then yield_self].freeze
237
244
 
238
245
  ENUMERABLE_NON_ESCAPING = %i[
239
- each map collect flat_map collect_concat
246
+ each reverse_each map collect flat_map collect_concat
240
247
  select filter reject filter_map
241
248
  find detect find_index find_all
242
249
  any? all? none? one? count tally sum
@@ -248,7 +255,7 @@ module Rigor
248
255
  zip
249
256
  ].freeze
250
257
 
251
- ARRAY_EXTRA = %i[each_index].freeze
258
+ ARRAY_EXTRA = %i[each_index rfind index rindex].freeze
252
259
  HASH_EXTRA = %i[
253
260
  each_pair each_key each_value
254
261
  transform_keys transform_values
@@ -3,7 +3,9 @@
3
3
  require "prism"
4
4
  require_relative "../source/node_walker"
5
5
  require_relative "../source/node_children"
6
- require_relative "../run_scoped_slots"
6
+ require_relative "../source/literals"
7
+ require_relative "../typing_state"
8
+ require_relative "../memo_audit"
7
9
 
8
10
  module Rigor
9
11
  module Inference
@@ -26,8 +28,10 @@ module Rigor
26
28
  # A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
27
29
  # class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
28
30
  # any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
29
- # body that calls `binding`, `eval`, `local_variable_set`, `instance_variable_set` or a string-form
30
- # `instance_eval` / `class_eval` / `module_eval`.
31
+ # body that reaches the frame reflectively: a call of `binding`, `eval`, `local_variable_get`,
32
+ # `local_variable_set`, `instance_variable_set` or a string-form `instance_eval` / `class_eval` /
33
+ # `module_eval`, a `send` / `__send__` / `public_send` dispatch whose first argument is one of those names
34
+ # as a literal Symbol or String, or a `TOPLEVEL_BINDING` reference.
31
35
  # The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
32
36
  # the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
33
37
  # scanned too, though its locals are its own; a same-named local there can only over-report an escape.
@@ -59,8 +63,11 @@ module Rigor
59
63
  )
60
64
 
61
65
  # Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
62
- FRAME_CALLS = %i[binding eval local_variable_set instance_variable_set].to_set.freeze
66
+ FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
63
67
  STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
68
+ # The spellings that reach a {FRAME_CALLS} or {STRING_EVAL_CALLS} name through a literal Symbol or String
69
+ # argument (`send(:eval, …)`, `send("eval", …)`).
70
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
64
71
 
65
72
  CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
66
73
  LOCAL_REFERENCES = [
@@ -73,11 +80,15 @@ module Rigor
73
80
  ].freeze
74
81
  ALL = :all
75
82
  EMPTY_CALLS = Set.new.freeze
76
- # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so a long-lived process never serves a verdict
77
- # from a body an earlier run parsed.
78
- CACHE_KEY = RunScopedSlots.register(:__rigor_construction_escape)
79
- private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE_KEY,
80
- :STRING_EVAL_CALLS
83
+ # ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so a long-lived process never serves a verdict from
84
+ # a body an earlier run parsed: keyed by body node, the only input of the scan.
85
+ CACHE = TypingState.memo(
86
+ :construction_escape,
87
+ run_scoped: true, audit: "construction escape",
88
+ reason: "keyed by the body node; the escape scan is a pure function of the body"
89
+ )
90
+ private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE,
91
+ :STRING_EVAL_CALLS, :SEND_CALLS
81
92
 
82
93
  # One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
83
94
  Result = Data.define(:escapes, :calls)
@@ -91,8 +102,7 @@ module Rigor
91
102
  def escaped?(body, name, class_name, scope)
92
103
  return true if body.nil? || class_name.nil?
93
104
 
94
- table = (Thread.current[CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
95
- result = table[body] ||= scan(body)
105
+ result = escape_scan(body)
96
106
  return true if result.equal?(ALL)
97
107
 
98
108
  reason = result.escapes[name]
@@ -122,11 +132,22 @@ module Rigor
122
132
  end
123
133
  end
124
134
 
135
+ # {.scan} of `body`, memoised for the run.
136
+ def escape_scan(body)
137
+ return scan(body) if MemoAudit.enabled? && MemoAudit.suspended?
138
+
139
+ table = TypingState.current.weak_key_map(CACHE)
140
+ stored = table[body]
141
+ return table[body] = MemoAudit.key_audit("construction escape") { scan(body) } if stored.nil?
142
+
143
+ MemoAudit.check("construction escape", stored, body) { scan(body) }
144
+ end
145
+
125
146
  def scan(body)
126
147
  escapes = {}
127
148
  calls = {}
128
149
  Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
129
- return ALL if frame_call?(node)
150
+ return ALL if frame_access?(node)
130
151
  next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
131
152
 
132
153
  record(escapes, calls, node, ancestors)
@@ -148,10 +169,30 @@ module Rigor
148
169
  escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
149
170
  end
150
171
 
151
- def frame_call?(node)
152
- return false unless node.is_a?(Prism::CallNode)
172
+ # A node that reaches the frame's locals by name: a {FRAME_CALLS} or string-eval call, a `send`-family
173
+ # dispatch on a literal frame-call name (`send(:eval, …)`), or a `TOPLEVEL_BINDING` reference under any
174
+ # qualification (a project's own `X::TOPLEVEL_BINDING` escapes too, costing only precision), whose
175
+ # `local_variable_get` / `local_variable_set` reads and rebinds the top frame.
176
+ def frame_access?(node)
177
+ case node
178
+ when Prism::CallNode then frame_call_name?(node)
179
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then node.name == :TOPLEVEL_BINDING
180
+ else false
181
+ end
182
+ end
183
+
184
+ def frame_call_name?(node)
185
+ name = node.name
186
+ return true if FRAME_CALLS.include?(name)
187
+ return true if STRING_EVAL_CALLS.include?(name) && !node.arguments.nil?
188
+ return false unless SEND_CALLS.include?(name)
189
+
190
+ arguments = node.arguments&.arguments
191
+ return false if arguments.nil? || arguments.empty?
153
192
 
154
- FRAME_CALLS.include?(node.name) || (STRING_EVAL_CALLS.include?(node.name) && !node.arguments.nil?)
193
+ dispatched = Source::Literals.symbol_or_string(arguments.first)
194
+ FRAME_CALLS.include?(dispatched) ||
195
+ (STRING_EVAL_CALLS.include?(dispatched) && arguments.size > 1)
155
196
  end
156
197
 
157
198
  # The call a read outside any closure is the receiver of, or nil.
@@ -5,3 +5,7 @@
5
5
  # the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
6
6
  # keeps the two free of a require cycle.
7
7
  require_relative "scope_indexer"
8
+
9
+ # {Rigor::Reflection.class_declarations_in}'s collectors: the plugin-facing walk lives with `Reflection` but needs the
10
+ # walk above, so it is loaded here rather than by `reflection/declarations`, which the engine-free paths load.
11
+ require_relative "../reflection/declaration_collectors"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
  require_relative "def_handle"
5
+ require_relative "../typing_state"
5
6
  require_relative "../source/node_children"
6
7
 
7
8
  module Rigor
@@ -20,23 +21,35 @@ module Rigor
20
21
  # file, and finally nil — conservative (the accessor then reports no user def, losing precision but never
21
22
  # firing a wrong diagnostic), never silent.
22
23
  #
23
- # The memo is a thread-local installed for the duration of a run (the {Cache::FileDigest} pattern:
24
- # process-local, Ractor-safe, never crosses a fork boundary, reset per run). Outside a run scope every
24
+ # The memo is a {TypingState} Frame installed for the duration of a run ({.with_run}) and restored on the way
25
+ # out: per thread, and a fork child inherits whatever is installed at the fork. Outside a run scope every
25
26
  # resolution parses directly — a correct if un-memoised fallback for runner-less probes.
26
27
  module DefNodeResolver
27
- MEMO_KEY = :rigor_def_node_resolver_memo
28
- private_constant :MEMO_KEY
28
+ # ADR-129 WD1 — the run's memo is a Frame: {.with_run} installs it for the run and restores the previous one, and
29
+ # whether one is installed changes answers (outside a run every resolution parses afresh and no `Module.nesting`
30
+ # is recorded). The installed hash is frozen; the four tables in it are extensional caches its owner fills in
31
+ # place without a touch (Memos by kind, whose entry is fixed once written): `nodes` is keyed on
32
+ # `(path, node_id)`, `indexes` and `refinement_queries` on the file's path, and `nestings` on the identity of
33
+ # the node this module minted, holding the nesting the handle's bundle row (or the file's `refine` bodies)
34
+ # recorded for it.
35
+ RUN_MEMO = TypingState.frame(
36
+ :def_node_resolver_run,
37
+ memos: %i[nodes indexes nestings refinement_queries],
38
+ reason: "installed per run by with_run; its tables are extensional caches, keyed on (path, node_id), on the " \
39
+ "path, and (nestings) on the minted node's identity, filled in place",
40
+ # G1′ witness: an incremental recheck types a file against handles of the unchanged files, and
41
+ # {.resolve} reads the run's tables from inside `type_of` (#2874).
42
+ witness: "spec/fixtures/typing_state_witnesses/def_node_resolver_run.rb"
43
+ )
44
+ private_constant :RUN_MEMO
29
45
 
30
46
  # Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
31
47
  # `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index;
32
48
  # `nestings` re-attaches each handle's recorded `Module.nesting` to the node this module minted for it
33
49
  # ({.rehydrated_nesting}).
34
- def self.with_run
35
- previous = Thread.current[MEMO_KEY]
36
- Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }
37
- yield
38
- ensure
39
- Thread.current[MEMO_KEY] = previous
50
+ def self.with_run(&)
51
+ memo = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }.freeze
52
+ TypingState.current.with(RUN_MEMO, memo, &)
40
53
  end
41
54
 
42
55
  # Whether a per-run memo scope is in force. The rehydrated `Module.nesting` ({.rehydrated_nesting}) is
@@ -44,7 +57,12 @@ module Rigor
44
57
  # per-run memo scope" clause states — and lets a spec assert that no handle-consuming entry point
45
58
  # resolves outside it, which is the difference between the peel fallback being a documented edge and
46
59
  # being silently reachable from production.
47
- def self.run_scope? = !Thread.current[MEMO_KEY].nil?
60
+ def self.run_scope? = !TypingState.current[RUN_MEMO].nil?
61
+
62
+ # Issue #3160 — the nodes this run's memo has minted so far, keyed `[path, node_id]` (nil outside a run scope).
63
+ # The return memo keys on them, so the runner keeps the table to find an unchanged file's def in the memo once
64
+ # the run scope has closed.
65
+ def self.resolved_nodes = TypingState.current[RUN_MEMO]&.fetch(:nodes)
48
66
 
49
67
  # Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
50
68
  # nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
@@ -52,7 +70,7 @@ module Rigor
52
70
  def self.resolve(handle)
53
71
  return handle unless handle.is_a?(DefHandle)
54
72
 
55
- memo = Thread.current[MEMO_KEY]
73
+ memo = TypingState.current[RUN_MEMO]
56
74
  return locate(handle, {}) if memo.nil?
57
75
 
58
76
  key = [handle.path, handle.node_id].freeze
@@ -96,7 +114,7 @@ module Rigor
96
114
  # def_node_resolver_spec.rb` pins that, so a new one added outside a run scope fails there rather than
97
115
  # silently reverting to the peel.
98
116
  def self.rehydrated_nesting(node)
99
- memo = Thread.current[MEMO_KEY]
117
+ memo = TypingState.current[RUN_MEMO]
100
118
  memo && memo[:nestings][node]
101
119
  end
102
120
 
@@ -117,7 +135,7 @@ module Rigor
117
135
  # read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
118
136
  # is recorded as a handle's is, so the re-typed body reads its constants where it is written.
119
137
  def self.refinement_query(path)
120
- memo = Thread.current[MEMO_KEY]
138
+ memo = TypingState.current[RUN_MEMO]
121
139
  return build_refinement_query(path, nil) if memo.nil?
122
140
 
123
141
  queries = memo[:refinement_queries]
@@ -126,7 +126,7 @@ module Rigor
126
126
  end
127
127
 
128
128
  def optimistic_call?(node, scope, context)
129
- if scope.optimistic_origins[node]
129
+ if scope.optimistic_origin(node)
130
130
  return false if guarded_lookup?(node, context) || non_empty_end?(node, scope)
131
131
 
132
132
  return true