Class: Rigor::Scope

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/scope.rb

Overview

Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State changes return new scopes through explicit transition methods (#with_local). The central query is #type_of(node), the Rigor counterpart of PHPStan’s $scope->getType($node).

See docs/internal-spec/inference-engine.md for the binding contract. rubocop:disable Metrics/ClassLength,Metrics/ParameterLists

Defined Under Namespace

Classes: ChainKey, IndexedKey

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, declared_types: EMPTY_DECLARED_TYPES, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, class_ivars: EMPTY_CLASS_BINDINGS, class_cvars: EMPTY_CLASS_BINDINGS, program_globals: EMPTY_VAR_BINDINGS, discovered_classes: EMPTY_VAR_BINDINGS, in_source_constants: EMPTY_VAR_BINDINGS, discovered_methods: EMPTY_CLASS_BINDINGS, discovered_def_nodes: EMPTY_CLASS_BINDINGS, discovered_def_sources: EMPTY_CLASS_BINDINGS, discovered_method_visibilities: EMPTY_CLASS_BINDINGS, discovered_superclasses: EMPTY_CLASS_BINDINGS, discovered_includes: EMPTY_CLASS_BINDINGS, discovered_class_sources: EMPTY_CLASS_BINDINGS, data_member_layouts: EMPTY_CLASS_BINDINGS, indexed_narrowings: EMPTY_INDEXED_NARROWINGS, method_chain_narrowings: EMPTY_CHAIN_NARROWINGS, source_path: nil) ⇒ Scope

Returns a new instance of Scope.



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
# File 'lib/rigor/scope.rb', line 79

def initialize(
  environment:, locals:,
  fact_store: Analysis::FactStore.empty,
  self_type: nil,
  declared_types: EMPTY_DECLARED_TYPES,
  ivars: EMPTY_VAR_BINDINGS,
  cvars: EMPTY_VAR_BINDINGS,
  globals: EMPTY_VAR_BINDINGS,
  class_ivars: EMPTY_CLASS_BINDINGS,
  class_cvars: EMPTY_CLASS_BINDINGS,
  program_globals: EMPTY_VAR_BINDINGS,
  discovered_classes: EMPTY_VAR_BINDINGS,
  in_source_constants: EMPTY_VAR_BINDINGS,
  discovered_methods: EMPTY_CLASS_BINDINGS,
  discovered_def_nodes: EMPTY_CLASS_BINDINGS,
  discovered_def_sources: EMPTY_CLASS_BINDINGS,
  discovered_method_visibilities: EMPTY_CLASS_BINDINGS,
  discovered_superclasses: EMPTY_CLASS_BINDINGS,
  discovered_includes: EMPTY_CLASS_BINDINGS,
  discovered_class_sources: EMPTY_CLASS_BINDINGS,
  data_member_layouts: EMPTY_CLASS_BINDINGS,
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
  source_path: nil
)
  @environment = environment
  @locals = locals
  @fact_store = fact_store
  @self_type = self_type
  @declared_types = declared_types
  @ivars = ivars
  @cvars = cvars
  @globals = globals
  @class_ivars = class_ivars
  @class_cvars = class_cvars
  @program_globals = program_globals
  @discovered_classes = discovered_classes
  @in_source_constants = in_source_constants
  @discovered_methods = discovered_methods
  @discovered_def_nodes = discovered_def_nodes
  @discovered_def_sources = discovered_def_sources
  @discovered_method_visibilities = discovered_method_visibilities
  @discovered_superclasses = discovered_superclasses
  @discovered_includes = discovered_includes
  @discovered_class_sources = discovered_class_sources
  @data_member_layouts = data_member_layouts
  @indexed_narrowings = indexed_narrowings
  @method_chain_narrowings = method_chain_narrowings
  @source_path = source_path
  freeze
end

Instance Attribute Details

#class_cvarsObject (readonly)

Returns the value of attribute class_cvars.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def class_cvars
  @class_cvars
end

#class_ivarsObject (readonly)

Returns the value of attribute class_ivars.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def class_ivars
  @class_ivars
end

#cvarsObject (readonly)

Returns the value of attribute cvars.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def cvars
  @cvars
end

#data_member_layoutsObject (readonly)

