rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -0,0 +1,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_children"
6
+ require_relative "context"
7
+ require_relative "errors"
8
+ require_relative "shadow"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # ADR-116 WD5 (ADR-53 Theme B): one traversal owns the declaration-context rules that `ScopeIndexer`'s table
13
+ # walkers each copied — what `self`, the cref and `Module.nesting` become under `class`/`module`, `class <<`,
14
+ # a `K = Class.new { … }`-family write, a bare factory block, and an eval-family block. A discovery table
15
+ # becomes a collector that receives events and never tracks context itself; the {Context} it is handed is
16
+ # the walk's.
17
+ #
18
+ # Require `rigor/inference/declaration_walk`, not this file: the rules the walk applies are `ScopeIndexer`
19
+ # functions, and that entry point loads them.
20
+ #
21
+ # The arms, in the order the walk tries them:
22
+ #
23
+ # - `class` / `module` — {Collector#on_declaration}, then the body under {Context#declaration_body}. The
24
+ # header's constant path and superclass expression are not walked. A header that renders no name raises
25
+ # no event and follows the `unrendered_header` rule (below).
26
+ # - `class << expr` — the expression under the enclosing context, the body under
27
+ # {Context#singleton_class_body}.
28
+ # - a constant write (the four spellings that can name a class) — {Collector#on_constant_write}; then, when
29
+ # the rvalue is the meta-new idiom, ONLY the factory call's receiver and arguments, under the enclosing
30
+ # context, and its block body, rebound to the class the write names. Nothing else of the write is
31
+ # walked: not the target's parent (`P` in `P::K = …`), not the guard's read in `K = K || …`, not a
32
+ # `.freeze` tail's call, not the block's parameters, and the factory call itself raises no
33
+ # {Collector#on_call}. A write whose rvalue is not the idiom walks its children, as any node does.
34
+ # - `def` — {Collector#on_def}, then the children.
35
+ # - a call — {Collector#on_call}; then a bare factory block (`Class.new { … }`, `Module.new`, `Struct.new`,
36
+ # `Data.define`) walks its receiver and arguments under the enclosing context and its body with an
37
+ # unnamed `self`, and an eval-family block (`class_eval`, `module_eval`, `class_exec`, `module_exec`,
38
+ # `instance_eval`, `instance_exec`) its receiver and arguments under the enclosing context and its body
39
+ # with `self` rebound to the receiver. Neither walks the block's parameters. Any other call walks its
40
+ # children. A collector following the `:ordinary_call` variant of the `factory_block` rule (below) sees
41
+ # a bare factory block walked as any other call instead.
42
+ #
43
+ # Every other node walks its children under the context it was reached with, in `compact_child_nodes`
44
+ # order, so a collector sees its events in the order the legacy walker it replaces accumulated them.
45
+ #
46
+ # ## The collector protocol
47
+ #
48
+ # A collector includes {Collector} and overrides the events it needs. Each handler answers {DESCEND} or
49
+ # {DECLINE}: {DECLINE} takes the node's whole subtree away from THIS collector, while the other collectors
50
+ # of the same run continue into it. Any other answer descends. The walk dispatches only the events some
51
+ # collector of the run overrides.
52
+ #
53
+ # ## Variants
54
+ #
55
+ # Where the legacy walkers disagree on a context rule, a port keeps its walker's answer by naming a
56
+ # variant of the rule in its class's `VARIANTS` (ADR-116 WD5; {RULE_VARIANTS} lists them). Collectors on
57
+ # different variants of a rule still share a run: where the variants give a subtree different contexts
58
+ # (a bare factory block, a meta-new or eval-family body, a header that renders no name) the walk goes down
59
+ # once per variant in use, each collector only in its own, and once for all of them everywhere else. A
60
+ # variant is a legacy answer kept on purpose, and #1521 tracks converging each one.
61
+ #
62
+ # The rule walk stays separate (ADR-53 rejected folding rule collectors into indexing): this walk only
63
+ # builds discovery tables.
64
+ module DeclarationWalk
65
+ # A handler's answers: go on into the node, or leave its subtree to the run's other collectors.
66
+ DESCEND = :descend
67
+ DECLINE = :decline
68
+
69
+ # Each context rule a collector may follow a legacy variant of, with its variants. The first is the
70
+ # walk's own rule, which a collector follows unless its `VARIANTS` names another.
71
+ #
72
+ # - `factory_block` — a bare `Class.new { … }` / `Module.new` / `Struct.new` / `Data.define` block.
73
+ # `:unnamed_self` walks the factory's receiver and arguments, then the body with an unnamed `self`,
74
+ # and skips the block's parameters (`walk_class_cvars`' rule). `:ordinary_call` walks the call's
75
+ # children like any call's — receiver, arguments, the block's parameters and body — under the
76
+ # enclosing context, so a `self::` header in the body anchors on the enclosing self
77
+ # (`walk_class_superclasses`' rule; `class C; Class.new { class self::E < S; end }; end` files
78
+ # `C::E`, a class Ruby never creates; #1521 item 8).
79
+ # - `anonymous_class_path` — the file path an anonymous class's synthetic name carries; answered by
80
+ # {Context#anonymous_class_path}, which documents `:whole_file` and `:outside_class_bodies` (#1521
81
+ # item 11).
82
+ # - `unrendered_header` — a `class` / `module` whose header renders no name, which only a parse error
83
+ # produces (`class foo`, a `module` keyword followed by a `def`). `:children` walks every child, the
84
+ # header's parts included, under the enclosing context (`walk_class_superclasses`' rule).
85
+ # `:skip` walks nothing below it (the member-layout walkers'). `:body_with_lost_nesting` walks the body
86
+ # alone under {Context#lost_header_body} (`walk_def_nestings`'). #1521 item 3.
87
+ # - `lexical_prefix` — the prefix meta-new and eval-family splits resolve against, answered by
88
+ # {Context#lexical_prefix}: `:prefix` or `walk_def_nestings`' `:nesting_head` (#1521 item 1).
89
+ RULE_VARIANTS = {
90
+ factory_block: %i[unnamed_self ordinary_call].freeze,
91
+ anonymous_class_path: %i[whole_file outside_class_bodies].freeze,
92
+ unrendered_header: %i[children skip body_with_lost_nesting].freeze,
93
+ lexical_prefix: %i[prefix nesting_head].freeze
94
+ }.freeze
95
+
96
+ # The event handlers a collector may override; each answers {DESCEND} until overridden.
97
+ module Collector
98
+ EVENTS = %i[on_declaration on_def on_call on_constant_write].freeze
99
+
100
+ # The legacy variants of {RULE_VARIANTS} this collector follows, `rule => variant`. A collector
101
+ # overrides the constant, documenting each entry where it declares it.
102
+ VARIANTS = {}.freeze
103
+
104
+ # A `class` / `module` header. `body` is the context its body is walked under; it exists even when
105
+ # the declaration has no body.
106
+ def on_declaration(_node, _context, _body)
107
+ DESCEND
108
+ end
109
+
110
+ def on_def(_node, _context)
111
+ DESCEND
112
+ end
113
+
114
+ # Every call, the factory and eval-family calls included, before the walk enters its block.
115
+ def on_call(_node, _context)
116
+ DESCEND
117
+ end
118
+
119
+ # `K = …`, `P::K = …`, `K ||= …` and `P::K ||= …`, before the walk decides whether the rvalue opens a
120
+ # meta-new body.
121
+ def on_constant_write(_node, _context)
122
+ DESCEND
123
+ end
124
+
125
+ # The {EVENTS} `klass` overrides. Answered once per class: the table is a module ivar, which only the
126
+ # main Ractor may touch, so a pool worker on the Ractor backend recomputes it instead (the #1055
127
+ # pattern in `FactStore::Target.local`); the answer is the same either way.
128
+ def self.events_of(klass)
129
+ return overridden_events(klass) unless Ractor.main?
130
+
131
+ @events_by_class[klass] ||= overridden_events(klass)
132
+ end
133
+ @events_by_class = {}.compare_by_identity
134
+
135
+ def self.overridden_events(klass)
136
+ EVENTS.reject { |event| klass.instance_method(event).owner.equal?(self) }.freeze
137
+ end
138
+ private_class_method :overridden_events
139
+
140
+ # The variant of `rule` `klass` follows: its `VARIANTS` entry, or the walk's own rule.
141
+ def self.variant_of(klass, rule)
142
+ klass::VARIANTS.fetch(rule) { RULE_VARIANTS.fetch(rule).first }
143
+ end
144
+
145
+ # Raises {UnknownVariant} unless every `VARIANTS` entry of `klass` names a rule and one of its
146
+ # variants, so a misspelt variant is a {ContractError} rather than the walk's rule followed silently:
147
+ # an error row on the file in a file's own index, and an aborted run in the project pre-pass.
148
+ def self.check_variants!(klass)
149
+ klass::VARIANTS.each do |rule, variant|
150
+ next if RULE_VARIANTS.fetch(rule, EMPTY).include?(variant)
151
+
152
+ raise UnknownVariant, "#{klass}: no #{rule.inspect} variant #{variant.inspect} (ADR-116 WD5)"
153
+ end
154
+ end
155
+ EMPTY = [].freeze
156
+ private_constant :EMPTY
157
+ end
158
+
159
+ NO_COLLECTORS = [].freeze
160
+ private_constant :NO_COLLECTORS
161
+
162
+ module_function
163
+
164
+ # Walks `root` once for every collector, from `context` (a file's top level by default).
165
+ def run(root, collectors, context = Context.root)
166
+ Traversal.for(collectors).walk(root, collectors, context)
167
+ collectors
168
+ end
169
+
170
+ # One run's dispatch: which events any collector overrides, so an event none of them handles costs no
171
+ # call per node.
172
+ class Traversal
173
+ # The traversal for `collectors`. A single-collector run's depends on the collector's class alone, so
174
+ # the main Ractor builds it once per class (the #1055 pattern in `FactStore::Target.local`); every
175
+ # other run builds its own.
176
+ def self.for(collectors)
177
+ return new(collectors) unless collectors.size == 1 && Ractor.main?
178
+
179
+ @single_runs[collectors.first.class] ||= new(collectors)
180
+ end
181
+ @single_runs = {}.compare_by_identity
182
+
183
+ def initialize(collectors)
184
+ @declarations = handles?(collectors, :on_declaration)
185
+ @defs = handles?(collectors, :on_def)
186
+ @calls = handles?(collectors, :on_call)
187
+ @constant_writes = handles?(collectors, :on_constant_write)
188
+ collectors.each { |collector| Collector.check_variants!(collector.class) }
189
+ @ordinary_factories = collectors.any? { |collector| ordinary_factory?(collector) }
190
+ @unnamed_factories = !collectors.all? { |collector| ordinary_factory?(collector) }
191
+ @unrendered_variants = collectors.any? { |collector| variant(collector, :unrendered_header) != :children }
192
+ @nesting_heads = collectors.any? { |collector| variant(collector, :lexical_prefix) == :nesting_head }
193
+ # Only a multi-collector run keeps its collectors, which a cached single-collector traversal must
194
+ # not hold on to, and the subsets it builds from them as it first needs each ({#subset}).
195
+ if collectors.size > 1
196
+ @run = collectors
197
+ @subsets = {}
198
+ end
199
+ freeze
200
+ end
201
+
202
+ def walk(node, collectors, context) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
203
+ return unless node.is_a?(Prism::Node)
204
+
205
+ # The arms are disjoint node classes, so their order is only a cost: the commonest kind is tried first.
206
+ case node
207
+ when Prism::CallNode
208
+ collectors = descending(collectors) { |c| c.on_call(node, context) } if @calls
209
+ return if collectors.empty?
210
+ # Both block arms need a literal block, which most calls lack.
211
+ return if node.block.is_a?(Prism::BlockNode) &&
212
+ (walk_factory_call?(node, collectors, context) || walk_eval_call?(node, collectors, context))
213
+ when Prism::DefNode
214
+ collectors = descending(collectors) { |c| c.on_def(node, context) } if @defs
215
+ return if collectors.empty?
216
+ when Prism::ClassNode, Prism::ModuleNode
217
+ return if walk_declaration?(node, collectors, context)
218
+ when Prism::SingletonClassNode
219
+ return walk_singleton_class(node, collectors, context)
220
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
221
+ Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode
222
+ collectors = descending(collectors) { |c| c.on_constant_write(node, context) } if @constant_writes
223
+ return if collectors.empty? || walk_meta_new_write?(node, collectors, context)
224
+ end
225
+
226
+ node.rigor_each_child { |child| walk(child, collectors, context) }
227
+ end
228
+
229
+ private
230
+
231
+ def handles?(collectors, event)
232
+ collectors.any? { |collector| Collector.events_of(collector.class).include?(event) }
233
+ end
234
+
235
+ # A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
236
+ # each collector alone (a pair in which one declines), the run without each collector (a larger run in
237
+ # which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
238
+ # them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
239
+ def subset(kind, collector = nil)
240
+ table = (@subsets[kind] ||= {}.compare_by_identity)
241
+ key = collector || kind
242
+ table.fetch(key) { table[key] = yield.freeze }
243
+ end
244
+
245
+ def alone(collector)
246
+ subset(:alone, collector) { [collector] }
247
+ end
248
+
249
+ def variant_split(rule, &)
250
+ subset(rule) do
251
+ second, first = @run.partition(&)
252
+ [first.freeze, second.freeze]
253
+ end
254
+ end
255
+
256
+ def variant(collector, rule)
257
+ Collector.variant_of(collector.class, rule)
258
+ end
259
+
260
+ def ordinary_factory?(collector)
261
+ variant(collector, :factory_block) == :ordinary_call
262
+ end
263
+
264
+ # The collectors that let the walk into the node the block asks each of them about. Every collector is
265
+ # asked, in order, whatever the others answer.
266
+ #
267
+ # Asking allocates nothing, so a node where no collector declines costs no object. Where exactly one
268
+ # declines, the answer is a {#subset} memoised for the run (the one survivor of a pair, or the whole run
269
+ # without the decliner), built the first time and free after that. Two cases copy instead: a decline
270
+ # inside a group of three or more that is not the whole run (a variant fork's group, or what is left of
271
+ # a run of four or more after an earlier decline), and two or more declines at one node of a group of
272
+ # three or more. Each allocates one Array.
273
+ def descending(collectors, &)
274
+ case collectors.size
275
+ when 0 then collectors
276
+ when 1 then yield(collectors.first) == DECLINE ? NO_COLLECTORS : collectors
277
+ when 2 then descending_pair(collectors, yield(collectors.first), yield(collectors.last))
278
+ else descending_many(collectors, &)
279
+ end
280
+ end
281
+
282
+ # An index loop rather than `each_with_index`, which allocates a block frame object (a T_IMEMO) per
283
+ # call: this runs at every node a run of three or more dispatches an event for.
284
+ def descending_many(collectors)
285
+ declined = nil
286
+ kept = nil
287
+ index = 0
288
+ while index < collectors.size
289
+ collector = collectors[index]
290
+ if yield(collector) != DECLINE
291
+ kept&.push(collector)
292
+ elsif declined.nil? && kept.nil?
293
+ declined = index
294
+ else
295
+ kept ||= collectors.take(index).tap { |list| list.delete_at(declined) }
296
+ end
297
+ index += 1
298
+ end
299
+ kept || (declined.nil? ? collectors : without(collectors, declined))
300
+ end
301
+
302
+ def without(collectors, index)
303
+ declined = collectors[index]
304
+ if collectors.equal?(@run)
305
+ return subset(:without, declined) { @run.reject { |collector| collector.equal?(declined) } }
306
+ end
307
+
308
+ collectors.dup.tap { |list| list.delete_at(index) }
309
+ end
310
+
311
+ def descending_pair(collectors, first, last)
312
+ if first == DECLINE
313
+ last == DECLINE ? NO_COLLECTORS : alone(collectors.last)
314
+ else
315
+ last == DECLINE ? alone(collectors.first) : collectors
316
+ end
317
+ end
318
+
319
+ # False only for a header that renders no prefix while every collector here follows the walk's
320
+ # `unrendered_header` rule, which then walks the node's children like any node's.
321
+ def walk_declaration?(node, collectors, context)
322
+ body_context = context.declaration_body(node)
323
+ return walk_unrendered_header?(node, collectors, context) if body_context.nil?
324
+
325
+ collectors = descending(collectors) { |c| c.on_declaration(node, context, body_context) } if @declarations
326
+ body = node.body
327
+ walk(body, collectors, body_context) if body && !collectors.empty?
328
+ true
329
+ end
330
+
331
+ # The `unrendered_header` rule: `:children` collectors walk every child under the enclosing context,
332
+ # `:body_with_lost_nesting` ones the body alone under {Context#lost_header_body}, `:skip` ones nothing.
333
+ def walk_unrendered_header?(node, collectors, context)
334
+ return false unless @unrendered_variants
335
+
336
+ children = collectors.select { |collector| variant(collector, :unrendered_header) == :children }
337
+ node.rigor_each_child { |child| walk(child, children, context) } unless children.empty?
338
+ lost = collectors.select { |collector| variant(collector, :unrendered_header) == :body_with_lost_nesting }
339
+ walk(node.body, lost, context.lost_header_body) if node.body && !lost.empty?
340
+ true
341
+ end
342
+
343
+ def walk_singleton_class(node, collectors, context)
344
+ walk(node.expression, collectors, context)
345
+ body = node.body
346
+ walk(body, collectors, context.singleton_class_body) if body
347
+ end
348
+
349
+ def walk_meta_new_write?(node, collectors, context)
350
+ enclosing, body, body_self = context.meta_new_split(node)
351
+ return false if enclosing.nil?
352
+
353
+ enclosing.each { |part| walk(part, collectors, context) }
354
+ walk_rebound_body(node, body, collectors, context, body_self, :meta_new) if body
355
+ true
356
+ end
357
+
358
+ # A meta-new or eval-family body, with `self` rebound to `owner`. The `lexical_prefix` rule decides
359
+ # `owner`, so where the run mixes its variants the `:nesting_head` collectors take the owner their
360
+ # prefix gives, and the body is walked once per distinct owner — once in all where the two agree.
361
+ def walk_rebound_body(node, body, collectors, context, owner, kind)
362
+ return walk(body, collectors, rebound(context, owner, kind)) unless @nesting_heads
363
+
364
+ prefixes, heads = prefix_groups(collectors)
365
+ head_owner = heads.empty? ? owner : split(node, context, kind, :nesting_head)[2]
366
+ if head_owner == owner
367
+ walk(body, collectors, rebound(context, owner, kind))
368
+ else
369
+ walk(body, prefixes, rebound(context, owner, kind)) unless prefixes.empty?
370
+ walk(body, heads, rebound(context, head_owner, kind))
371
+ end
372
+ end
373
+
374
+ def rebound(context, owner, kind)
375
+ kind == :eval ? context.eval_body(owner) : context.meta_new_body(owner)
376
+ end
377
+
378
+ def split(node, context, kind, lexical_variant)
379
+ kind == :eval ? context.eval_split(node, lexical_variant) : context.meta_new_split(node, lexical_variant)
380
+ end
381
+
382
+ # `[prefix, nesting_head]` collectors of a run that mixes the `lexical_prefix` variants.
383
+ def prefix_groups(collectors)
384
+ if collectors.equal?(@run)
385
+ return variant_split(:lexical_prefix) { |collector| variant(collector, :lexical_prefix) == :nesting_head }
386
+ end
387
+
388
+ heads, prefixes = collectors.partition { |collector| variant(collector, :lexical_prefix) == :nesting_head }
389
+ [prefixes, heads]
390
+ end
391
+
392
+ # The `factory_block` rule, for a call with a literal block. False when every collector here follows
393
+ # `:ordinary_call`, so the call walks its children like any call. Otherwise the receiver and arguments
394
+ # are walked once, for every collector, since both variants walk them under the enclosing context;
395
+ # the variants part only at the block: the `:unnamed_self` collectors walk its body with an unnamed
396
+ # `self`, the `:ordinary_call` ones the whole block, parameters included, under the enclosing context.
397
+ # Each collector sees the same events in the same order as in a run of its own.
398
+ def walk_factory_call?(node, collectors, context)
399
+ return false unless @unnamed_factories && ScopeIndexer.meta_new_constant_rvalue?(node)
400
+
401
+ unnamed = unnamed_factory_group(collectors)
402
+ return false if unnamed.empty?
403
+
404
+ walk(node.receiver, collectors, context)
405
+ node.arguments&.arguments&.each { |argument| walk(argument, collectors, context) }
406
+ block = node.block
407
+ walk(block.body, unnamed, context.factory_body) if block.body
408
+ walk(block, ordinary_factory_group(collectors), context) if unnamed.size < collectors.size
409
+ true
410
+ end
411
+
412
+ def unnamed_factory_group(collectors)
413
+ return collectors unless @ordinary_factories
414
+ return factory_split.first if collectors.equal?(@run)
415
+
416
+ collectors.reject { |collector| ordinary_factory?(collector) }
417
+ end
418
+
419
+ def ordinary_factory_group(collectors)
420
+ return factory_split.last if collectors.equal?(@run)
421
+
422
+ collectors.select { |collector| ordinary_factory?(collector) }
423
+ end
424
+
425
+ # `[unnamed_self, ordinary_call]` collectors of the whole run.
426
+ def factory_split
427
+ variant_split(:factory_block) { |collector| ordinary_factory?(collector) }
428
+ end
429
+
430
+ def walk_eval_call?(node, collectors, context)
431
+ enclosing, body, eval_self = context.eval_split(node)
432
+ return false if enclosing.nil?
433
+
434
+ enclosing.each { |part| walk(part, collectors, context) }
435
+ walk_rebound_body(node, body, collectors, context, eval_self, :eval) if body
436
+ true
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ADR-116 WD5 — the entry point for {Rigor::Inference::DeclarationWalk}. The walk lives in
4
+ # `declaration_walk/traversal`, and the context rules it applies are `ScopeIndexer` functions, so this file loads
5
+ # the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
6
+ # keeps the two free of a require cycle.
7
+ require_relative "scope_indexer"
@@ -33,7 +33,7 @@ module Rigor
33
33
  # ({.rehydrated_nesting}).
