Class: Rigor::Scope

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/scope.rb,
lib/rigor/scope/discovery_index.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, DiscoveryIndex, 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, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, discovery: DiscoveryIndex::EMPTY, indexed_narrowings: EMPTY_INDEXED_NARROWINGS, method_chain_narrowings: EMPTY_CHAIN_NARROWINGS, declaration_sourced: EMPTY_DECLARATION_SOURCED, source_path: nil, struct_fold_safe_locals: EMPTY_FOLD_SAFE, dynamic_origins: {}.compare_by_identity) ⇒ Scope

Returns a new instance of Scope.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rigor/scope.rb', line 131

def initialize(
  environment:, locals:,
  fact_store: Analysis::FactStore.empty,
  self_type: nil,
  ivars: EMPTY_VAR_BINDINGS,
  cvars: EMPTY_VAR_BINDINGS,
  globals: EMPTY_VAR_BINDINGS,
  discovery: DiscoveryIndex::EMPTY,
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
  declaration_sourced: EMPTY_DECLARATION_SOURCED,
  source_path: nil,
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
  dynamic_origins: {}.compare_by_identity
)
  @environment = environment
  @locals = locals
  @fact_store = fact_store
  @self_type = self_type
  @ivars = ivars
  @cvars = cvars
  @globals = globals
  @discovery = discovery
  @indexed_narrowings = indexed_narrowings
  @method_chain_narrowings = method_chain_narrowings
  @declaration_sourced = declaration_sourced
  @source_path = source_path
  @struct_fold_safe_locals = struct_fold_safe_locals
  @dynamic_origins = dynamic_origins
  freeze
end

Instance Attribute Details

#cvarsObject (readonly)

Returns the value of attribute cvars.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def cvars
  @cvars
end

#declaration_sourcedObject (readonly)

Returns the value of attribute declaration_sourced.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def declaration_sourced
  @declaration_sourced
end

#discoveryObject (readonly)

Returns the value of attribute discovery.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def discovery
  @discovery
end

#dynamic_originsObject (readonly)

Returns the value of attribute dynamic_origins.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def dynamic_origins
  @dynamic_origins
end

#environmentObject (readonly)

Returns the value of attribute environment.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def environment
  @environment
end

#fact_storeObject (readonly)

Returns the value of attribute fact_store.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def fact_store
  @fact_store
end

#globalsObject (readonly)

Returns the value of attribute globals.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def globals
  @globals
end

#indexed_narrowingsObject (readonly)

Returns the value of attribute indexed_narrowings.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def indexed_narrowings
  @indexed_narrowings
end

#ivarsObject (readonly)

Returns the value of attribute ivars.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def ivars
  @ivars
end

#localsObject (readonly)

Returns the value of attribute locals.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def locals
  @locals
end

#method_chain_narrowingsObject (readonly)

Returns the value of attribute method_chain_narrowings.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def method_chain_narrowings
  @method_chain_narrowings
end

#self_typeObject (readonly)

Returns the value of attribute self_type.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def self_type
  @self_type
end

#source_pathObject (readonly)

Returns the value of attribute source_path.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def source_path
  @source_path
end

#struct_fold_safe_localsObject (readonly)

Returns the value of attribute struct_fold_safe_locals.



22
23
24
# File 'lib/rigor/scope.rb', line 22

def struct_fold_safe_locals
  @struct_fold_safe_locals
end

Class Method Details

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



120
121
122
123
# File 'lib/rigor/scope.rb', line 120

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?



698
699
700
701
702
703
704
705
706
707
708
709
710
# File 'lib/rigor/scope.rb', line 698

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 &&
    @declaration_sourced == other.declaration_sourced
end

#class_cvarsObject



42
# File 'lib/rigor/scope.rb', line 42

def class_cvars = @discovery.class_cvars

#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).



350
351
352
353
354
# File 'lib/rigor/scope.rb', line 350

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

  @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
end

#class_ivarsObject



41
# File 'lib/rigor/scope.rb', line 41

def class_ivars = @discovery.class_ivars