Returns the value of attribute data_member_layouts.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def data_member_layouts
  @data_member_layouts
end

#declared_typesObject (readonly)

Returns the value of attribute declared_types.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def declared_types
  @declared_types
end

#discovered_class_sourcesObject (readonly)

Returns the value of attribute discovered_class_sources.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_class_sources
  @discovered_class_sources
end

#discovered_classesObject (readonly)

Returns the value of attribute discovered_classes.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_classes
  @discovered_classes
end

#discovered_def_nodesObject (readonly)

Returns the value of attribute discovered_def_nodes.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_def_nodes
  @discovered_def_nodes
end

#discovered_def_sourcesObject (readonly)

Returns the value of attribute discovered_def_sources.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_def_sources
  @discovered_def_sources
end

#discovered_includesObject (readonly)

Returns the value of attribute discovered_includes.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_includes
  @discovered_includes
end

#discovered_method_visibilitiesObject (readonly)

Returns the value of attribute discovered_method_visibilities.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_method_visibilities
  @discovered_method_visibilities
end

#discovered_methodsObject (readonly)

Returns the value of attribute discovered_methods.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_methods
  @discovered_methods
end

#discovered_superclassesObject (readonly)

Returns the value of attribute discovered_superclasses.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def discovered_superclasses
  @discovered_superclasses
end

#environmentObject (readonly)

Returns the value of attribute environment.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def environment
  @environment
end

#fact_storeObject (readonly)

Returns the value of attribute fact_store.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def fact_store
  @fact_store
end

#globalsObject (readonly)

Returns the value of attribute globals.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def globals
  @globals
end

#in_source_constantsObject (readonly)

Returns the value of attribute in_source_constants.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def in_source_constants
  @in_source_constants
end

#indexed_narrowingsObject (readonly)

Returns the value of attribute indexed_narrowings.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def indexed_narrowings
  @indexed_narrowings
end

#ivarsObject (readonly)

Returns the value of attribute ivars.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def ivars
  @ivars
end

#localsObject (readonly)

Returns the value of attribute locals.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def locals
  @locals
end

#method_chain_narrowingsObject (readonly)

Returns the value of attribute method_chain_narrowings.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def method_chain_narrowings
  @method_chain_narrowings
end

#program_globalsObject (readonly)

Returns the value of attribute program_globals.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def program_globals
  @program_globals
end

#self_typeObject (readonly)

Returns the value of attribute self_type.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def self_type
  @self_type
end

#source_pathObject (readonly)

Returns the value of attribute source_path.



20
21
22
# File 'lib/rigor/scope.rb', line 20

def source_path
  @source_path
end

Class Method Details

.empty(environment: Environment.default, source_path: nil) ⇒ Object



73
74
75
76
# File 'lib/rigor/scope.rb', line 73

def empty(environment: Environment.default, source_path: nil)
  new(environment: environment, locals: {}.freeze,
      fact_store: Analysis::FactStore.empty, source_path: source_path)
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/rigor/scope.rb', line 655

def ==(other)
  other.is_a?(Scope) &&
    environment.equal?(other.environment) &&
    @locals == other.locals &&
    fact_store == other.fact_store &&
    self_type == other.self_type &&
    @ivars == other.ivars &&
    @cvars == other.cvars &&
    @globals == other.globals &&
    @indexed_narrowings == other.indexed_narrowings &&
    @method_chain_narrowings == other.method_chain_narrowings
end

#class_cvars_for(class_name) ⇒ Object

