Module: Rigor::Inference::ScopeIndexer

Defined in:
lib/rigor/inference/scope_indexer.rb

Overview

Builds a per-node scope index for a Prism program by running ‘Rigor::Inference::StatementEvaluator` over the root and recording the entry scope visible at every node. Expression-interior nodes the evaluator does not specialise (call receivers, arguments, array/hash elements, …) inherit their nearest statement-y ancestor’s recorded scope, so a downstream caller that looks up the scope for any Prism node in the tree always gets the scope that was effectively visible at that point.

The CLI commands ‘rigor type-of` and `rigor type-scan` consume the index so that local-variable bindings established earlier in the program are visible to the typer when probing later nodes. Without the index, both commands would type every node under an empty scope and miss the constant-folding / dispatch precision that Slice 3 phase 2’s StatementEvaluator unlocks.

The returned object is an identity-comparing Hash:

“‘ruby index = Rigor::Inference::ScopeIndexer.index(program, default_scope: Scope.empty) index #=> the Rigor::Scope visible at that node “`

Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the fact) yield the ‘default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only; the indexer itself never exposes a way to update it past construction. rubocop:disable Metrics/ModuleLength

Constant Summary collapse

TOP_LEVEL_DEF_KEY =

v0.0.3 A — sentinel key under which ‘record_def_node` files DefNodes that live outside any class / module body (top-level helpers, `def`s nested inside DSL blocks like `RSpec.describe … do; def helper; end`). Looked up by `Scope#top_level_def_for` to give implicit-self calls priority over RBS dispatch when the file defines a same-named local method.

"<toplevel>"
MIXIN_CALL_NAMES =
%i[include prepend].freeze
VISIBILITY_MODIFIERS =
%i[public private protected].freeze

Class Method Summary collapse

Class Method Details

.apply_alias_def_nodes(root, accumulator) ⇒ Object

Post-pass over the ‘def_nodes` accumulator: for every `alias` declaration inside a class body, if the original method name maps to a `Prism::DefNode`, register the new name pointing to the same node so inter-procedural return-type inference works for the aliased name.



846
847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'lib/rigor/inference/scope_indexer.rb', line 846

def apply_alias_def_nodes(root, accumulator)
  alias_map = collect_class_alias_map(root, [], {})
  alias_map.each do |class_name, aliases|
    class_defs = accumulator[class_name]
    next unless class_defs

    aliases.each do |new_name, old_name|
      def_node = class_defs[old_name]
      next unless def_node.is_a?(Prism::DefNode)

      (accumulator[class_name] ||= {})[new_name] = def_node
    end
  end
end

.apply_named_visibility(args, qualified_prefix, visibility, accumulator) ⇒ Object



809
810
811
812
813
814
815
816
817
818
# File 'lib/rigor/inference/scope_indexer.rb', line 809

def apply_named_visibility(args, qualified_prefix, visibility, accumulator)
  class_name = qualified_prefix.join("::")
  args.each do |arg|
    name = visibility_target_name(arg)
    next if name.nil?

    accumulator[class_name] ||= {}
    accumulator[class_name][name] = visibility
  end
end

.apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator) ⇒ Object

Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated) current visibility:

  • ‘private` / `public` / `protected` (no args) —switch the running default for subsequent defs.

  • ‘private :foo, :bar` — back-patch the named methods in the accumulator. Returns `current_visibility` unchanged because the running default does NOT change for this form.



795
796
797
798
799
800
801
802
803
804
805
806
807
# File 'lib/rigor/inference/scope_indexer.rb', line 795

def apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator)
  return current_visibility unless call_node.receiver.nil?
  return current_visibility unless VISIBILITY_MODIFIERS.include?(call_node.name)
  return current_visibility if qualified_prefix.empty?

  args = call_node.arguments&.arguments || []
  if args.empty?
    call_node.name
  else
    apply_named_visibility(args, qualified_prefix, call_node.name, accumulator)
    current_visibility
  end
end

.build_class_cvar_index(root, default_scope) ⇒ Object

Slice 7 phase 6 — class-cvar pre-pass. Same shape as the ivar pre-pass but collects ‘Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class, because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton method bodies through `Scope#class_cvars_for`.



247
248
249
250
251
# File 'lib/rigor/inference/scope_indexer.rb', line 247

def build_class_cvar_index(root, default_scope)
  accumulator = {}
  walk_class_cvars(root, [], default_scope, accumulator)
  accumulator.transform_values(&:freeze).freeze
end

.build_class_ivar_index(root, default_scope) ⇒ Object

Slice 7 phase 2. Builds the class-level ivar accumulator by walking every ‘Prism::ClassNode` / `Prism::ModuleNode` body, descending into each nested `Prism::DefNode`, and typing every `Prism::InstanceVariableWriteNode` rvalue under a scope that carries the appropriate `self_type` for that def (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level threading — so `@x = 1` records `Constant` but `@x = some_local + 1` records `Dynamic` (since `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.



173
174
175
176
177
# File 'lib/rigor/inference/scope_indexer.rb', line 173

def build_class_ivar_index(root, default_scope)
  accumulator = {}
  walk_class_ivars(root, [], default_scope, accumulator)
  accumulator.transform_values(&:freeze).freeze
end

.build_declaration_artifacts(root) ⇒ Object

Walks the program once for ‘Prism::ModuleNode` and `Prism::ClassNode`, recording the `Singleton` type for the outermost `constant_path` node of each declaration. Inner segments of a `class Foo::Bar::Baz` path remain real references (resolved through the ordinary lexical walk), so we annotate ONLY the topmost path node. Nested declarations contribute their fully qualified path: `class A::B; class C; …` produces `A::B` for the outer and `A::B::C` for the inner.



1030
1031
1032
1033
1034
1035
# File 'lib/rigor/inference/scope_indexer.rb', line 1030

def build_declaration_artifacts(root)
  identity_table = {}.compare_by_identity
  discovered = {}
  record_declarations(root, [], identity_table, discovered)
  [identity_table.freeze, discovered.freeze]
end

.build_discovered_def_nodes(root) ⇒ Object

v0.0.2 #5 — instance-side def-node recording. Walks class bodies the same way as ‘build_discovered_methods` but records the actual `Prism::DefNode` for each instance method so `ExpressionTyper` can re-type the body at the call site for inter-procedural return inference. Singleton methods and `define_method` calls are intentionally skipped: the inference path needs a statically introspectable body, and singleton dispatch has its own complications (Class / Module ancestry) the first-iteration rule does not yet model.



