Class: Rigor::Analysis::Runner

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

Overview

rubocop:disable Metrics/ClassLength

Constant Summary collapse

RUBY_GLOB =
"**/*.rb"
DEFAULT_CACHE_ROOT =
".rigor/cache"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration:, explain: false, cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT), plugin_requirer: nil, workers: 0, collect_stats: true) ⇒ Runner

Returns a new instance of Runner.

Parameters:

  • configuration (Rigor::Configuration)
  • explain (Boolean) (defaults to: false)

    surface fail-soft fallback events as ‘:info` diagnostics.

  • cache_store (Rigor::Cache::Store, nil) (defaults to: Cache::Store.new(root: DEFAULT_CACHE_ROOT))

    the persistent cache the runner exposes to producers (‘RbsConstantTable` and successors). Pass `nil` to disable caching for this run; the CLI’s ‘–no-cache` flag wires `nil` through. v0.0.9 group A slice 1 introduces the surface; later slices route real producers through it.

  • workers (Integer) (defaults to: 0)

    ADR-15 Phase 4b — when greater than zero, per-file analysis dispatches across a pool of N Ractor workers built around WorkerSession. Default ‘0` keeps the sequential code path bit-for-bit unchanged. Phase 4c will wire the CLI / `.rigor.yml` surface that produces non-zero values; this slice leaves the parameter as a programmatic opt-in only.

  • collect_stats (Boolean) (defaults to: true)

    when true (default), ‘#run` builds a Rigor::Analysis::RunStats summary exposed via `result.stats` — this forces the RBS env build at end-of-run so the `class_decl_paths` snapshot has real source attribution. Set to false to skip the stats summary entirely; the CLI’s ‘–no-stats` threads `false` through to keep trivial-fixture runs from warming `.rigor/cache`.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rigor/analysis/runner.rb', line 55

def initialize(configuration:, explain: false,
               cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
               plugin_requirer: nil, workers: 0, collect_stats: true)
  @configuration = configuration
  @explain = explain
  @cache_store = cache_store
  @plugin_requirer = plugin_requirer
  @workers = workers
  @collect_stats = collect_stats
  @plugin_registry = Plugin::Registry::EMPTY
  @dependency_source_index = DependencySourceInference::Index::EMPTY
  @rbs_extended_reporter = RbsExtended::Reporter.new
  @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
end

Instance Attribute Details

#boundary_cross_reporterObject (readonly)

Returns the value of attribute boundary_cross_reporter.



29
30
31
# File 'lib/rigor/analysis/runner.rb', line 29

def boundary_cross_reporter
  @boundary_cross_reporter
end

#cache_storeObject (readonly)

Returns the value of attribute cache_store.



29
30
31
# File 'lib/rigor/analysis/runner.rb', line 29

def cache_store
  @cache_store
end

#dependency_source_indexObject (readonly)

Returns the value of attribute dependency_source_index.



29
30
31
# File 'lib/rigor/analysis/runner.rb', line 29

def dependency_source_index
  @dependency_source_index
end

#plugin_registryObject (readonly)

Returns the value of attribute plugin_registry.



29
30
31
# File 'lib/rigor/analysis/runner.rb', line 29

def plugin_registry
  @plugin_registry
end

#rbs_extended_reporterObject (readonly)

Returns the value of attribute rbs_extended_reporter.



29
30
31
# File 'lib/rigor/analysis/runner.rb', line 29

def rbs_extended_reporter
  @rbs_extended_reporter
end

Instance Method Details

#analyze_files(files) ⇒ Object