Slice 7 phase 6 — class-level cvar accumulator (same shape as ‘class_ivars` but populated from `Prism::ClassVariableWriteNode` writes, and seeded on BOTH instance and singleton method bodies because Ruby cvars are visible from each).



257
258
259
260
261
# File 'lib/rigor/scope.rb', line 257

def class_cvars_for(class_name)
  return EMPTY_VAR_BINDINGS if class_name.nil?

  @class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
end

#class_ivars_for(class_name) ⇒ Object

Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. ‘“Rigor::Scope”`); the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of that class. `StatementEvaluator#build_method_entry_scope` seeds the method body’s ‘ivars` map from this table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.

‘ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map is frozen and shared by structural reference across every derived scope.



243
244
245
246
247
# File 'lib/rigor/scope.rb', line 243

def class_ivars_for(class_name)
  return EMPTY_VAR_BINDINGS if class_name.nil?

  @class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS
end

#cvar(name) ⇒ Object



206
207
208
# File 'lib/rigor/scope.rb', line 206

def cvar(name)
  @cvars[name.to_sym]
end

#data_member_layout(class_name) ⇒ Object

ADR-48 — per-class table mapping a fully qualified class name to its ordered ‘Data.define` / `Struct.new` member-name list. Populated by `ScopeIndexer` for both the constant-assigned form (`Point = Data.define(:x, :y)`) and the named-subclass form (`class Point < Data.define(:x, :y)`). Consumed by Inference::MethodDispatcher::DataFolding so `Point.new(…)` on a `Singleton` receiver materialises a precise member instance. Returns nil when the class has no recorded layout.



460
461
462
463
464
465
466
467
# File 'lib/rigor/scope.rb', line 460

def data_member_layout(class_name)
  layout = @data_member_layouts[class_name.to_s]
  # Record the ancestry dependency only on a hit — DataFolding consults
  # this for every `Singleton[*].new`, and a miss (the common case: an
  # ordinary class) must not manufacture a spurious cross-file edge.
  record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
  layout
end

#discovered_method?(class_name, method_name, kind) ⇒ Boolean

Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a ‘Hash[Symbol, Symbol]` of `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically, even when no RBS sig describes them.

Returns:

  • (Boolean)


313
314
315
316
317
318
# File 'lib/rigor/scope.rb', line 313

def discovered_method?(class_name, method_name, kind)
  table = @discovered_methods[class_name.to_s]
  return false unless table

  table[method_name.to_sym] == kind
end

#discovered_method_visibility(class_name, method_name) ⇒ Object

v0.1.2 — per-class table mapping ‘method_name (Symbol) →:public | :private | :protected`. Populated by `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding `private` / `protected` / `public` modifier state plus any post-hoc `private :name, …` named-argument calls. Consumed by the `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.



528
529
530
531
532
533
# File 'lib/rigor/scope.rb', line 528

def discovered_method_visibility(class_name, method_name)
  table = @discovered_method_visibilities[class_name.to_s]
  return nil unless table

  table[method_name.to_sym]
end

#evaluate(node, tracer: nil) ⇒ Object

Statement-level evaluation: returns the pair ‘[type, scope’]‘ where `type` is what the node produces and `scope’‘ is the scope observable after the node has run. The receiver scope is never mutated. See Inference::StatementEvaluator for the catalogue of nodes that thread scope; everything else defers to #type_of and returns the receiver scope unchanged.



630
631
632
# File 'lib/rigor/scope.rb', line 630

def evaluate(node, tracer: nil)
  Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node)
end

#facts_for(target: nil, bucket: nil) ⇒ Object



612
613
614
# File 'lib/rigor/scope.rb', line 612

def facts_for(target: nil, bucket: nil)
  fact_store.facts_for(target: target, bucket: bucket)
end

#global(name) ⇒ Object



210
211
212
# File 'lib/rigor/scope.rb', line 210

def global(name)
  @globals[name.to_sym]
end

#hashObject



669
670
671
# File 'lib/rigor/scope.rb', line 669

def hash
  [Scope, environment.object_id, @locals, fact_store, self_type, @ivars, @cvars, @globals].hash
end

#includes_of(class_name) ⇒ 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. Populated by `ScopeIndexer` (per-file plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an implicit-self call resolves against an included module’s ‘def`s, not just the superclass chain. As-written names are resolved to qualified classes at walk time.



482
483
484
485
# File 'lib/rigor/scope.rb', line 482

def includes_of(class_name)
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
  @discovered_includes[class_name.to_s] || []
end

#indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object