549
550
551
552
553
554
# File 'lib/rigor/inference/scope_indexer.rb', line 549

def build_discovered_def_nodes(root)
  accumulator = {}
  walk_def_nodes(root, [], false, accumulator)
  apply_alias_def_nodes(root, accumulator)
  accumulator.transform_values(&:freeze).freeze
end

.build_discovered_includes(root) ⇒ Object

ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module names it ‘include`s / `prepend`s, AS WRITTEN at the mixin call (`include Foo` / `include Foo::Bar`). Only constant arguments are recorded; dynamic mixins (`include some_method`) produce no entry. `prepend` is bucketed with `include` — both contribute instance methods to the ancestor chain. `extend` is NOT tracked (it adds singleton methods; ADR-24 slice 2 resolves the instance-side chain).



659
660
661
662
663
# File 'lib/rigor/inference/scope_indexer.rb', line 659

def build_discovered_includes(root)
  accumulator = {}
  walk_class_includes(root, [], nil, accumulator)
  accumulator.transform_values { |mods| mods.uniq.freeze }.freeze
end

.build_discovered_method_visibilities(root) ⇒ Object

v0.1.2 — per-class method-visibility table for the ‘def.method-visibility-mismatch` CheckRule.

Tracks two visibility-changing forms:

  • **Modifier blocks**: a bare ‘private` / `protected` / `public` call inside a class body switches the “current default” visibility for every subsequent `def` until another modifier flips it again.

  • **Named-argument form**: ‘private :foo, :bar` (or the same with `protected` / `public`) marks specific names already-recorded under the class. Symbol-only args are recognised; `private def foo; end` (the wrap-around form) is not yet — it would need tracking the def-call’s return-value visibility, which is a separate slice.

Top-level (no surrounding class) defs do not contribute — Ruby’s top-level visibility nuances (private at top-level marks the method on ‘Object`) are out of scope for v0.1.2.



718
719
720
721
722
# File 'lib/rigor/inference/scope_indexer.rb', line 718

def build_discovered_method_visibilities(root)
  accumulator = {}
  walk_method_visibilities(root, [], false, :public, accumulator)
  accumulator.transform_values(&:freeze).freeze
end

.build_discovered_methods(root) ⇒ Object

Slice 7 phase 12 — in-source method discovery pre-pass. Walks every class/module body and records the methods introduced via ‘Prism::DefNode` (instance + singleton) and via recognised `define_method(:name) { … }` calls. The returned table maps qualified class name to a `Hash[Symbol, :instance | :singleton]`.



396
397
398
399
400
# File 'lib/rigor/inference/scope_indexer.rb', line 396

def build_discovered_methods(root)
  accumulator = {}
  walk_methods(root, [], false, accumulator)
  accumulator.transform_values(&:freeze).freeze
end

.build_discovered_superclasses(root) ⇒ Object

ADR-24 slice 2 — per-class table mapping a fully qualified user class to its superclass name AS WRITTEN at the ‘class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(…)` and other non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call site against the subclass’s lexical nesting —see ‘ExpressionTyper#resolve_ancestor_class_name`.



615
616
617
618
619
# File 'lib/rigor/inference/scope_indexer.rb', line 615

def build_discovered_superclasses(root)
  accumulator = {}
  walk_class_superclasses(root, [], accumulator)
  accumulator.freeze
end

.build_in_source_constants(root, default_scope) ⇒ Object

Slice 7 phase 9 — in-source constant value pre-pass. Walks the entire program (top-level AND inside class / module / def bodies) for ‘Prism::ConstantWriteNode` and `Prism::ConstantPathWriteNode`, types each rvalue, and accumulates by qualified name. Constants defined inside a class body are qualified with the surrounding class path; constants written via a path (`Foo::BAR = …`) use the rendered path as-is.



333
334
335
336
337
# File 'lib/rigor/inference/scope_indexer.rb', line 333

def build_in_source_constants(root, default_scope)
  accumulator = {}
  walk_constant_writes(root, [], default_scope, accumulator)
  accumulator.freeze
end

.build_program_global_index(root, default_scope) ⇒ Object

Slice 7 phase 6 — program-global pre-pass. Globals are process-wide so the accumulator is a flat ‘Hash[Symbol, Type::t]` populated from every `Prism::GlobalVariableWriteNode` in the program (top-level AND inside method bodies). The same accumulator is seeded into every method body and the top-level scope.



305
306
307
308
309
# File 'lib/rigor/inference/scope_indexer.rb', line 305

def build_program_global_index(root, default_scope)
  accumulator = {}
  gather_global_writes(root, default_scope, accumulator)
  accumulator.freeze
end

.class_new_call?(node) ⇒ Boolean

Recognises ‘Class.new`, `Class.new(super_class)`, and the block form `Class.new { … }`. Like `module_new_call?`, the block body is walked as the anonymous class’s body. The optional ‘super_class` positional is accepted but does NOT route through `ancestor` discovery in this slice — the synthesised class still answers method lookups via its own body’s defs, mirroring how ‘Struct.new` / `Data.define` are handled.

Returns:

  • (Boolean)


1141
1142
1143
# File 'lib/rigor/inference/scope_indexer.rb', line 1141

def class_new_call?(node)
  meta_call_with_name?(node, :Class, :new)
end

.collect_class_alias_map(node, qualified_prefix, accumulator) ⇒ Object

Builds a map ‘=> {new_name_sym => old_name_sym}` by walking the tree for `AliasMethodNode` nodes inside class bodies.



863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
# File 'lib/rigor/inference/scope_indexer.rb', line 863

def collect_class_alias_map(node, qualified_prefix, accumulator)
  return accumulator unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      collect_class_alias_map(node.body, qualified_prefix + [name], accumulator) if node.body
      return accumulator
    end
  when Prism::SingletonClassNode
    return accumulator
  when Prism::AliasMethodNode
    record_alias_map_entry(node, qualified_prefix, accumulator)
    return accumulator
  end

  node.compact_child_nodes.each { |child| collect_class_alias_map(child, qualified_prefix, accumulator) }
  accumulator
end

.collect_class_decls(node, qualified_prefix, accumulator) ⇒ Object

Class-only variant of ‘record_declarations` — descends into nested module bodies (so `module Foo; class Bar` registers `Foo::Bar`) but never registers the module itself in `accumulator`.



1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/rigor/inference/scope_indexer.rb', line 1002

def collect_class_decls(node, qualified_prefix, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode
    name = qualified_name_for(node.constant_path)
    if name
      full = (qualified_prefix + [name]).join("::")
      accumulator[full] = Type::Combinator.singleton_of(full)
      return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
    end
  when Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
  end

  node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
end

.collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object



274
275
276
277
278
279
280
# File 'lib/rigor/inference/scope_indexer.rb', line 274

def collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator)
  return if def_node.body.nil? || qualified_prefix.empty?

  class_name = qualified_prefix.join("::")
  body_scope = default_scope.with_self_type(Type::Combinator.nominal_of(class_name))
  gather_cvar_writes(def_node.body, body_scope, class_name, accumulator)
end

.collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/rigor/inference/scope_indexer.rb', line 200

def collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator)
  return if def_node.body.nil? || qualified_prefix.empty?

  class_name = qualified_prefix.join("::")
  singleton = def_node.receiver.is_a?(Prism::SelfNode) ||
              def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
  self_type =
    if singleton
      Type::Combinator.singleton_of(class_name)
    else
      Type::Combinator.nominal_of(class_name)
    end
  body_scope = default_scope.with_self_type(self_type)

  gather_ivar_writes(def_node.body, body_scope, class_name, accumulator)
end

.data_define_call?(node) ⇒ Boolean

Recognises ‘Data.define(*Symbol)` and `Data.define(*Symbol) do … end` at constant-write rvalue position. The receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call return) are rejected because their identity is not statically known.