#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.



340
341
342
343
344
# File 'lib/rigor/scope.rb', line 340

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

  @discovery.class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS
end

#cvar(name) ⇒ Object



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

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.



513
514
515
516
517
518
519
520
# File 'lib/rigor/scope.rb', line 513

def data_member_layout(class_name)
  layout = @discovery.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

#data_member_layoutsObject



54
# File 'lib/rigor/scope.rb', line 54

def data_member_layouts = @discovery.data_member_layouts

#declaration_sourced?(kind, name) ⇒ Boolean

ADR-58 WD1 — true when ‘(kind, name)`’s binding optionality is purely declaration-sourced (no flow-live write/narrowing has touched it).

Returns:

  • (Boolean)


298
299
300
# File 'lib/rigor/scope.rb', line 298

def declaration_sourced?(kind, name)
  @declaration_sourced.include?([kind.to_sym, name.to_sym])
end

#declared_typesObject

ADR-53 Track A — the seed-time discovery tables live on the DiscoveryIndex the scope carries by a single reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the extraction. The whole index is swapped in one transition through #with_discovery.

‘declared_types` carries the identity-comparing `Prism::Node => Rigor::Type` declaration overrides `ExpressionTyper#type_of(node)` MUST consult before any other dispatch (a `module Foo` / `class Bar` header types as `Singleton[<qualified path>]` rather than `Dynamic`).



40
# File 'lib/rigor/scope.rb', line 40

def declared_types = @discovery.declared_types

#discovered_class_sourcesObject



53
# File 'lib/rigor/scope.rb', line 53

def discovered_class_sources = @discovery.discovered_class_sources

#discovered_classesObject



44
# File 'lib/rigor/scope.rb', line 44

def discovered_classes = @discovery.discovered_classes

#discovered_def_nodesObject



47
# File 'lib/rigor/scope.rb', line 47

def discovered_def_nodes = @discovery.discovered_def_nodes

#discovered_def_sourcesObject



49
# File 'lib/rigor/scope.rb', line 49

def discovered_def_sources = @discovery.discovered_def_sources

#discovered_includesObject



52
# File 'lib/rigor/scope.rb', line 52

def discovered_includes = @discovery.discovered_includes

#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)


365
366
367
368
369
370
# File 'lib/rigor/scope.rb', line 365

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

  table[method_name.to_sym] == kind
end

#discovered_method_visibilitiesObject



50
# File 'lib/rigor/scope.rb', line 50

def discovered_method_visibilities = @discovery.discovered_method_visibilities

#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.



575
576
577
578
579
580
# File 'lib/rigor/scope.rb', line 575

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

  table[method_name.to_sym]
end

#discovered_methodsObject



46
# File 'lib/rigor/scope.rb', line 46

def discovered_methods = @discovery.discovered_methods

#discovered_singleton_def_nodesObject



48
# File 'lib/rigor/scope.rb', line 48

def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes

#discovered_superclassesObject



51
# File 'lib/rigor/scope.rb', line 51

def discovered_superclasses = @discovery.discovered_superclasses

#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.



673
674
675
# File 'lib/rigor/scope.rb', line 673

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

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



655
656
657
# File 'lib/rigor/scope.rb', line 655

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

#forget_match_globalsObject



321
322
323
324
325
# File 'lib/rigor/scope.rb', line 321

def forget_match_globals
  return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) }

  rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
end

#global(name) ⇒ Object



253
254
255
# File 'lib/rigor/scope.rb', line 253

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

#hashObject



713
714
715
# File 'lib/rigor/scope.rb', line 713

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

#in_source_constantsObject



45
# File 'lib/rigor/scope.rb', line 45

def in_source_constants = @discovery.in_source_constants

#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.



545
546
547
548
# File 'lib/rigor/scope.rb', line 545