Closes the “‘params ||= []; params << x`” precision gap (ROADMAP § Type-language / engine — indexed-collection narrowing through `Hash ||= default`). After `receiver ||= default`, the next read at `receiver` is known non-nil; recording the post-`||=` type keyed on `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper’s ‘[]` dispatch hand back the narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in `Inference::StatementEvaluator`.



549
550
551
# File 'lib/rigor/scope.rb', line 549

def indexed_narrowing(receiver_kind, receiver_name, key)
  @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
end

#ivar(name) ⇒ Object

Slice 7 phase 1 — instance/class/global variable bindings. ‘ivar(name)` / `cvar(name)` / `global(name)` return the type currently bound for the named variable, or `nil` when the variable has not been written in the analyzed slice of the program. The first cut tracks bindings only within a single method body (each `def` enters with a fresh binding map), so reads in other methods of the same class fall through to `Dynamic`. Cross-method ivar/cvar inference is a follow-up slice.



202
203
204
# File 'lib/rigor/scope.rb', line 202

def ivar(name)
  @ivars[name.to_sym]
end

#join(other) ⇒ Object

Joins this scope with another at a control-flow merge point. The joined scope is bound to every local that BOTH branches bind, with the type widened to the union of both sides. Names bound in only one branch are dropped from the joined scope; the eventual statement-level evaluator (Slice 3 phase 2) is responsible for nil-injecting half-bound names where the language semantics demand it. The two scopes MUST share the same Environment.

Raises:

  • (ArgumentError)


641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/rigor/scope.rb', line 641

def join(other)
  raise ArgumentError, "join requires a Rigor::Scope, got #{other.class}" unless other.is_a?(Scope)

  unless environment.equal?(other.environment)
    raise ArgumentError, "join requires both scopes to share the same Environment"
  end

  joined_locals = join_bindings(locals, other.locals)
  joined_ivars = join_bindings(ivars, other.ivars)
  joined_cvars = join_bindings(cvars, other.cvars)
  joined_globals = join_bindings(globals, other.globals)
  build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
end

#local(name) ⇒ Object



131
132
133
# File 'lib/rigor/scope.rb', line 131

def local(name)
  @locals[name.to_sym]
end

#local_facts(name, bucket: nil) ⇒ Object



616
617
618
# File 'lib/rigor/scope.rb', line 616

def local_facts(name, bucket: nil)
  facts_for(target: Analysis::FactStore::Target.local(name), bucket: bucket)
end

#method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object

Closes the “stable receiver method-chain narrowing” gap (ROADMAP § Future cycles / Type-language / engine —“Method-call receiver narrowing across stable receivers”; 2026-05-28 Redmine survey). After ‘if x.last.is_a?(Array)` the dominated body’s ‘x.last` reads MUST observe the truthy-narrowed type; the same chain reaching the falsey edge observes the negative narrowing.

Address shape mirrors #indexed_narrowing: stable root variable + no-arg single-hop method name. See ChainKey for the precise contract.



586
587
588
# File 'lib/rigor/scope.rb', line 586

def method_chain_narrowing(receiver_kind, receiver_name, method_name)
  @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
end

#superclass_of(class_name) ⇒ Object

ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN at the ‘class Foo < Bar` declaration (`“Bar”`, possibly a qualified `“A::B”`). Populated by `ScopeIndexer` — per-file plus the cross-file project pre-pass — and consumed by `ExpressionTyper#try_user_method_inference` to walk the superclass chain when an implicit-self call does not resolve against the enclosing class’s own defs. The as-written name is resolved to a qualified class at walk time against the call’s lexical nesting.



443
444
445
446
# File 'lib/rigor/scope.rb', line 443

def superclass_of(class_name)
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
  @discovered_superclasses[class_name.to_s]
end

#top_level_def_for(method_name) ⇒ Object