Returns:

  • (Boolean)


1094
1095
1096
1097
1098
1099
1100
1101
# File 'lib/rigor/inference/scope_indexer.rb', line 1094

def data_define_call?(node)
  return false unless node.is_a?(Prism::CallNode)
  return false unless node.name == :define
  return false unless meta_constant_receiver?(node.receiver, :Data)

  args = node.arguments&.arguments || []
  args.all?(Prism::SymbolNode)
end

.def_receiver_targets_lexical_self?(receiver, qualified_prefix) ⇒ Boolean

Only ‘Prism::ConstantReadNode` is observed in real Ruby —Prism mis-parses `def C::P.method` as `def C.P` (Ruby itself rejects the form as a SyntaxError). The ConstantPathNode branch stays defensive in case Prism’s grammar widens.

Returns:

  • (Boolean)


522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/rigor/inference/scope_indexer.rb', line 522

def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
  return false if qualified_prefix.empty?

  case receiver
  when Prism::ConstantReadNode
    receiver.name.to_s == qualified_prefix.last
  when Prism::ConstantPathNode
    rendered = render_constant_path(receiver)
    return false unless rendered

    path = rendered.split("::")
    qualified_prefix.last(path.length) == path
  else
    false
  end
end

.def_singleton?(def_node, qualified_prefix, in_singleton_class) ⇒ Boolean

‘def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def self.bar`: at the def-site, the runtime value of the constant `Foo` is the module itself (== `self`). Recognise the form so the method registers as singleton on the enclosing class.

The cross-class form ‘def Bar.baz` inside `module Foo` —where the receiver names a constant other than the enclosing class — is not supported at this slice; falls through to `:instance` (current behaviour) rather than silently re-routing the registration.

Returns:

  • (Boolean)


512
513
514
515
516
# File 'lib/rigor/inference/scope_indexer.rb', line 512

def def_singleton?(def_node, qualified_prefix, in_singleton_class)
  return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class

  def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
end

.discovered_classes_for_paths(paths, buffer: nil) ⇒ Hash{String => Rigor::Type::Singleton}

Walks every file in ‘paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned project-wide `discovered_classes` Hash: `=> Singleton`. Used by `Analysis::Runner` to seed each file’s ‘default_scope.discovered_classes` so that lexical constant lookup in one file resolves a `class Foo` declared in a sibling file. Per-file collisions are last-write-wins (matches the existing in-file merge semantics). Parse failures fail-soft to an empty contribution. The `buffer` argument, when present, redirects reads for the bound logical path to the buffer’s physical path so editor-mode pre-passes see the in-flight bytes.

**Modules are intentionally excluded** from the project-wide seed: a ‘module M; module_function; def x; end; end` body, when surfaced as `singleton(M)` to the dispatcher, falls through to `Kernel#x` (or any Module ancestor method) when the project’s per-file ‘discovered_methods` doesn’t know ‘M.x` — leading to surprising types like `Kernel.select → Array`. Until cross-file `discovered_methods` follows the same project-wide seed, registering modules here would introduce regressions in modules-with-module_function idioms that previously resolved to `Dynamic`. Class declarations are safe because per-file `discovered_methods` already tracks `def self.x` / `def x` instance and singleton methods consistently.

Parameters:

Returns:



945
946
947
948
949
950
951
952
953
954
955
956
957
958
# File 'lib/rigor/inference/scope_indexer.rb', line 945

def discovered_classes_for_paths(paths, buffer: nil)
  accumulator = {}
  paths.each do |path|
    physical = buffer ? buffer.resolve(path) : path
    source = File.read(physical)
    root = Prism.parse(source, filepath: path).value
    collect_class_decls(root, [], accumulator)
  rescue StandardError
    # Skip files that fail to parse or read; the per-file
    # analyzer surfaces the parse error separately.
    next
  end
  accumulator.freeze
end

.discovered_def_index_for_paths(paths, buffer: nil) ⇒ Hash{Symbol => Hash}