def includes_of(class_name)
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
  @discovery.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`.



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

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.



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

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)


684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/rigor/scope.rb', line 684

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



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

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

#local_facts(name, bucket: nil) ⇒ Object



659
660
661
# File 'lib/rigor/scope.rb', line 659

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.



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

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

#param_inferred_typesObject

ADR-67 WD3 — call-site-inferred parameter types, keyed by ‘[class_name, method_name, kind]`. `build_method_entry_scope` consults this to seed an undeclared `def` parameter with the union of its resolved call-site argument types (precision-additive; an RBS-declared parameter always wins). Empty unless a collection pass seeded it.



61
# File 'lib/rigor/scope.rb', line 61

def param_inferred_types = @discovery.param_inferred_types

#program_globalsObject



43
# File 'lib/rigor/scope.rb', line 43

def program_globals = @discovery.program_globals

#record_dynamic_origin(node, cause) ⇒ Object



126
127
128
129
# File 'lib/rigor/scope.rb', line 126

def record_dynamic_origin(node, cause)
  @dynamic_origins[node] = cause
  self
end

#seed_declaration_sourced_ivar(name, type) ⇒ Object

ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index. Unlike ‘with_ivar` this binds the type AND records the declaration-sourced mark in one transition.



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

def seed_declaration_sourced_ivar(name, type)
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
          declaration_sourced: add_declaration_sourced(:ivar, name))
end

#singleton_def_for(class_name, method_name) ⇒ Object

Module-singleton call resolution (ADR-57 follow-up) — companion of #user_def_for for SINGLETON-side defs (‘def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The `discovered_def_nodes` table is deliberately instance-side only (its ancestor walk binds `self` as `Nominal`), so singleton bodies live in a parallel table the `ScopeIndexer` populates alongside it. Records the same cross-file dependency edge as the instance path (ADR-46).



412
413
414
415
416
417
# File 'lib/rigor/scope.rb', line 412

def singleton_def_for(class_name, method_name)
  table = @discovery.discovered_singleton_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

#struct_fold_safe?(name) ⇒ Boolean

True when ‘name`’s ‘Struct` member reads are fold-safe in this body (the local is provably never mutated / aliased / escaped).

Returns:

  • (Boolean)


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

def struct_fold_safe?(name)
  @struct_fold_safe_locals.include?(name.to_sym)
end

#struct_member_layout(class_name) ⇒ Object

ADR-48 Struct follow-up — the ‘{ members:, keyword_init: }` layout recorded for a `Struct.new(…)`-defined class, in the constant form (`Point = Struct.new(:x, :y)`) and the named-subclass form (`class Point < Struct.new(:x, :y)`). Consumed by Inference::MethodDispatcher::StructFolding so `Point.new(…)` on a `Singleton` receiver materialises a member instance. Returns nil when the class has no recorded struct layout. Mirrors #data_member_layout’s dependency-recording contract.



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

def struct_member_layout(class_name)
  layout = @discovery.struct_member_layouts[class_name.to_s]
  record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
  layout
end

#struct_member_layoutsObject



55
56
57
58
59
60
# File 'lib/rigor/scope.rb', line 55

def struct_member_layouts = @discovery.struct_member_layouts
# ADR-67 WD3 — call-site-inferred parameter types, keyed by
# `[class_name, method_name, kind]`. `build_method_entry_scope` consults
# this to seed an undeclared `def` parameter with the union of its
# resolved call-site argument types (precision-additive; an RBS-declared
# parameter always wins). Empty unless a collection pass seeded it.

#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.



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

def superclass_of(class_name)
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
  @discovery.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.



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

def top_level_def_for(method_name)
  table = @discovery.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 — the same nil-`self_type` signal ADR-24’s self-call return adoption historically keyed on before ADR-57 opened the gate unconditionally). 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)


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

def toplevel?
  @self_type.nil?
end

#type_of(node, tracer: nil) ⇒ Object



663
664
665
# File 'lib/rigor/scope.rb', line 663

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.



396
397
398
399
400
401
# File 'lib/rigor/scope.rb', line 396

def user_def_for(class_name, method_name)
  table = @discovery.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.



483
484
485
486
487
488
# File 'lib/rigor/scope.rb', line 483

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

  table[method_name.to_sym]
end