v0.0.3 A — top-level def lookup for implicit-self calls. Returns the ‘Prism::DefNode` for a top-level (or DSL-block-nested, outside any class body) `def <method_name>` in the file, or nil. The sentinel key is owned by `Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY`; consumers should treat its presence as an opaque implementation detail and go through this accessor.



380
381
382
383
384
385
# File 'lib/rigor/scope.rb', line 380

def top_level_def_for(method_name)
  table = @discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
  node = table && table[method_name.to_sym]
  record_cross_file_toplevel(method_name, node) if Analysis::DependencyRecorder.active?
  node
end

#toplevel?Boolean

ADR-34 § “Decision” — predicate identifying a toplevel-shaped scope (no enclosing ‘class` / `module` body). True at the top of a file AND inside a top-level `def` body (since toplevel defs leave `self_type` nil per the existing scope-construction contract, mirroring how ADR-24’s ‘adoptable_self_call_result?` also keys on `self_type.nil?` for the same context). Used by `CheckRules#unresolved_toplevel_diagnostic` to gate the `call.unresolved-toplevel` rule so it fires only outside class / module bodies, where Rails-DSL metaprogramming leniency (ADR-24 WD3 → WD4) does not apply.

Returns:

  • (Boolean)


330
331
332
# File 'lib/rigor/scope.rb', line 330

def toplevel?
  @self_type.nil?
end

#type_of(node, tracer: nil) ⇒ Object



620
621
622
# File 'lib/rigor/scope.rb', line 620

def type_of(node, tracer: nil)
  Inference::ExpressionTyper.new(scope: self, tracer: tracer).type_of(node)
end

#user_def_for(class_name, method_name) ⇒ Object

v0.0.2 #5 — per-class table mapping ‘method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer` alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.



347
348
349
350
351
352
# File 'lib/rigor/scope.rb', line 347

def user_def_for(class_name, method_name)
  table = @discovered_def_nodes[class_name.to_s]
  node = table && table[method_name.to_sym]
  record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active?
  node
end

#user_def_site_for(class_name, method_name) ⇒ Object

Companion to #user_def_for: returns the ‘“path:line”` where the project defines `class_name#method_name` (instance-side), or nil. Populated only by the cross-file project pre-pass (Inference::ScopeIndexer.discovered_def_index_for_paths) — a `Prism::Location` hides its source file, so the site is recorded at scan time. `CheckRules#undefined_method_diagnostic` consults this to name the defining file when a project monkey-patch on a core/stdlib/gem class is called cross-file, so the diagnostic can point at `pre_eval:` (ADR-17) instead of reading as a bare unresolved call.



422
423
424
425
426
427
# File 'lib/rigor/scope.rb', line 422

def user_def_site_for(class_name, method_name)
  table = @discovered_def_sources[class_name.to_s]
  return nil unless table

  table[method_name.to_sym]
end

#with_class_cvars(table) ⇒ Object



263
264
265
# File 'lib/rigor/scope.rb', line 263

def with_class_cvars(table)
  rebuild(class_cvars: table)
end

#with_class_ivars(table) ⇒ Object



249
250
251
# File 'lib/rigor/scope.rb', line 249

def with_class_ivars(table)
  rebuild(class_ivars: table)
end

#with_cvar(name, type) ⇒ Object



222
223
224
# File 'lib/rigor/scope.rb', line 222

def with_cvar(name, type)
  rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
end

#with_data_member_layouts(table) ⇒ Object



469
470
471
# File 'lib/rigor/scope.rb', line 469

def with_data_member_layouts(table)
  rebuild(data_member_layouts: table)
end

#with_declared_types(table) ⇒ Object

Slice A-declarations. Returns a scope that carries an identity-comparing Hash of ‘Prism::Node => Rigor::Type` overrides. `ExpressionTyper#type_of(node)` MUST consult `declared_types` before any other dispatch and return the recorded type as-is when present. The table is populated by `ScopeIndexer` for declaration-position nodes (the `constant_path` of `Prism::ModuleNode` and `Prism::ClassNode`) so a `module Foo` / `class Bar` header types as `Singleton[<qualified path>]` instead of falling through to `Dynamic`. The table is shared by structural reference across every derived scope so `with_local` / `with_fact` / `with_self_type` carry it transparently.



189
190
191
# File 'lib/rigor/scope.rb', line 189

def with_declared_types(table)
  rebuild(declared_types: table)
end

#with_discovered_class_sources(table) ⇒ Object

ADR-46 slice 1 — per-class table mapping a fully qualified user class/module name to the set of ‘“path:line”` sites that declare, reopen, set the superclass of, or `include` into it. Populated only by the cross-file project pre-pass (Inference::ScopeIndexer.discovered_def_index_for_paths) and consumed by #superclass_of / #includes_of when dependency recording is active: resolving a class’s ancestry edge records every file that contributes to that class’s declaration shape, so a later edit to any of them re-checks the consumer. Over-records by design (a superclass read also pulls in the include-declaring files) — the conservative direction ADR-46 mandates.



502
503
504
# File 'lib/rigor/scope.rb', line 502

def with_discovered_class_sources(table)
  rebuild(discovered_class_sources: table)
end

#with_discovered_classes(table) ⇒ Object

Slice 7 phase 7 — in-source class discovery. Maps a qualified class name (e.g. ‘“Account”`) to its `Type::Singleton` so references to user-defined classes in the analyzed files resolve through `ExpressionTyper#resolve_constant_name` even when no RBS decl exists. Populated once at index time by `ScopeIndexer` from every `Prism::ClassNode` and `Prism::ModuleNode` it walks.