ADR-24 slice 2 — cross-file companion to ‘discovered_classes_for_paths`. Walks every project file once and returns both the merged `discovered_def_nodes` table (a class reopened across files has its method tables merged) and the merged class -> superclass-name map. The engine consults these so an implicit-self call inside a subclass resolves against a superclass `def` declared in a sibling file (`Mastodon::CLI::Accounts` calling a helper defined in `Mastodon::CLI::Base`).

Parameters:

Returns:

  • (Hash{Symbol => Hash})

    ‘{ def_nodes:, superclasses: }`



974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'lib/rigor/inference/scope_indexer.rb', line 974

def discovered_def_index_for_paths(paths, buffer: nil)
  def_nodes = {}
  superclasses = {}
  includes = {}
  paths.each do |path|
    physical = buffer ? buffer.resolve(path) : path
    root = Prism.parse(File.read(physical), filepath: path).value
    build_discovered_def_nodes(root).each do |class_name, methods|
      (def_nodes[class_name] ||= {}).merge!(methods)
    end
    superclasses.merge!(build_discovered_superclasses(root))
    build_discovered_includes(root).each do |class_name, mods|
      includes[class_name] = ((includes[class_name] || []) + mods).uniq
    end
  rescue StandardError
    # Skip files that fail to parse or read; the per-file
    # analyzer surfaces the parse error separately.
    next
  end
  def_nodes.each_value(&:freeze)
  includes.each_value(&:freeze)
  { def_nodes: def_nodes.freeze, superclasses: superclasses.freeze, includes: includes.freeze }
end

.gather_cvar_writes(node, scope, class_name, accumulator) ⇒ Object



282
283
284
285
286
287
288
289
# File 'lib/rigor/inference/scope_indexer.rb', line 282

def gather_cvar_writes(node, scope, class_name, accumulator)
  return unless node.is_a?(Prism::Node)

  record_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::ClassVariableWriteNode)
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }

  node.compact_child_nodes.each { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
end

.gather_global_writes(node, scope, accumulator) ⇒ Object



311
312
313
314
315
316
# File 'lib/rigor/inference/scope_indexer.rb', line 311

def gather_global_writes(node, scope, accumulator)
  return unless node.is_a?(Prism::Node)

  record_global_write(node, scope, accumulator) if node.is_a?(Prism::GlobalVariableWriteNode)
  node.compact_child_nodes.each { |c| gather_global_writes(c, scope, accumulator) }
end

.gather_ivar_writes(node, scope, class_name, accumulator) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
# File 'lib/rigor/inference/scope_indexer.rb', line 220

def gather_ivar_writes(node, scope, class_name, accumulator)
  return unless node.is_a?(Prism::Node)

  record_ivar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::InstanceVariableWriteNode)

  # Don't recurse into nested defs, classes, or modules; their
  # ivars belong to their own enclosing class.
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }

  node.compact_child_nodes.each { |c| gather_ivar_writes(c, scope, class_name, accumulator) }
end

.index(root, default_scope:) ⇒ Hash{Prism::Node => Rigor::Scope}

Build the scope index for a Prism program subtree.

Parameters:

  • root (Prism::Node)

    usually a ‘Prism::ProgramNode`, but any subtree the caller wants the indexer to walk works.

  • default_scope (Rigor::Scope)

    the scope used for the root, and the fallback returned for any Prism node not contained in ‘root`’s subtree.

Returns:

  • (Hash{Prism::Node => Rigor::Scope})

    identity-comparing table whose default value is ‘default_scope`.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/rigor/inference/scope_indexer.rb', line 53

def index(root, default_scope:) # rubocop:disable Metrics/AbcSize
  # Slice A-declarations. Build the declaration overrides
  # first so every scope handed to the StatementEvaluator
  # already carries the table; structural sharing through
  # `Scope#with_local` / `#with_fact` / `#with_self_type`
  # propagates it across every derived scope.
  declared_types, discovered_classes = build_declaration_artifacts(root)
  # Merge the indexer's findings on top of whatever the
  # base scope already carries so callers that seed
  # cross-file class knowledge (e.g. the ADR-14
  # `SigGen::ObservationCollector` pre-walking project
  # `lib/` before scanning `spec/`) keep their seeds
  # alongside the per-file declarations the indexer
  # itself discovers. Indexer-found entries win on
  # collision — same-file declarations are the most
  # specific authority.
  merged_classes = default_scope.discovered_classes.merge(discovered_classes)
  seeded_scope = default_scope
                 .with_declared_types(declared_types)
                 .with_discovered_classes(merged_classes)

  # Slice 7 phase 2. Pre-pass over every class/module body
  # to collect the per-class ivar accumulator. Seeded after
  # declared_types so the rvalue typer in the pre-pass can
  # see declaration overrides.
  class_ivars = build_class_ivar_index(root, seeded_scope)
  seeded_scope = seeded_scope.with_class_ivars(class_ivars)

  # Slice 7 phase 6. Same pre-pass shape for cvars (per
  # class) and globals (program-wide). Globals are also
  # materialised into the top-level scope's `globals` map
  # so reads at the top level (and in CLI probes that do
  # not enter a method body) observe the precise type
  # without consulting the accumulator on every lookup.
  class_cvars = build_class_cvar_index(root, seeded_scope)
  seeded_scope = seeded_scope.with_class_cvars(class_cvars)
  program_globals = build_program_global_index(root, seeded_scope)
  seeded_scope = seeded_scope.with_program_globals(program_globals)
  program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }

  # Slice 7 phase 9. In-source constant value tracking.
  # Walks every ConstantWriteNode/ConstantPathWriteNode in
  # the program and types its rvalue under a scope that
  # carries the surrounding qualified prefix as
  # `self_type`, so the rvalue typer sees in-class
  # references resolve correctly. Multiple writes to the
  # same qualified name union via `Type::Combinator.union`.
  in_source_constants = build_in_source_constants(root, seeded_scope)
  seeded_scope = seeded_scope.with_in_source_constants(in_source_constants)

  # Slice 7 phase 12. In-source method discovery. Walks
  # every class/module body for `Prism::DefNode` and
  # recognised `define_method` calls and records the
  # introduced method names. `rigor check` consults the
  # table to suppress false positives for methods the
  # user has defined but no RBS sig describes.
  discovered_methods = build_discovered_methods(root)
  seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)

  # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
  # def nodes, the class -> superclass map, and the
  # class/module -> included-modules map, each merged under
  # the cross-file pre-pass seed (see below).
  seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)

  # v0.1.2 — per-class table of method visibilities
  # (`:public` / `:private` / `:protected`). The
  # `def.method-visibility-mismatch` CheckRule consults
  # the table to flag explicit-non-self calls to a
  # private user method.
  discovered_method_visibilities = build_discovered_method_visibilities(root)
  seeded_scope = seeded_scope.with_discovered_method_visibilities(discovered_method_visibilities)

  table = {}.compare_by_identity
  table.default = seeded_scope

  on_enter = ->(node, scope) { table[node] = scope unless table.key?(node) }
  StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter).evaluate(root)

  propagate(root, table, seeded_scope)
  table
