Class: Rigor::Environment

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/environment.rb,
lib/rigor/environment/rbs_loader.rb,
lib/rigor/environment/reflection.rb,
lib/rigor/environment/rbs_hierarchy.rb,
lib/rigor/environment/class_registry.rb,
lib/rigor/environment/lockfile_resolver.rb,
lib/rigor/environment/rbs_coverage_report.rb,
lib/rigor/environment/bundle_sig_discovery.rb,
lib/rigor/environment/rbs_collection_discovery.rb

Overview

The engine’s view of the type universe outside the current scope. Slice 1 only exposed the class registry; Slice 4 adds the RBS loader, which threads through ExpressionTyper and MethodDispatcher to type constant references and method calls that the literal-typer and constant-folding tiers cannot answer.

See docs/internal-spec/inference-engine.md for the binding contract.

Defined Under Namespace

Modules: BundleSigDiscovery, LockfileResolver, RbsCollectionDiscovery, RbsCoverageReport Classes: ClassRegistry, RbsHierarchy, RbsLoader, Reflection

Constant Summary collapse

DEFAULT_LIBRARIES =

Slice A stdlib expansion. Stdlib libraries that ‘Environment.for_project` loads on top of RBS core unless the caller passes an explicit `libraries:` array. Each entry MUST be a stdlib library name accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common stdlib surface a Ruby program is likely to import (`pathname`, `optparse`, `json`, `yaml`, `fileutils`, `tempfile`, `uri`, `logger`, `date`) plus the analyzer- adjacent gems shipping their own RBS in this bundle (`prism`, `rbs`). On hosts where one of these libraries is not installed, the loader silently drops it.

Callers MAY add to the default by passing ‘libraries: %w[csv …]`; the explicit list is appended to `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an `RbsLoader` directly instead of going through `for_project`.

%w[
  pathname optparse json yaml fileutils tempfile tmpdir
  stringio forwardable digest securerandom
  uri logger date
  pp delegate observable abbrev find tsort singleton
  shellwords benchmark base64 did_you_mean
  monitor mutex_m timeout
  open3 erb etc ipaddr bigdecimal bigdecimal-math
  prettyprint random-formatter time open-uri resolv
  csv pstore objspace io-console cgi cgi-escape
  strscan
  prism rbs
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(class_registry: ClassRegistry.default, rbs_loader: nil, plugin_registry: nil, dependency_source_index: nil, rbs_extended_reporter: nil, boundary_cross_reporter: nil, synthetic_method_index: nil) ⇒ Environment

Returns a new instance of Environment.

Parameters:

  • class_registry (Rigor::Environment::ClassRegistry) (defaults to: ClassRegistry.default)
  • rbs_loader (Rigor::Environment::RbsLoader, nil) (defaults to: nil)

    when nil the environment is “RBS-blind”; useful in tests that want to assert how the engine behaves without RBS data. The default Environment wires the shared core loader, which is itself lazy: requesting an environment instance does NOT load RBS until a method or class query actually consults the loader.

  • plugin_registry (Rigor::Plugin::Registry, nil) (defaults to: nil)

    v0.1.1 Track 2 slice 7. The per-run plugin registry the inference engine consults at call sites for plugin ‘#flow_contribution_for` overrides. When nil (the default), no plugin-level return-type contribution participates — useful for tests, the `Environment.default` facade, and analyses that don’t load plugins.

  • dependency_source_index (Rigor::Analysis::DependencySourceInference::Index, nil) (defaults to: nil)

    ADR-10 slice 2b-ii. The per-run index of opt-in gem sources the dispatcher consults BELOW RBS dispatch. When nil (the default), no dep-source contribution participates and the dispatcher tier is a no-op.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rigor/environment.rb', line 82

def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
               plugin_registry: nil, dependency_source_index: nil,
               rbs_extended_reporter: nil, boundary_cross_reporter: nil,
               synthetic_method_index: nil)
  @class_registry = class_registry
  @rbs_loader = rbs_loader
  @plugin_registry = plugin_registry
  @dependency_source_index = dependency_source_index
  @rbs_extended_reporter = rbs_extended_reporter
  @boundary_cross_reporter = boundary_cross_reporter
  @synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
  @name_scope = build_name_scope
  freeze
end

Instance Attribute Details

#boundary_cross_reporterObject (readonly)

Returns the value of attribute boundary_cross_reporter.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def boundary_cross_reporter
  @boundary_cross_reporter
end