285
286
287
# File 'lib/rigor/scope.rb', line 285

def with_discovered_classes(table)
  rebuild(discovered_classes: table)
end

#with_discovered_def_nodes(table) ⇒ Object



408
409
410
# File 'lib/rigor/scope.rb', line 408

def with_discovered_def_nodes(table)
  rebuild(discovered_def_nodes: table)
end

#with_discovered_def_sources(table) ⇒ Object



429
430
431
# File 'lib/rigor/scope.rb', line 429

def with_discovered_def_sources(table)
  rebuild(discovered_def_sources: table)
end

#with_discovered_includes(table) ⇒ Object



487
488
489
# File 'lib/rigor/scope.rb', line 487

def with_discovered_includes(table)
  rebuild(discovered_includes: table)
end

#with_discovered_method_visibilities(table) ⇒ Object



535
536
537
# File 'lib/rigor/scope.rb', line 535

def with_discovered_method_visibilities(table)
  rebuild(discovered_method_visibilities: table)
end

#with_discovered_methods(table) ⇒ Object



334
335
336
# File 'lib/rigor/scope.rb', line 334

def with_discovered_methods(table)
  rebuild(discovered_methods: table)
end

#with_discovered_superclasses(table) ⇒ Object



448
449
450
# File 'lib/rigor/scope.rb', line 448

def with_discovered_superclasses(table)
  rebuild(discovered_superclasses: table)
end

#with_fact(fact) ⇒ Object



153
154
155
# File 'lib/rigor/scope.rb', line 153

def with_fact(fact)
  rebuild(fact_store: fact_store.with_fact(fact))
end

#with_global(name, type) ⇒ Object



226
227
228
# File 'lib/rigor/scope.rb', line 226

def with_global(name, type)
  rebuild(globals: @globals.merge(name.to_sym => type).freeze)
end

#with_in_source_constants(table) ⇒ Object

Slice 7 phase 9 — in-source constant-value tracking. Maps a qualified constant name (e.g. ‘“BUCKETS”` or `“Rigor::Analysis::FactStore::BUCKETS”`) to the type of the rvalue assigned at its `Prism::ConstantWriteNode` / `Prism::ConstantPathWriteNode`. Populated by `ScopeIndexer` once at index time. `ExpressionTyper#resolve_constant_name` consults this map after class lookups so an in-source constant assignment overrides any RBS-declared constant of the same qualified name (matching Ruby’s runtime precedence: a constant defined in user code is the authoritative value).



300
301
302
# File 'lib/rigor/scope.rb', line 300

def with_in_source_constants(table)
  rebuild(in_source_constants: table)
end

#with_indexed_narrowing(receiver_kind, receiver_name, key, type) ⇒ Object



553
554
555
556
557
558
# File 'lib/rigor/scope.rb', line 553

def with_indexed_narrowing(receiver_kind, receiver_name, key, type)
  new_table = @indexed_narrowings.merge(
    indexed_key(receiver_kind, receiver_name, key) => type
  ).freeze
  rebuild(indexed_narrowings: new_table)
end

#with_ivar(name, type) ⇒ Object



214
215
216
217
218
219
220
# File 'lib/rigor/scope.rb', line 214

def with_ivar(name, type)
  new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
  new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
          indexed_narrowings: new_indexed_narrowings,
          method_chain_narrowings: new_chain_narrowings)
end

#with_local(name, type) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/rigor/scope.rb', line 135