end

.literal_method_name(node) ⇒ Object



907
908
909
910
911
# File 'lib/rigor/inference/scope_indexer.rb', line 907

def literal_method_name(node)
  return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)

  node.unescaped&.to_sym
end

.merge_project_method_indexes(seeded_scope, default_scope, root) ⇒ Object

v0.0.2 #5 + ADR-24 slice 2 — seeds the three project-method indexes onto ‘seeded_scope`: the per-instance-method def-node table, the class -> superclass map, and the class/module -> included-modules map. Each per-file table is merged UNDER the cross-file `discovered_def_index_for_paths` seed carried on `default_scope` — same-file declarations win per entry, the cross-file seed supplies sibling-file ancestors.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/rigor/inference/scope_indexer.rb', line 144

def merge_project_method_indexes(seeded_scope, default_scope, root)
  def_nodes = default_scope.discovered_def_nodes.merge(
    build_discovered_def_nodes(root)
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
  superclasses = default_scope.discovered_superclasses.merge(
    build_discovered_superclasses(root)
  )
  includes = default_scope.discovered_includes.merge(
    build_discovered_includes(root)
  ) { |_class, cross_file, per_file| (cross_file + per_file).uniq }

  seeded_scope
    .with_discovered_def_nodes(def_nodes)
    .with_discovered_superclasses(superclasses)
    .with_discovered_includes(includes)
end

.meta_call_with_name?(node, receiver_name, method_name) ⇒ Boolean

Returns:

  • (Boolean)


1145
1146
1147
1148
1149
1150
# File 'lib/rigor/inference/scope_indexer.rb', line 1145

def meta_call_with_name?(node, receiver_name, method_name)
  return false unless node.is_a?(Prism::CallNode)
  return false unless node.name == method_name

  meta_constant_receiver?(node.receiver, receiver_name)
end

.meta_constant_receiver?(node, expected_name) ⇒ Boolean

Returns:

  • (Boolean)


1156
1157
1158
1159
1160
1161
1162
1163
# File 'lib/rigor/inference/scope_indexer.rb', line 1156

def meta_constant_receiver?(node, expected_name)
  case node
  when Prism::ConstantReadNode
    node.name == expected_name
  when Prism::ConstantPathNode
    node.parent.nil? && node.name == expected_name
  end
end

.meta_new_block_body(node) ⇒ Object

v0.1.2 — when a ‘Const = Data.define(*sym) do … end` / `Const = Struct.new(*sym) do … end` constant write carries a block, the block body holds method overrides whose canonical class is `Const`. Survey item (e) extended the recognition to `Const = Module.new do … end` and `Const = Class.new(?super) do … end` — the ADR-16 Tier A “block-as-method” idiom at constant-write position. Returns the block body node (a `Prism::StatementsNode`) when the rvalue matches; nil otherwise. Used by `walk_methods` / `walk_def_nodes` to push `Const` onto the qualified prefix before recursing.



479
480
481
482
483
484
485
486
487
488
489
# File 'lib/rigor/inference/scope_indexer.rb', line 479

def meta_new_block_body(node)
  return nil unless node.is_a?(Prism::ConstantWriteNode)

  rvalue = node.value
  return nil unless data_define_call?(rvalue) ||
                    struct_new_call?(rvalue) ||
                    module_new_call?(rvalue) ||
                    class_new_call?(rvalue)

  rvalue.block&.body
end

.meta_new_constant_type(node, full) ⇒ Object

Survey item (e): when the rvalue is a recognised ‘Module.new do … end` / `Class.new do … end` / `Struct.new(*sym) do … end` / `Data.define(*sym) do … end` form, type the named constant as `Singleton` so the discovered-method table registered under `full` becomes reachable through singleton-side dispatch (`Const.[]=` etc.). Returns nil for non-meta-new rvalues so the caller falls back to the default `body_scope.type_of(node.value)` shape.



384
385
386
387
388
# File 'lib/rigor/inference/scope_indexer.rb', line 384

def meta_new_constant_type(node, full)
  return nil unless meta_new_block_body(node)

  Type::Combinator.singleton_of(full)
end

.module_new_call?(node) ⇒ Boolean

Recognises ‘Module.new` and `Module.new(&block)` / `Module.new do … end` at constant-write rvalue position. The block body is the anonymous module’s ‘module_eval` body; defs inside it bind methods on the named constant (`Const = Module.new do …; def foo; …; end; end`). Arguments are NOT inspected because `Module.new` accepts no positionals — Ruby raises ArgumentError if any are passed — so a malformed call falls through the walker without affecting analysis.

Returns:

  • (Boolean)


1129
1130
1131
# File 'lib/rigor/inference/scope_indexer.rb', line 1129

def module_new_call?(node)
  meta_call_with_name?(node, :Module, :new)
end

.propagate(node, table, parent_scope) ⇒ Object

Walks ‘node`’s subtree DFS and fills in scope entries for every Prism node the StatementEvaluator did not visit (i.e. expression- interior nodes like the receiver/args of a CallNode). Those nodes inherit their nearest recorded ancestor’s scope.

‘IfNode` / `UnlessNode` are special-cased: the truthy and falsey branches each get their predicate’s narrowed scope before recursing. This handles expression-position conditionals (e.g. ‘cache = if cond; t; else; e; end` and conditionals nested as call arguments) which are typed by ExpressionTyper without going through `eval_if`’s narrowing path.



1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
# File 'lib/rigor/inference/scope_indexer.rb', line 1195

def propagate(node, table, parent_scope)
  return unless node.is_a?(Prism::Node)

  current_scope =
    if table.key?(node)
      table[node]
    else
      table[node] = parent_scope
      parent_scope
    end

  case node
  when Prism::IfNode
    propagate_if_branches(node, table, current_scope)
  when Prism::UnlessNode
    propagate_unless_branches(node, table, current_scope)
  else
    node.compact_child_nodes.each { |child| propagate(child, table, current_scope) }
  end
end

.propagate_if_branches(node, table, current_scope) ⇒ Object



1216
1217
1218
1219
1220
1221
# File 'lib/rigor/inference/scope_indexer.rb', line 1216

def propagate_if_branches(node, table, current_scope)
  truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, current_scope)
  propagate(node.predicate, table, current_scope) if node.predicate
  propagate(node.statements, table, truthy_scope) if node.statements
  propagate(node.subsequent, table, falsey_scope) if node.subsequent
end

.propagate_unless_branches(node, table, current_scope) ⇒ Object



1223
1224
1225
1226
1227
1228
# File 'lib/rigor/inference/scope_indexer.rb', line 1223

def propagate_unless_branches(node, table, current_scope)
  truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, current_scope)
  propagate(node.predicate, table, current_scope) if node.predicate
  propagate(node.statements, table, falsey_scope) if node.statements
  propagate(node.else_clause, table, truthy_scope) if node.else_clause
end

.qualified_name_for(constant_path_node) ⇒ Object



1165
1166
1167
1168
1169
1170
1171
1172
# File 'lib/rigor/inference/scope_indexer.rb', line 1165

def qualified_name_for(constant_path_node)
  case constant_path_node
  when Prism::ConstantReadNode
    constant_path_node.name.to_s
  when Prism::ConstantPathNode
    render_constant_path(constant_path_node)
  end
end

.record_alias_map_entry(alias_node, qualified_prefix, accumulator) ⇒ Object



884
885
886
887
888
889
890
891
892
# File 'lib/rigor/inference/scope_indexer.rb', line 884

def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
  return if qualified_prefix.empty?
  return unless alias_node.new_name.is_a?(Prism::SymbolNode) && alias_node.old_name.is_a?(Prism::SymbolNode)

  class_name = qualified_prefix.join("::")
  new_name = alias_node.new_name.unescaped.to_sym
  old_name = alias_node.old_name.unescaped.to_sym
  (accumulator[class_name] ||= {})[new_name] = old_name
end

.record_alias_method(alias_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object

Registers the alias name in the ‘discovered_methods` table so `undefined-method` diagnostics are not emitted for calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body, singleton inside `class << self`).



831
832
833
834
835
836
837
838
839
# File 'lib/rigor/inference/scope_indexer.rb', line 831

def record_alias_method(alias_node, qualified_prefix, in_singleton_class, accumulator)
  return if qualified_prefix.empty?
  return unless alias_node.new_name.is_a?(Prism::SymbolNode)

  class_name = qualified_prefix.join("::")
  new_name = alias_node.new_name.unescaped.to_sym
  kind = in_singleton_class ? :singleton : :instance
  (accumulator[class_name] ||= {})[new_name] = kind
end

.record_class_or_module?(node, qualified_prefix, identity_table, discovered) ⇒ Boolean

Returns:

  • (Boolean)


1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
# File 'lib/rigor/inference/scope_indexer.rb', line 1052

def record_class_or_module?(node, qualified_prefix, identity_table, discovered)
  name = qualified_name_for(node.constant_path)
  return false unless name

  full = (qualified_prefix + [name]).join("::")
  singleton = Type::Combinator.singleton_of(full)
  identity_table[node.constant_path] = singleton
  discovered[full] = singleton
  child_prefix = qualified_prefix + [name]
  record_declarations(node.body, child_prefix, identity_table, discovered) if node.body
  true
end

.record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name) ⇒ Object



