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.
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 |
# File 'lib/rigor/analysis/runner.rb', line 82 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 end |
Instance Attribute Details
#boundary_cross_reporter ⇒ Object (readonly)
Returns the value of attribute boundary_cross_reporter.
32 33 34 |
# File 'lib/rigor/analysis/runner.rb', line 32 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.
113 114 115 |
# File 'lib/rigor/analysis/runner.rb', line 113 def buffer @buffer end |
#cache_store ⇒ Object (readonly)
Returns the value of attribute cache_store.
32 33 34 |
# File 'lib/rigor/analysis/runner.rb', line 32 def cache_store @cache_store end |
#dependency_source_index ⇒ Object (readonly)
Returns the value of attribute dependency_source_index.
32 33 34 |
# File 'lib/rigor/analysis/runner.rb', line 32 def dependency_source_index @dependency_source_index end |
#plugin_registry ⇒ Object (readonly)
Returns the value of attribute plugin_registry.
32 33 34 |
# File 'lib/rigor/analysis/runner.rb', line 32 def plugin_registry @plugin_registry end |
#rbs_extended_reporter ⇒ Object (readonly)
Returns the value of attribute rbs_extended_reporter.
32 33 34 |
# File 'lib/rigor/analysis/runner.rb', line 32 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.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/rigor/analysis/runner.rb', line 277 def analyze_files(files) return [] if files.empty? if pool_mode? analyze_files_in_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
384 385 386 387 388 389 390 |
# File 'lib/rigor/analysis/runner.rb', line 384 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.
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/rigor/analysis/runner.rb', line 367 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).
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/rigor/analysis/runner.rb', line 328 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.
175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/rigor/analysis/runner.rb', line 175 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.
123 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 |
# File 'lib/rigor/analysis/runner.rb', line 123 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.
350 351 352 353 354 |
# File 'lib/rigor/analysis/runner.rb', line 350 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.
398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/rigor/analysis/runner.rb', line 398 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 |