def with_local(name, type)
  new_locals = @locals.merge(name.to_sym => type).freeze
  new_fact_store = fact_store.invalidate_target(Analysis::FactStore::Target.local(name))
  # Rebinding `name` invalidates every "after `receiver[key]
  # ||= default`" narrowing keyed on it — the slot at `name[*]`
  # is reachable through the old binding only, so the
  # next read against the new binding does not inherit the
  # earlier non-nil guarantee. The same logic applies to
  # method-chain narrowings: `x.last` after `x = something_new`
  # is a call on the new binding and any prior `is_a?`-driven
  # narrowing keyed on `(local, :x, :last)` no longer holds.
  new_indexed_narrowings = drop_indexed_narrowings_for(:local, name)
  new_chain_narrowings = drop_chain_narrowings_for(:local, name)
  rebuild(locals: new_locals, fact_store: new_fact_store,
          indexed_narrowings: new_indexed_narrowings,
          method_chain_narrowings: new_chain_narrowings)
end

#with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) ⇒ Object



590
591
592
593
594
595
# File 'lib/rigor/scope.rb', line 590

def with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type)
  new_table = @method_chain_narrowings.merge(
    chain_key(receiver_kind, receiver_name, method_name) => type
  ).freeze
  rebuild(method_chain_narrowings: new_table)
end

#with_program_globals(table) ⇒ Object

Slice 7 phase 6 — program-level globals accumulator. Globals are process-wide in Ruby, so the analyzer carries a single map (‘Hash[Symbol, Type]`) keyed by the variable name and seeded into every method body (instance and singleton) plus the top-level program scope. `ScopeIndexer` populates it from a single program-wide pre-pass.



273
274
275
# File 'lib/rigor/scope.rb', line 273

def with_program_globals(table)
  rebuild(program_globals: table)
end

#with_self_type(type) ⇒ Object

Slice A-engine. Returns a scope with ‘self_type` set to `type`, preserving locals and facts. `StatementEvaluator` injects this at class-body and method-body boundaries; `ExpressionTyper` consults it when typing `Prism::SelfNode` and implicit-self `Prism::CallNode` receivers.



162
163
164
# File 'lib/rigor/scope.rb', line 162

def with_self_type(type)
  rebuild(self_type: type)
end

#with_source_path(path) ⇒ Object

ADR-11 per-call-site assertion gating prerequisite. The analyzer’s per-file boundary stamps the current source file’s path onto the seed scope; nested rebuilds carry the value through so plugin hooks like ‘flow_contribution_for` can resolve “which file does this call site belong to?” without thread-locals.



172
173
174
# File 'lib/rigor/scope.rb', line 172

def with_source_path(path)
  rebuild(source_path: path)
end

#without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object



560
561
562
563
564
565
566
# File 'lib/rigor/scope.rb', line 560

def without_indexed_narrowing(receiver_kind, receiver_name, key)
  lookup = indexed_key(receiver_kind, receiver_name, key)
  return self unless @indexed_narrowings.key?(lookup)

  new_table = @indexed_narrowings.reject { |k, _| k == lookup }.freeze
  rebuild(indexed_narrowings: new_table)
end

#without_indexed_narrowings_for(receiver_kind, receiver_name) ⇒ Object



568
569
570
571
572
573
# File 'lib/rigor/scope.rb', line 568

def without_indexed_narrowings_for(receiver_kind, receiver_name)
  new_table = drop_indexed_narrowings_for(receiver_kind, receiver_name)
  return self if new_table.equal?(@indexed_narrowings)

  rebuild(indexed_narrowings: new_table)
end

#without_method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object



597
598
599
600
601
602
603
# File 'lib/rigor/scope.rb', line 597

def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
  lookup = chain_key(receiver_kind, receiver_name, method_name)
  return self unless @method_chain_narrowings.key?(lookup)

  new_table = @method_chain_narrowings.reject { |k, _| k == lookup }.freeze
  rebuild(method_chain_narrowings: new_table)
end

#without_method_chain_narrowings_for(receiver_kind, receiver_name) ⇒ Object



605
606
607
608
609
610
# File 'lib/rigor/scope.rb', line 605

def without_method_chain_narrowings_for(receiver_kind, receiver_name)
  new_table = drop_chain_narrowings_for(receiver_kind, receiver_name)
  return self if new_table.equal?(@method_chain_narrowings)

  rebuild(method_chain_narrowings: new_table)
end