364
365
366
367
368
369
370
371
372
373
# File 'lib/rigor/inference/scope_indexer.rb', line 364

def record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name)
  full = qualified_prefix.empty? ? base_name : "#{qualified_prefix.join('::')}::#{base_name}"
  body_scope = default_scope
  unless qualified_prefix.empty?
    body_scope = body_scope.with_self_type(Type::Combinator.singleton_of(qualified_prefix.join("::")))
  end
  rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
  existing = accumulator[full]
  accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
end

.record_cvar_write(node, scope, class_name, accumulator) ⇒ Object



291
292
293
294
295
296
297
# File 'lib/rigor/inference/scope_indexer.rb', line 291

def record_cvar_write(node, scope, class_name, accumulator)
  rvalue_type = scope.type_of(node.value)
  accumulator[class_name] ||= {}
  existing = accumulator[class_name][node.name]
  accumulator[class_name][node.name] =
    existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
end

.record_declarations(node, qualified_prefix, identity_table, discovered) ⇒ Object



1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
# File 'lib/rigor/inference/scope_indexer.rb', line 1037

def record_declarations(node, qualified_prefix, identity_table, discovered)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ModuleNode, Prism::ClassNode
    return if record_class_or_module?(node, qualified_prefix, identity_table, discovered)
  when Prism::ConstantWriteNode
    return if record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
  end

  node.compact_child_nodes.each do |child|
    record_declarations(child, qualified_prefix, identity_table, discovered)
  end
end

.record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object



491
492
493
494
495
496
497
498
499
# File 'lib/rigor/inference/scope_indexer.rb', line 491

def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
  return if qualified_prefix.empty?

  class_name = qualified_prefix.join("::")
  singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
  kind = singleton ? :singleton : :instance
  accumulator[class_name] ||= {}
  accumulator[class_name][def_node.name] = kind
end

.record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object



599
600
601
602
603
604
605
# File 'lib/rigor/inference/scope_indexer.rb', line 599