34
34
  def self.with_run
35
35
  previous = Thread.current[MEMO_KEY]
36
- Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity }
36
+ Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }
37
37
  yield
38
38
  ensure
39
39
  Thread.current[MEMO_KEY] = previous
@@ -76,8 +76,9 @@ module Rigor
76
76
  # walk over the analyzer's own parse produced. This holds per NODE and needs nothing about files.
77
77
  # 2. AGREEMENT — where both tables answer for the same DEF (through different objects), they answer the
78
78
  # same chain. A bundle is only reused when the file's content SHA-256 matches
79
- # ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and `build_def_nestings` is a
80
- # pure function of that file's AST, so identical bytes yield an identical chain.
79
+ # ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and the def-nesting table
80
+ # (`ScopeIndexer.declaration_walk_tables`) is a pure function of that file's AST, so identical bytes
81
+ # yield an identical chain.
81
82
  #
82
83
  # What is NOT true — and was asserted here before it was measured — is that a file takes exactly ONE of
83
84
  # the two branches per run. It does so in the cross-file pre-pass only. An UNCHANGED file re-analysed as
@@ -112,6 +113,28 @@ module Rigor
112
113
  end
113
114
  private_class_method :record_nesting
114
115
 
116
+ # Issue #1664 — the {InEffectRefinements} over `path`'s own parse, once a run, or nil when the file cannot be
117
+ # read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
118
+ # is recorded as a handle's is, so the re-typed body reads its constants where it is written.
119
+ def self.refinement_query(path)
120
+ memo = Thread.current[MEMO_KEY]
121
+ return build_refinement_query(path, nil) if memo.nil?
122
+
123
+ queries = memo[:refinement_queries]
124
+ return queries[path] if queries.key?(path)
125
+
126
+ queries[path] = build_refinement_query(path, memo)
127
+ end
128
+
129
+ def self.build_refinement_query(path, memo)
130
+ query = InEffectRefinements.new(Prism.parse(File.read(path)).value)
131
+ query.refine_def_nestings.each { |node, nesting| record_nesting(memo, node, nesting) } if memo
132
+ query
133
+ rescue StandardError
134
+ nil
135
+ end
136
+ private_class_method :build_refinement_query
137
+
115
138
  # Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
116
139
  def self.locate(handle, index_cache)
117
140
  index = index_cache[handle.path] ||= build_file_index(handle.path)