Class: Rigor::Analysis::Runner
- Inherits:
-
Object
- Object
- Rigor::Analysis::Runner
- 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
-
#boundary_cross_reporter ⇒ Object
readonly
Returns the value of attribute boundary_cross_reporter.
-
#buffer ⇒ Object
readonly
ADR-pending editor mode — present when the runner is wired for the ‘–tmp-file` / `–instead-of` buffer-binding shape (`docs/design/20260516-editor-mode.md`).
-
#cache_store ⇒ Object
readonly
Returns the value of attribute cache_store.
-
#dependency_source_index ⇒ Object
readonly
Returns the value of attribute dependency_source_index.
-
#plugin_registry ⇒ Object
readonly
Returns the value of attribute plugin_registry.
-
#rbs_extended_reporter ⇒ Object
readonly
Returns the value of attribute rbs_extended_reporter.
Instance Method Summary collapse
-
#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:`).
- #diagnostic_from_hash(hash) ⇒ Object
-
#initialize(configuration:, explain: false, cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT), plugin_requirer: nil, workers: 0, collect_stats: true, buffer: nil, prebuilt: nil, environment: nil) ⇒ Runner
constructor
A new instance of Runner.
-
#pre_eval_diagnostics ⇒ Object
ADR-17 slice 1 — surface a ‘:error` diagnostic for each `pre_eval:` entry whose resolved path doesn’t exist on disk.
-
#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`.
-
#prepare_project_scan(paths: @configuration.paths) ⇒ Object
Runs every project-wide pre-pass (‘load_plugins` + `plugin#prepare` + dependency-source builder + synthetic-method scanner + project-patched scanner) exactly once, then returns a frozen ProjectScan snapshot.
-
#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.
-
#shared_fact_store ⇒ Object
Returns the per-run shared ‘Plugin::FactStore` instance.
-
#validate_target_ruby ⇒ Object
‘target_ruby` flows through to Prism’s ‘version:` option.
Constructor Details
#initialize(configuration:, explain: false, cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT), plugin_requirer: nil, workers: 0, collect_stats: true, buffer: nil, prebuilt: nil, environment: nil) ⇒ Runner
Returns a new instance of Runner.
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 83 def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT), plugin_requirer: nil, workers: 0, collect_stats: true, buffer: nil, prebuilt: nil, environment: nil) @configuration = configuration @explain = explain @cache_store = enforce_read_only_cache(cache_store, buffer) @plugin_requirer = plugin_requirer @workers = workers @collect_stats = collect_stats @buffer = buffer @prebuilt = prebuilt @environment_override = environment @plugin_registry = Plugin::Registry::EMPTY @dependency_source_index = DependencySourceInference::Index::EMPTY @rbs_extended_reporter = RbsExtended::Reporter.new @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new # `#run` resets these for each invocation; pre-seed them to # empty containers so `build_run_stats` / `pre_file_diagnostics` # (private, called only from `#run`) can read them without # nil-guards. @class_decl_paths_snapshot = {}.freeze @signature_paths_snapshot = [].freeze @cached_plugin_prepare_diagnostics = [].freeze @project_discovered_classes = {}.freeze @project_discovered_def_nodes = {}.freeze @project_discovered_superclasses = {}.freeze @project_discovered_includes = {}.freeze end |
Instance Attribute Details
#boundary_cross_reporter ⇒ Object (readonly)
Returns the value of attribute boundary_cross_reporter.
33 34 35 |
# File 'lib/rigor/analysis/runner.rb', line 33 def boundary_cross_reporter @boundary_cross_reporter end |
#buffer ⇒ Object (readonly)
ADR-pending editor mode — present when the runner is wired for the ‘–tmp-file` / `–instead-of` buffer-binding shape (`docs/design/20260516-editor-mode.md`). Nil for normal project runs.
117 118 119 |
# File 'lib/rigor/analysis/runner.rb', line 117 def buffer @buffer end |
#cache_store ⇒ Object (readonly)
Returns the value of attribute cache_store.
33 34 35 |
# File 'lib/rigor/analysis/runner.rb', line 33 def cache_store @cache_store end |
#dependency_source_index ⇒ Object (readonly)
Returns the value of attribute dependency_source_index.
33 34 35 |
# File 'lib/rigor/analysis/runner.rb', line 33 def dependency_source_index @dependency_source_index end |
#plugin_registry ⇒ Object (readonly)
Returns the value of attribute plugin_registry.
33 34 35 |
# File 'lib/rigor/analysis/runner.rb', line 33 def plugin_registry @plugin_registry end |
#rbs_extended_reporter ⇒ Object (readonly)
Returns the value of attribute rbs_extended_reporter.
33 34 35 |
# File 'lib/rigor/analysis/runner.rb', line 33 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.
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/rigor/analysis/runner.rb', line 291 def analyze_files(files) return [] if files.empty? if pool_mode? dispatch_pool(files) else environment = resolve_sequential_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 |
#diagnostic_from_hash(hash) ⇒ Object
398 399 400 401 402 403 404 |
# File 'lib/rigor/analysis/runner.rb', line 398 def diagnostic_from_hash(hash) Diagnostic.new( path: hash.fetch(:path), line: hash.fetch(:line), column: hash.fetch(:column), message: hash.fetch(:message), severity: hash.fetch(:severity), rule: hash.fetch(:rule), source_family: :builtin ) end |
#pre_eval_diagnostics ⇒ Object
ADR-17 slice 1 — surface a ‘:error` diagnostic for each `pre_eval:` entry whose resolved path doesn’t exist on disk. Loud failure mode (‘:error`, not `:warning`): a missing pre_eval path is a configuration mistake the user must fix before analysis is meaningful.
Slice 2 adds the ‘:warning` `pre-eval.parse-error` stream from the pre-pass scanner — accumulated as `@pre_eval_diagnostics_from_scanner` during #run and merged here so both diagnostics flow through the same severity / ordering pipeline.
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 |
# File 'lib/rigor/analysis/runner.rb', line 381 def pre_eval_diagnostics not_found = @configuration.pre_eval.filter_map do |path| next if File.file?(path) Diagnostic.new( path: ".rigor.yml", line: 1, column: 1, message: "pre_eval entry not found: #{path.inspect}. " \ "Pre-evaluation requires the file to exist on disk; remove the entry " \ "or create the file before re-running analysis.", severity: :error, rule: "pre-eval.file-not-found", source_family: :builtin ) end not_found + Array(@pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) } 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).
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/rigor/analysis/runner.rb', line 342 def pre_file_diagnostics(expansion) # ADR-18 slice 3 — prepare diagnostics are captured # earlier in #run (before the synthetic-method scanner) # so cross-plugin facts are available to the scanner. # We re-surface the captured diagnostics here so the # existing pre_file_diagnostics ordering is preserved. prepare = pool_mode? ? [] : @cached_plugin_prepare_diagnostics plugin_load_diagnostics + prepare + pre_eval_diagnostics + dependency_source_diagnostics + dependency_source_budget_diagnostics + dependency_source_config_conflict_diagnostics + rbs_coverage_diagnostics + expansion.fetch(:errors) end |
#prepare_project_scan(paths: @configuration.paths) ⇒ Object
Runs every project-wide pre-pass (‘load_plugins` + `plugin#prepare` + dependency-source builder + synthetic-method scanner + project-patched scanner) exactly once, then returns a frozen ProjectScan snapshot.
Long-lived integrations (‘Rigor::LanguageServer::ProjectContext`) call this once per project-state generation and feed the snapshot back into `Runner.new(prebuilt: scan)` for every subsequent per-buffer publish. The cold pre-pass cost is paid once per generation rather than once per keystroke.
Notes for callers:
-
The runner this method is called on may be a “build only” instance — ‘@buffer` is typically nil so the scanners observe on-disk bytes for the full project. Callers that want pre-passes to see a particular buffer’s edits should build the runner WITH ‘buffer:` set.
-
The returned ProjectScan is frozen and shareable; the underlying ‘plugin_registry` is the same object that ran `#prepare`, so the per-plugin `services.fact_store` is already populated for subsequent dispatch use.
179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/rigor/analysis/runner.rb', line 179 def prepare_project_scan(paths: @configuration.paths) expansion = (paths) run_project_pre_passes(expansion: expansion) ProjectScan.new( plugin_registry: @plugin_registry, dependency_source_index: @dependency_source_index, synthetic_method_index: @synthetic_method_index, project_patched_methods: @project_patched_methods, plugin_prepare_diagnostics: @cached_plugin_prepare_diagnostics.dup.freeze, pre_eval_diagnostics: @pre_eval_diagnostics_from_scanner.dup.freeze ) 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.
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 |
# File 'lib/rigor/analysis/runner.rb', line 127 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 expansion = (paths) @class_decl_paths_snapshot = {}.freeze @signature_paths_snapshot = [] if @prebuilt adopt_prebuilt_project_scan(@prebuilt) else run_project_pre_passes(expansion: expansion) end diagnostics = pre_file_diagnostics(expansion) diagnostics += analyze_files(target_files(expansion)) 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 |
#shared_fact_store ⇒ Object
Returns the per-run shared ‘Plugin::FactStore` instance. All loaded plugins share this store through their respective `Plugin::Services` (the same instance is threaded by `Plugin::Loader.load`). Returns `nil` when no plugins are loaded.
364 365 366 367 368 |
# File 'lib/rigor/analysis/runner.rb', line 364 def shared_fact_store return nil if @plugin_registry.nil? || @plugin_registry.empty? @plugin_registry.plugins.first&.services&.fact_store end |
#validate_target_ruby ⇒ Object
‘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.
412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/rigor/analysis/runner.rb', line 412 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.}", severity: :error, rule: "configuration-error", source_family: :builtin ) end |