def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
  return if def_singleton?(def_node, qualified_prefix, in_singleton_class)

  class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
  accumulator[class_name] ||= {}
  accumulator[class_name][def_node.name] = def_node
end

.record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator) ⇒ Object

rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize



776
777
778
779
780
781
782
783
# File 'lib/rigor/inference/scope_indexer.rb', line 776

def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
  return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
  return if qualified_prefix.empty?

  class_name = qualified_prefix.join("::")
  accumulator[class_name] ||= {}
  accumulator[class_name][def_node.name] = current_visibility
end

.record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object



894
895
896
897
898
899
900
901
902
903
904
905
# File 'lib/rigor/inference/scope_indexer.rb', line 894

def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
  return if qualified_prefix.empty?
  return if call_node.arguments.nil? || call_node.arguments.arguments.empty?

  first_arg = call_node.arguments.arguments.first
  method_name = literal_method_name(first_arg)
  return if method_name.nil?

  class_name = qualified_prefix.join("::")
  accumulator[class_name] ||= {}
  accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
end

.record_global_write(node, scope, accumulator) ⇒ Object



318
319
320
321
322
323
# File 'lib/rigor/inference/scope_indexer.rb', line 318

def record_global_write(node, scope, accumulator)
  rvalue_type = scope.type_of(node.value)
  existing = accumulator[node.name]
  accumulator[node.name] =
    existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
end

.record_ivar_write(node, scope, class_name, accumulator) ⇒ Object



232
233
234
235
236
237
238
# File 'lib/rigor/inference/scope_indexer.rb', line 232

def record_ivar_write(node, scope, class_name, accumulator)
  rvalue_type = scope.type_of(node.value)
  accumulator[class_name] ||= {}
  existing = accumulator[class_name][node.name]
  accumulator[class_name][node.name] =
    existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
end

.record_meta_new_constant?(node, qualified_prefix, identity_table, discovered) ⇒ Boolean

Recognises class-creating meta calls at constant-write rvalue position and registers ‘Const` (qualified by the surrounding class/module path) as a discovered class. `Const.new(…)` then resolves to a fresh `Nominal` via `meta_new`, instead of the un-narrowed `Dynamic` returned by the default `Class#new` envelope.

Two recognised meta forms:

  • ‘Const = Data.define(*Symbol) [do … end]`

  • ‘Const = Struct.new(*Symbol [, keyword_init: …]) [do … end]`

The block body, if present, is recursed into so any nested class/module declarations in the override block (rare but legal) still feed the discovered table.

Returns:

  • (Boolean)


1080
1081
1082
1083
1084
1085
1086
1087
# File 'lib/rigor/inference/scope_indexer.rb', line 1080

def record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
  return false unless data_define_call?(node.value) || struct_new_call?(node.value)

  full = (qualified_prefix + [node.name.to_s]).join("::")
  discovered[full] = Type::Combinator.singleton_of(full)
  record_declarations(node.value, qualified_prefix, identity_table, discovered)
  true
end

.record_mixin_call(node, current_class, accumulator) ⇒ Object



685
686
687
688
689
690
691
692
693
# File 'lib/rigor/inference/scope_indexer.rb', line 685

def record_mixin_call(node, current_class, accumulator)
  return unless current_class && node.receiver.nil?
  return unless MIXIN_CALL_NAMES.include?(node.name)

  node.arguments&.arguments&.each do |arg|
    mod = qualified_name_for(arg)
    (accumulator[current_class] ||= []) << mod if mod
  end
end

.render_constant_path(node) ⇒ Object



1174
1175
1176
1177
1178
1179
1180
1181
1182
# File 'lib/rigor/inference/scope_indexer.rb', line 1174

def render_constant_path(node)
  prefix =
    case node.parent
    when Prism::ConstantReadNode then "#{node.parent.name}::"
    when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
    else ""
    end
  "#{prefix}#{node.name}"
end

.singleton_class_prefix(node, qualified_prefix) ⇒ Object