ADR-15 Phase 4b — routes per-file analysis to either the sequential coordinator-side Environment (legacy path, default) or a Ractor worker pool built around WorkerSession (opt-in via ‘workers:`). The sequential path is bit-for-bit unchanged from v0.1.4 / earlier; the pool path is the substrate exercised by phase 4c when `RIGOR_RACTOR_WORKERS` / `.rigor.yml` `parallel.workers:` is wired.

Sequential mode also snapshots ‘class_decl_paths` from the local environment after the per-file loop completes so `RunStats` can attribute the RBS class universe between project-sig and bundled sources. The env stays a LOCAL variable (not an ivar) so it goes GC-eligible when the method returns — holding it as long-lived state added memory pressure that surfaced as a Bus Error during the spec suite under Ruby 4.0 + rbs 4.0.2.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/rigor/analysis/runner.rb', line 130

def analyze_files(files)
  return [] if files.empty?

  if pool_mode?
    analyze_files_in_pool(files)
  else
    environment = build_runner_environment
    result = files.flat_map { |path| analyze_file(path, environment) }
    if @collect_stats
      loader = environment.rbs_loader
      @class_decl_paths_snapshot = loader&.class_decl_paths || {}.freeze
      @signature_paths_snapshot = loader&.signature_paths || [].freeze
    end
    result
  end
end

#pre_file_diagnostics(expansion) ⇒ Object

Pre-file diagnostic streams that fire once per run rather than per analyzed file: plugin load / prepare envelopes, the ADR-10 dependency-source resolution surface, and the ‘expand_paths` errors for `paths:` entries that don’t exist or aren’t ‘.rb`. Aggregated here so `#run` stays under the ABC budget.

ADR-15 Phase 4b — ‘plugin_prepare_diagnostics` runs on the coordinator’s plugin registry under sequential mode; under pool mode each worker re-runs ‘prepare` against its own plugin instances, so the pool path drains the first worker’s prepare-diagnostic snapshot into the aggregated diagnostic stream instead (see #analyze_files_in_pool). Skipping the coordinator prepare in pool mode avoids double-running ‘#prepare` against the coordinator-side plugin instances (which the pool path never consults for per-file analysis).



164
165
166
167
168
169
170
171
172
173
# File 'lib/rigor/analysis/runner.rb', line 164

def pre_file_diagnostics(expansion)
  prepare = pool_mode? ? [] : plugin_prepare_diagnostics
  plugin_load_diagnostics +
    prepare +
    dependency_source_diagnostics +
    dependency_source_budget_diagnostics +
    dependency_source_config_conflict_diagnostics +
    rbs_coverage_diagnostics +
    expansion.fetch(:errors)
end

#run(paths = @configuration.paths) ⇒ Object

Walks every Ruby file under ‘paths`, parses it, builds a per-node scope index through `Rigor::Inference::ScopeIndexer`, and runs the `Rigor::Analysis::CheckRules` catalogue over it. Returns a `Rigor::Analysis::Result` aggregating every produced diagnostic plus any Prism parse errors. The Environment is built once at run start through `Environment.for_project` so all files share the same RBS load.



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
# File 'lib/rigor/analysis/runner.rb', line 78

def run(paths = @configuration.paths)
  Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
    @configuration.fold_platform_specific_paths

  wall_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  target_ruby_error = validate_target_ruby
  return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error

  @plugin_registry = load_plugins
  @dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
  expansion = expand_paths(paths)
  @class_decl_paths_snapshot = {}.freeze
  @signature_paths_snapshot = []
  # ADR-16 slice 2b — Tier C pre-pass. Built once per run
  # against the resolved file set + the loaded plugin
  # registry's `heredoc_templates` so synthetic methods are
  # visible cross-file when per-file inference dispatches.
  @synthetic_method_index = Inference::SyntheticMethodScanner.scan(
    plugin_registry: @plugin_registry,
    paths: expansion.fetch(:files),
    environment: nil
  )

  diagnostics = pre_file_diagnostics(expansion)
  diagnostics += analyze_files(expansion.fetch(:files))
  diagnostics += rbs_extended_reporter_diagnostics
  diagnostics += boundary_cross_diagnostics

  Result.new(
    diagnostics: apply_severity_profile(diagnostics),
    stats: @collect_stats ? build_run_stats(wall_started_at: wall_started_at, expansion: expansion) : nil
  )
end

#validate_target_rubyObject

‘target_ruby` flows through to Prism’s ‘version:` option. Prism enforces the supported range and raises `ArgumentError` for versions it does not recognise. Run a one-time smoke parse here so a misconfigured target_ruby surfaces as a single project-level diagnostic instead of crashing the whole run on the first file.



181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/rigor/analysis/runner.rb', line 181

def validate_target_ruby
  Prism.parse("nil", version: @configuration.target_ruby)
  nil
rescue ArgumentError => e
  Diagnostic.new(
    path: ".rigor.yml", line: 1, column: 1,
    message: "target_ruby #{@configuration.target_ruby.inspect} is not accepted by Prism: #{e.message}",
    severity: :error,
    rule: "configuration-error",
    source_family: :builtin
  )
end