#class_registryObject (readonly)

Returns the value of attribute class_registry.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def class_registry
  @class_registry
end

#dependency_source_indexObject (readonly)

Returns the value of attribute dependency_source_index.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def dependency_source_index
  @dependency_source_index
end

#name_scopeObject (readonly)

Returns the value of attribute name_scope.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def name_scope
  @name_scope
end

#plugin_registryObject (readonly)

Returns the value of attribute plugin_registry.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def plugin_registry
  @plugin_registry
end

#rbs_extended_reporterObject (readonly)

Returns the value of attribute rbs_extended_reporter.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def rbs_extended_reporter
  @rbs_extended_reporter
end

#rbs_loaderObject (readonly)

Returns the value of attribute rbs_loader.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def rbs_loader
  @rbs_loader
end

#synthetic_method_indexObject (readonly)

Returns the value of attribute synthetic_method_index.



59
60
61
# File 'lib/rigor/environment.rb', line 59

def synthetic_method_index
  @synthetic_method_index
end

Class Method Details

.defaultObject



98
99
100
# File 'lib/rigor/environment.rb', line 98

def default
  @default ||= new(rbs_loader: RbsLoader.default).freeze
end

.for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, plugin_registry: nil, dependency_source_index: nil, rbs_extended_reporter: nil, boundary_cross_reporter: nil, bundler_bundle_path: nil, bundler_auto_detect: false, bundler_lockfile: nil, rbs_collection_lockfile: nil, rbs_collection_auto_detect: false, synthetic_method_index: nil) ⇒ Rigor::Environment

Builds an Environment that consults the project’s local signatures and any opt-in stdlib libraries on top of RBS core.

rubocop:disable Metrics/MethodLength, Metrics/ParameterLists

Parameters:

  • root (String, Pathname) (defaults to: Dir.pwd)

    project root used to auto-detect the default signature path. Defaults to the current working directory.

  • libraries (Array<String, Symbol>) (defaults to: [])

    additional stdlib libraries to load on top of DEFAULT_LIBRARIES. The final list is the union of the two, de-duplicated while preserving order. Pass an empty array (the default) to load only the defaults.

  • signature_paths (Array<String, Pathname>, nil) (defaults to: nil)

    explicit list of ‘sig/`-style directories. When `nil` (the default), the canonical project layout `<root>/sig` is used if it exists, otherwise no signature path is loaded.

  • cache_store (Rigor::Cache::Store, nil) (defaults to: nil)

    persistent cache threaded into the underlying RbsLoader so constant lookups (and, in later v0.0.9 slices, other reflection artefacts) consult the cache. Pass ‘nil` (the default) to skip caching for this environment.

Returns:



124
125
126
127
128
129
130
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/rigor/environment.rb', line 124

def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
                plugin_registry: nil, dependency_source_index: nil,
                rbs_extended_reporter: nil, boundary_cross_reporter: nil,
                bundler_bundle_path: nil, bundler_auto_detect: false,
                bundler_lockfile: nil,
                rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
                synthetic_method_index: nil)
  resolved_paths = signature_paths || default_signature_paths(root)
  # O4 MVP — append per-gem `sig/` directories discovered
  # under the target project's bundler install root. Empty
  # array when neither an explicit path nor auto-detection
  # finds a bundle. Order: user `signature_paths:` win first
  # (semantic precedence inside `RbsLoader.build_env_for`);
  # gem-shipped sigs append last so user overrides stay
  # authoritative.
  #
  # O4 Layer 3 — when a Gemfile.lock is available (explicit
  # `bundler_lockfile:` or auto-detected next to the project
  # root), use the locked gem set to filter the discovered
  # `sig/` directories. Stale gems in the bundle install
  # tree (out-of-band installs, version drift after a
  # `bundle update`) are silently dropped so only gems the
  # project actually declares contribute RBS.
  locked = LockfileResolver.locked_gems(
    lockfile_path: bundler_lockfile,
    project_root: root,
    auto_detect: bundler_auto_detect
  )
  gem_sig_paths = BundleSigDiscovery.discover(
    bundle_path: bundler_bundle_path,
    project_root: root,
    auto_detect: bundler_auto_detect,
    locked_gems: locked.empty? ? nil : locked
  ).map(&:to_s)
  # O4 Layer 3 slice 2 — when `rbs collection install`
  # has been run for the target project, parse the
  # resulting `rbs_collection.lock.yaml` and feed each
  # gem's `<collection_path>/<name>/<version>/` directory
  # into `signature_paths:`. Stdlib-typed entries are
  # skipped (already covered by `DEFAULT_LIBRARIES`).
  collection_paths = RbsCollectionDiscovery.discover(
    lockfile_path: rbs_collection_lockfile,
    project_root: root,
    auto_detect: rbs_collection_auto_detect
  ).map(&:to_s)
  loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
  loader = RbsLoader.new(
    libraries: merged_libraries,
    signature_paths: loader_signature_paths,
    cache_store: cache_store
  )
  new(
    rbs_loader: loader,
    plugin_registry: plugin_registry,
    dependency_source_index: dependency_source_index,
    rbs_extended_reporter: rbs_extended_reporter,
    boundary_cross_reporter: boundary_cross_reporter,
    synthetic_method_index: synthetic_method_index
  )
