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>"- VISIBILITY_MODIFIERS =
%i[public private protected].freeze
Class Method Summary collapse
-
.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.
- .apply_named_visibility(args, qualified_prefix, visibility, accumulator) ⇒ Object
-
.apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator) ⇒ Object
Recognises modifier calls on the implicit-self receiver inside a class body.
-
.build_class_cvar_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — class-cvar pre-pass.
-
.build_class_ivar_index(root, default_scope) ⇒ Object
Slice 7 phase 2.
-
.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.
-
.build_discovered_def_nodes(root) ⇒ Object
v0.0.2 #5 — instance-side def-node recording.
-
.build_discovered_method_visibilities(root) ⇒ Object
v0.1.2 — per-class method-visibility table for the ‘def.method-visibility-mismatch` CheckRule.
-
.build_discovered_methods(root) ⇒ Object
Slice 7 phase 12 — in-source method discovery pre-pass.
-
.build_in_source_constants(root, default_scope) ⇒ Object
Slice 7 phase 9 — in-source constant value pre-pass.
-
.build_program_global_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — program-global pre-pass.
-
.class_new_call?(node) ⇒ Boolean
Recognises ‘Class.new`, `Class.new(super_class)`, and the block form `Class.new { … }`.
-
.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.
-
.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`.
- .collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
-
.data_define_call?(node) ⇒ Boolean
Recognises ‘Data.define(*Symbol)` and `Data.define(*Symbol) do …
-
.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).
-
.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`).
-
.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`.
- .gather_cvar_writes(node, scope, class_name, accumulator) ⇒ Object
- .gather_global_writes(node, scope, accumulator) ⇒ Object
- .gather_ivar_writes(node, scope, class_name, accumulator) ⇒ Object
-
.index(root, default_scope:) ⇒ Hash{Prism::Node => Rigor::Scope}
Build the scope index for a Prism program subtree.
- .literal_method_name(node) ⇒ Object
- .meta_call_with_name?(node, receiver_name, method_name) ⇒ Boolean
- .meta_constant_receiver?(node, expected_name) ⇒ Boolean
-
.meta_new_block_body(node) ⇒ Object
v0.1.2 — when a ‘Const = Data.define(*sym) do …
-
.meta_new_constant_type(node, full) ⇒ Object
Survey item (e): when the rvalue is a recognised ‘Module.new do …
-
.module_new_call?(node) ⇒ Boolean
Recognises ‘Module.new` and `Module.new(&block)` / `Module.new do …
-
.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).
- .propagate_if_branches(node, table, current_scope) ⇒ Object
- .propagate_unless_branches(node, table, current_scope) ⇒ Object
- .qualified_name_for(constant_path_node) ⇒ Object
- .record_alias_map_entry(alias_node, qualified_prefix, accumulator) ⇒ Object
-
.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.
- .record_class_or_module?(node, qualified_prefix, identity_table, discovered) ⇒ Boolean
- .record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name) ⇒ Object
- .record_cvar_write(node, scope, class_name, accumulator) ⇒ Object
- .record_declarations(node, qualified_prefix, identity_table, discovered) ⇒ Object
- .record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
- .record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
-
.record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator) ⇒ Object
rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize.
- .record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
- .record_global_write(node, scope, accumulator) ⇒ Object
- .record_ivar_write(node, scope, class_name, accumulator) ⇒ Object
-
.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.
- .render_constant_path(node) ⇒ Object
-
.singleton_class_prefix(node, qualified_prefix) ⇒ Object
Resolves a ‘class << X` body’s qualified prefix.
-
.struct_new_call?(node) ⇒ Boolean
Recognises ‘Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue position.
- .struct_new_positionals(args) ⇒ Object
- .visibility_target_name(arg) ⇒ Object
- .walk_class_cvars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .walk_class_ivars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .walk_constant_writes(node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
-
.walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize.
-
.walk_methods(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength.
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.
734 735 736 737 738 739 740 741 742 743 744 745 746 747 |
# File 'lib/rigor/inference/scope_indexer.rb', line 734 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
697 698 699 700 701 702 703 704 705 706 |
# File 'lib/rigor/inference/scope_indexer.rb', line 697 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.
683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'lib/rigor/inference/scope_indexer.rb', line 683 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`.
223 224 225 226 227 |
# File 'lib/rigor/inference/scope_indexer.rb', line 223 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`.
149 150 151 152 153 |
# File 'lib/rigor/inference/scope_indexer.rb', line 149 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.
880 881 882 883 884 885 |
# File 'lib/rigor/inference/scope_indexer.rb', line 880 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.
525 526 527 528 529 530 |
# File 'lib/rigor/inference/scope_indexer.rb', line 525 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_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.
606 607 608 609 610 |
# File 'lib/rigor/inference/scope_indexer.rb', line 606 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]`.
372 373 374 375 376 |
# File 'lib/rigor/inference/scope_indexer.rb', line 372 def build_discovered_methods(root) accumulator = {} walk_methods(root, [], false, accumulator) accumulator.transform_values(&:freeze).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.
309 310 311 312 313 |
# File 'lib/rigor/inference/scope_indexer.rb', line 309 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.
281 282 283 284 285 |
# File 'lib/rigor/inference/scope_indexer.rb', line 281 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.
991 992 993 |
# File 'lib/rigor/inference/scope_indexer.rb', line 991 def class_new_call?(node) (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.
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 |
# File 'lib/rigor/inference/scope_indexer.rb', line 751 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`.
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 |
# File 'lib/rigor/inference/scope_indexer.rb', line 852 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
250 251 252 253 254 255 256 |
# File 'lib/rigor/inference/scope_indexer.rb', line 250 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
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/rigor/inference/scope_indexer.rb', line 176 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.
944 945 946 947 948 949 950 951 |
# File 'lib/rigor/inference/scope_indexer.rb', line 944 def data_define_call?(node) return false unless node.is_a?(Prism::CallNode) return false unless node.name == :define return false unless (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.
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 |
# File 'lib/rigor/inference/scope_indexer.rb', line 498 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.
488 489 490 491 492 |
# File 'lib/rigor/inference/scope_indexer.rb', line 488 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.
833 834 835 836 837 838 839 840 841 842 843 844 845 846 |
# File 'lib/rigor/inference/scope_indexer.rb', line 833 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 |
.gather_cvar_writes(node, scope, class_name, accumulator) ⇒ Object
258 259 260 261 262 263 264 265 |
# File 'lib/rigor/inference/scope_indexer.rb', line 258 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
287 288 289 290 291 292 |
# File 'lib/rigor/inference/scope_indexer.rb', line 287 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
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/rigor/inference/scope_indexer.rb', line 196 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.
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 135 |
# 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 — also record the def node itself for # instance methods so the engine can re-type the body # when a call site dispatches against a user-defined # method without an RBS sig. discovered_def_nodes = build_discovered_def_nodes(root) seeded_scope = seeded_scope.with_discovered_def_nodes(discovered_def_nodes) # 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
795 796 797 798 799 |
# File 'lib/rigor/inference/scope_indexer.rb', line 795 def literal_method_name(node) return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode) node.unescaped&.to_sym end |
.meta_call_with_name?(node, receiver_name, method_name) ⇒ Boolean
995 996 997 998 999 1000 |
# File 'lib/rigor/inference/scope_indexer.rb', line 995 def (node, receiver_name, method_name) return false unless node.is_a?(Prism::CallNode) return false unless node.name == method_name (node.receiver, receiver_name) end |
.meta_constant_receiver?(node, expected_name) ⇒ Boolean
1006 1007 1008 1009 1010 1011 1012 1013 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1006 def (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.
455 456 457 458 459 460 461 462 463 464 465 |
# File 'lib/rigor/inference/scope_indexer.rb', line 455 def (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.
360 361 362 363 364 |
# File 'lib/rigor/inference/scope_indexer.rb', line 360 def (node, full) return nil unless (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.
979 980 981 |
# File 'lib/rigor/inference/scope_indexer.rb', line 979 def module_new_call?(node) (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.
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1045 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
1066 1067 1068 1069 1070 1071 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1066 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
1073 1074 1075 1076 1077 1078 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1073 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
1015 1016 1017 1018 1019 1020 1021 1022 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1015 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
772 773 774 775 776 777 778 779 780 |
# File 'lib/rigor/inference/scope_indexer.rb', line 772 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`).
719 720 721 722 723 724 725 726 727 |
# File 'lib/rigor/inference/scope_indexer.rb', line 719 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
902 903 904 905 906 907 908 909 910 911 912 913 |
# File 'lib/rigor/inference/scope_indexer.rb', line 902 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
340 341 342 343 344 345 346 347 348 349 |
# File 'lib/rigor/inference/scope_indexer.rb', line 340 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 = (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
267 268 269 270 271 272 273 |
# File 'lib/rigor/inference/scope_indexer.rb', line 267 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
887 888 889 890 891 892 893 894 895 896 897 898 899 900 |
# File 'lib/rigor/inference/scope_indexer.rb', line 887 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 (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
467 468 469 470 471 472 473 474 475 |
# File 'lib/rigor/inference/scope_indexer.rb', line 467 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
575 576 577 578 579 580 581 |
# File 'lib/rigor/inference/scope_indexer.rb', line 575 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
664 665 666 667 668 669 670 671 |
# File 'lib/rigor/inference/scope_indexer.rb', line 664 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
782 783 784 785 786 787 788 789 790 791 792 793 |
# File 'lib/rigor/inference/scope_indexer.rb', line 782 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
294 295 296 297 298 299 |
# File 'lib/rigor/inference/scope_indexer.rb', line 294 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
208 209 210 211 212 213 214 |
# File 'lib/rigor/inference/scope_indexer.rb', line 208 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.
930 931 932 933 934 935 936 937 |
# File 'lib/rigor/inference/scope_indexer.rb', line 930 def (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 |
.render_constant_path(node) ⇒ Object
1024 1025 1026 1027 1028 1029 1030 1031 1032 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1024 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.
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/rigor/inference/scope_indexer.rb', line 427 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.
960 961 962 963 964 965 966 967 968 |
# File 'lib/rigor/inference/scope_indexer.rb', line 960 def struct_new_call?(node) return false unless (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
1002 1003 1004 |
# File 'lib/rigor/inference/scope_indexer.rb', line 1002 def struct_new_positionals(args) args.last.is_a?(Prism::KeywordHashNode) ? args[0..-2] : args end |
.visibility_target_name(arg) ⇒ Object
708 709 710 711 712 |
# File 'lib/rigor/inference/scope_indexer.rb', line 708 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
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/rigor/inference/scope_indexer.rb', line 229 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_ivars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/rigor/inference/scope_indexer.rb', line 155 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_constant_writes(node, qualified_prefix, default_scope, accumulator) ⇒ Object
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/rigor/inference/scope_indexer.rb', line 315 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
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 |
# File 'lib/rigor/inference/scope_indexer.rb', line 532 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 (node) child_prefix = qualified_prefix + [node.name.to_s] walk_def_nodes((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
613 614 615 616 617 618 619 620 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 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 |
# File 'lib/rigor/inference/scope_indexer.rb', line 613 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 (node) child_prefix = qualified_prefix + [node.name.to_s] walk_method_visibilities((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
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/rigor/inference/scope_indexer.rb', line 379 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 (node) child_prefix = qualified_prefix + [node.name.to_s] walk_methods((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 |