#with_cvar(name, type) ⇒ Object



302
303
304
# File 'lib/rigor/scope.rb', line 302

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

#with_declaration_sourced_local(name, type) ⇒ Object

ADR-58 WD1 — a local assignment ‘r = @right` whose RHS is a pure read of a declaration-sourced ivar inherits the mark, so the survey’s exact rotation/traversal shape (‘r = @right; r.key`) does not fire. Binds the type and stamps the local’s mark in one transition (the plain ‘with_local` would have dropped it).



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

def with_declaration_sourced_local(name, type)
  written = with_local(name, type)
  written.with_local_declaration_mark(name)
end

#with_discovery(index) ⇒ Object

ADR-53 Track A — swaps the whole discovery index in one transition. The sole seeding path; the per-table writers it replaced are derived off-‘Scope` through `scope.discovery.with(table_name: table)`.



232
233
234
# File 'lib/rigor/scope.rb', line 232

def with_discovery(index)
  rebuild(discovery: index)
end

#with_fact(fact) ⇒ Object



193
194
195
# File 'lib/rigor/scope.rb', line 193

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

#with_global(name, type) ⇒ Object



306
307
308
# File 'lib/rigor/scope.rb', line 306

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

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



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

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



257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/rigor/scope.rb', line 257

def with_ivar(name, type)
  new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
  new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
  # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live:
  # drop any declaration-sourced mark so subsequent reads of `@name`
  # observe flow-live provenance and fire as before. The seed path uses
  # `seed_declaration_sourced_ivar` to (re-)establish the mark.
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
          indexed_narrowings: new_indexed_narrowings,
          method_chain_narrowings: new_chain_narrowings,
          declaration_sourced: drop_declaration_sourced_for(:ivar, name))
end

#with_local(name, type) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rigor/scope.rb', line 167

def with_local(name, type)
  # `rigor trace` — the moment a local enters the scope.
  Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
  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)
  # ADR-58 WD1 — rebinding a local is a flow-live touch: any prior
  # declaration-sourced mark on `name` no longer holds (the new value
  # may carry a method-local nil). `with_declaration_sourced_local`
  # re-establishes the mark afterward when the RHS is a pure copy of a
  # declaration-sourced ivar read; the default is to drop it.
  rebuild(locals: new_locals, fact_store: new_fact_store,
          indexed_narrowings: new_indexed_narrowings,
          method_chain_narrowings: new_chain_narrowings,
          declaration_sourced: drop_declaration_sourced_for(:local, name))
end

#with_local_declaration_mark(name) ⇒ Object

ADR-58 WD1 — re-stamp the local mark on a scope produced by ‘with_local` (which always drops it). Public so the sibling `with_declaration_sourced_local` can call it across the new post-write receiver without reaching into a private method.



292
293
294
# File 'lib/rigor/scope.rb', line 292

def with_local_declaration_mark(name)
  rebuild(declaration_sourced: add_declaration_sourced(:local, name))
end

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



633
634
635
636
637
638
# File 'lib/rigor/scope.rb', line 633

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_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.



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

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

#with_source_path(path) ⇒ Object

ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope. The analyzer stamps the current file’s path onto the seed scope; nested rebuilds propagate it so plugin rules (‘dynamic_return`’s ‘file_methods:` gate, sigil checks) can resolve “which file does this call site belong to?” without thread-locals.



212
213
214
# File 'lib/rigor/scope.rb', line 212

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

#with_struct_fold_safe(locals) ⇒ Object

ADR-48 Struct slice 3 — installs the per-body fold-safe-local set (Inference::StructFoldSafety). Set once at body entry; inherited unchanged through subsequent flow transitions.



219
220
221
# File 'lib/rigor/scope.rb', line 219

def with_struct_fold_safe(locals)
  rebuild(struct_fold_safe_locals: locals)
end

#without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object



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

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



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

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



640
641
642
643
644
645
646
# File 'lib/rigor/scope.rb', line 640

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



648
649
650
651
652
653
# File 'lib/rigor/scope.rb', line 648

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