end

Instance Method Details

#class_known?(name) ⇒ Boolean

Returns true when the constant name is known to either the static registry or the RBS loader. Useful for callers that only need a presence check without materialising a type carrier.

Returns:

  • (Boolean)


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

def class_known?(name)
  return true if class_registry.nominal_for_name(name)

  class_known_in_rbs?(name)
end

#class_ordering(lhs, rhs) ⇒ Object

Compares two class/module names using analyzer-owned class data. Returns ‘:equal`, `:subclass`, `:superclass`, `:disjoint`, or `:unknown`. The static registry handles built-ins cheaply; the RBS loader handles project/stdlib classes without relying on host Ruby constants being loaded.



265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/rigor/environment.rb', line 265

def class_ordering(lhs, rhs)
  lhs = normalize_class_name(lhs)
  rhs = normalize_class_name(rhs)
  return :equal if lhs == rhs

  registry_result = class_registry.class_ordering(lhs, rhs)
  return registry_result unless registry_result == :unknown

  return :unknown unless rbs_loader

  rbs_loader.class_ordering(lhs, rhs)
end

#constant_for_name(name) ⇒ Object

Slice A constant-value lookup. Returns the translated ‘Rigor::Type` for an RBS-declared non-class constant (`Rigor::Analysis::FactStore::BUCKETS: Array`, `Rigor::Configuration::DEFAULT_PATH: String`, …) or `nil` when no RBS constant declaration covers `name`. This is the value-bearing counterpart of #singleton_for_name, which only resolves names that name a class or module. Callers that need to type a `Prism::ConstantReadNode`/ `Prism::ConstantPathNode` MUST consult #singleton_for_name first and fall through to this query when the constant is not a class.



234
235
236
237
238
# File 'lib/rigor/environment.rb', line 234

def constant_for_name(name)
  return nil if rbs_loader.nil?

  rbs_loader.constant_type(name.to_s)
end

#nominal_for_name(name) ⇒ Object

Resolves a constant name to a Rigor::Type::Nominal (the instance type carrier). Consults the static class registry first (cheap, hardcoded), then falls back to the RBS loader. Returns nil when the name is unknown to both.

NOTE: This is the construction helper for “an instance of class ‘Foo`”. For “the class object `Foo` itself” (the value of the constant), use #singleton_for_name instead.



203
204
205
206
207
208
# File 'lib/rigor/environment.rb', line 203

def nominal_for_name(name)
  registered = class_registry.nominal_for_name(name)
  return registered if registered

  class_known_in_rbs?(name) ? Type::Combinator.nominal_of(name.to_s) : nil
end

#reflectionObject

ADR-15 Phase 2b — returns the loader’s read-only, ‘Ractor.shareable?` query surface as a frozen Reflection. Built lazily on first access; subsequent calls return the same instance. Returns `nil` when the environment carries no RBS loader (test-only `Environment.new` without `rbs_loader:`).



256
257
258
# File 'lib/rigor/environment.rb', line 256

def reflection
  @rbs_loader&.reflection
end

#singleton_for_name(name) ⇒ Object

Resolves a constant name to a Rigor::Type::Singleton (the *class object* carrier). The expression ‘Foo` evaluates to the class object, whose RBS type is `singleton(Foo)` – this method is the corresponding Rigor construction helper.

The lookup uses the same registry/RBS chain as #nominal_for_name so a class is either known to both queries or to neither.



217
218
219
220
221
# File 'lib/rigor/environment.rb', line 217

def singleton_for_name(name)
  return nil unless class_known?(name)

  Type::Combinator.singleton_of(name.to_s)
end