Resolves a ‘class << X` body’s qualified prefix.

- `class << self` keeps `qualified_prefix` (the enclosing class).
- `class << Foo` inside `class Foo` collapses to the same prefix
  (semantically `class << self`).
- `class << Foo` not nested in `class Foo` returns `[Foo]`
  so methods defined inside register on Foo's singleton.
- Any other expression (variable, method call) returns nil
  so the walker falls through and skips the body.


451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/rigor/inference/scope_indexer.rb', line 451

def singleton_class_prefix(node, qualified_prefix)
  case node.expression
  when Prism::SelfNode
    qualified_prefix
  when Prism::ConstantReadNode, Prism::ConstantPathNode
    rendered = qualified_name_for(node.expression)
    return nil unless rendered

    if !qualified_prefix.empty? && qualified_prefix.last == rendered
      qualified_prefix
    else
      rendered.split("::")
    end
  end
end

.struct_new_call?(node) ⇒ Boolean

Recognises ‘Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue position. A trailing `KeywordHashNode` (the `keyword_init: …` form) is accepted but does not contribute to member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —`Struct.new()` is a degenerate form callers don’t typically use.

Returns:

  • (Boolean)


1110
1111
1112
1113
1114
1115
1116
1117
1118
# File 'lib/rigor/inference/scope_indexer.rb', line 1110

def struct_new_call?(node)
  return false unless meta_call_with_name?(node, :Struct, :new)

  args = node.arguments&.arguments || []
  positional = struct_new_positionals(args)
  return false if positional.nil? || positional.empty?

  positional.all?(Prism::SymbolNode)
end

.struct_new_positionals(args) ⇒ Object



1152
1153
1154
# File 'lib/rigor/inference/scope_indexer.rb', line 1152

def struct_new_positionals(args)
  args.last.is_a?(Prism::KeywordHashNode) ? args[0..-2] : args
end

.visibility_target_name(arg) ⇒ Object



820
821
822
823
824
# File 'lib/rigor/inference/scope_indexer.rb', line 820

def visibility_target_name(arg)
  return arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)

  nil
end

.walk_class_cvars(node, qualified_prefix, default_scope, accumulator) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/rigor/inference/scope_indexer.rb', line 253

def walk_class_cvars(node, qualified_prefix, default_scope, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_class_cvars(node.body, child_prefix, default_scope, accumulator) if node.body
      return
    end
  when Prism::DefNode
    collect_def_cvar_writes(node, qualified_prefix, default_scope, accumulator)
    return
  end

  node.compact_child_nodes.each do |child|
    walk_class_cvars(child, qualified_prefix, default_scope, accumulator)
  end
end

.walk_class_includes(node, qualified_prefix, current_class, accumulator) ⇒ Object



665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
# File 'lib/rigor/inference/scope_indexer.rb', line 665

def walk_class_includes(node, qualified_prefix, current_class, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      full = (qualified_prefix + [name]).join("::")
      walk_class_includes(node.body, qualified_prefix + [name], full, accumulator) if node.body
      return
    end
  when Prism::CallNode
    record_mixin_call(node, current_class, accumulator)
  end

  node.compact_child_nodes.each do |child|
    walk_class_includes(child, qualified_prefix, current_class, accumulator)
  end
end

.walk_class_ivars(node, qualified_prefix, default_scope, accumulator) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/rigor/inference/scope_indexer.rb', line 179

def walk_class_ivars(node, qualified_prefix, default_scope, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_class_ivars(node.body, child_prefix, default_scope, accumulator) if node.body
      return
    end
  when Prism::DefNode
    collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator)
    return
  end

  node.compact_child_nodes.each do |child|
    walk_class_ivars(child, qualified_prefix, default_scope, accumulator)
  end
end

.walk_class_superclasses(node, qualified_prefix, accumulator) ⇒ Object



621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/rigor/inference/scope_indexer.rb', line 621

def walk_class_superclasses(node, qualified_prefix, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode
    name = qualified_name_for(node.constant_path)
    if name
      full = (qualified_prefix + [name]).join("::")
      superclass = node.superclass && qualified_name_for(node.superclass)
      accumulator[full] = superclass if superclass
      walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
      return
    end
  when Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
      return
    end
  end

  node.compact_child_nodes.each do |child|
    walk_class_superclasses(child, qualified_prefix, accumulator)
  end
end

.walk_constant_writes(node, qualified_prefix, default_scope, accumulator) ⇒ Object



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/rigor/inference/scope_indexer.rb', line 339

def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_constant_writes(node.body, child_prefix, default_scope, accumulator) if node.body
      return
    end
  when Prism::ConstantWriteNode
    record_constant_write(node, qualified_prefix, default_scope, accumulator, node.name.to_s)
    return
  when Prism::ConstantPathWriteNode
    full = qualified_name_for(node.target)
    record_constant_write(node, [], default_scope, accumulator, full) if full
    return
  end

  node.compact_child_nodes.each do |child|
    walk_constant_writes(child, qualified_prefix, default_scope, accumulator)
  end
end

.walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/rigor/inference/scope_indexer.rb', line 556

def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
      return
    end
  when Prism::SingletonClassNode
    if node.body
      singleton_prefix = singleton_class_prefix(node, qualified_prefix)
      if singleton_prefix
        walk_def_nodes(node.body, singleton_prefix, true, accumulator)
        return
      end
    end
  when Prism::ConstantWriteNode
    if meta_new_block_body(node)
      child_prefix = qualified_prefix + [node.name.to_s]
      walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
      return
    end
  when Prism::DefNode
    record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
    return
  end

  node.compact_child_nodes.each do |child|
    walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
  end
end

.walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize



725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
# File 'lib/rigor/inference/scope_indexer.rb', line 725

def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
  return current_visibility unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_method_visibilities(node.body, child_prefix, false, :public, accumulator) if node.body
      return current_visibility
    end
  when Prism::SingletonClassNode
    if node.body
      singleton_prefix = singleton_class_prefix(node, qualified_prefix)
      if singleton_prefix
        walk_method_visibilities(node.body, singleton_prefix, true, :public, accumulator)
        return current_visibility
      end
    end
  when Prism::ConstantWriteNode
    if meta_new_block_body(node)
      child_prefix = qualified_prefix + [node.name.to_s]
      walk_method_visibilities(meta_new_block_body(node), child_prefix, false, :public, accumulator)
      return current_visibility
    end
  when Prism::DefNode
    record_def_visibility(node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
    return current_visibility
  when Prism::CallNode
    updated = apply_visibility_call(node, qualified_prefix, current_visibility, accumulator)
    return updated unless updated.equal?(current_visibility)
  end

  # Statement-position StatementsNode preserves
  # left-to-right visibility flow; everything else
  # recurses with the entry visibility unchanged.
  if node.is_a?(Prism::StatementsNode)
    local_visibility = current_visibility
    node.compact_child_nodes.each do |child|
      local_visibility = walk_method_visibilities(child, qualified_prefix, in_singleton_class,
                                                  local_visibility, accumulator)
    end
  else
    node.compact_child_nodes.each do |child|
      walk_method_visibilities(child, qualified_prefix, in_singleton_class, current_visibility, accumulator)
    end
  end
  current_visibility
end

.walk_methods(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/rigor/inference/scope_indexer.rb', line 403

def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
  return unless node.is_a?(Prism::Node)

  case node
  when Prism::ClassNode, Prism::ModuleNode
    name = qualified_name_for(node.constant_path)
    if name
      child_prefix = qualified_prefix + [name]
      walk_methods(node.body, child_prefix, false, accumulator) if node.body
      return
    end
  when Prism::SingletonClassNode
    if node.body
      singleton_prefix = singleton_class_prefix(node, qualified_prefix)
      if singleton_prefix
        walk_methods(node.body, singleton_prefix, true, accumulator)
        return
      end
    end
  when Prism::ConstantWriteNode
    if meta_new_block_body(node)
      child_prefix = qualified_prefix + [node.name.to_s]
      walk_methods(meta_new_block_body(node), child_prefix, false, accumulator)
      return
    end
  when Prism::DefNode
    record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
    return
  when Prism::AliasMethodNode
    record_alias_method(node, qualified_prefix, in_singleton_class, accumulator)
    return
  when Prism::CallNode
    record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
  end

  node.compact_child_nodes.each do |child|
    walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
  end
end