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
-
#analyzed_files ⇒ Object
readonly
Returns the value of attribute analyzed_files.
-
#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.
-
#file_dependencies ⇒ Object
readonly
Returns the value of attribute file_dependencies.
-
#plugin_registry ⇒ Object
readonly
Returns the value of attribute plugin_registry.
-
#rbs_extended_reporter ⇒ Object
readonly
Returns the value of attribute rbs_extended_reporter.
-
#unresolved_self_calls ⇒ Object
readonly
Returns the value of attribute unresolved_self_calls.
Instance Method Summary collapse
-
#analysis_file_set(paths = @configuration.paths) ⇒ Object
ADR-46 — the project file set that a run over ‘paths` would analyze, computed by globbing only (no RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path before deciding whether to build the env at all.
-
#analyze_files(files, environment: nil) ⇒ 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:`).
- #analyze_files_sequentially(files, environment) ⇒ Object
- #analyzed_file_entries(expansion) ⇒ Object
- #assemble_run_diagnostics(expansion, environment: nil) ⇒ Object
-
#class_declarations ⇒ Object
ADR-46 slice 3 — per-file set of the qualified class/module names declared in that file.
-
#compute_run_diagnostics(expansion) ⇒ Object
ADR-45 — unchanged-project fast path.
- #config_hash_entry(key, payload) ⇒ Object
- #diagnostic_from_hash(hash) ⇒ Object
-
#file_dependents ⇒ Object
ADR-46 §2 — inverts #file_dependencies into the reverse edge the incremental step walks: ‘dependents = { A : A read a declaration / body from X }`.
-
#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, record_dependencies: false, record_self_calls: false, analyze_only: 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.
-
#run_dependency_descriptor(expansion, rbs_descriptor) ⇒ Object
Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS ‘sig` file (`rbs_descriptor.files`), and every file each plugin read (complete post-run, so reads made mid-analysis are included).
-
#run_key_descriptor(expansion, rbs_descriptor) ⇒ Object
Stable cache key inputs — known before the run: a digest of the resolved configuration, the engine + rbs versions + ‘–explain`, and the analyzed-path SET (adding/removing a file changes the key; editing one is caught by dependency validation).
-
#run_result_cacheable? ⇒ Boolean
Cacheable only for a full sequential project run with a writable cache and no per-buffer / prebuilt override — every other mode has a different result identity (pool workers read in separate processes; editor mode is per-buffer; prebuilt is the LSP path).
-
#run_source(source:, path: "(source).rb") ⇒ Result
Analyze a single source String in memory, without writing it to disk — a clean entry point for embedders (LSP / editor mode) and a faster spec path than the per-call tmpdir + chdir.
-
#shared_fact_store ⇒ Object
Returns the per-run shared ‘Plugin::FactStore` instance.
-
#stats_for_run(wall_started_at:, expansion:) ⇒ Object
A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never gathered — report none rather than the stale snapshot defaults.
-
#symbol_fingerprints ⇒ Object
ADR-46 slice 4 — per-symbol body fingerprints, computed from the project pre-pass def index.
-
#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, record_dependencies: false, record_self_calls: false, analyze_only: nil) ⇒ Runner
Returns a new instance of Runner.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/rigor/analysis/runner.rb', line 93 def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT), plugin_requirer: nil, workers: 0, collect_stats: true, buffer: nil, prebuilt: nil, environment: nil, record_dependencies: false, record_self_calls: false, analyze_only: 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 # ADR-46 slice 1 — opt-in cross-file dependency recording. Off by # default; when true, `analyze_file` records each file's # cross-file reads into `file_dependencies` (the incremental # cache, a later slice, consumes them). @record_dependencies = record_dependencies # ADR-24 slice 4a — opt-in unresolved-implicit-self-call recording. # Off by default; when true, `analyze_file` activates the engine # choke-point recorder and collects each file's misses into # `unresolved_self_calls` (a later closed-class-gated rule consumes # them). Purely observational — diagnostics are byte-identical. @record_self_calls = record_self_calls @unresolved_self_calls = {} # Memoised activation decision for the `call.self-undefined-method` # rule (nil = not yet computed). See `self_undefined_rule_active?`. @self_undefined_rule_active = nil @analyzed_files = [].freeze # In-memory source map for `#run_source` — `{ logical_path => source # String }`. When set, `parse_source` reads bytes from here instead # of disk and `expand_paths` accepts the (possibly non-existent) # logical path. nil on a normal disk-backed run. @in_memory_sources = nil # ADR-46 slice 2 — the subset-analysis hook. When set (a collection # of paths), the whole-project pre-pass still runs over every file # (so the cross-file index is complete), but only files in this set # are analyzed for diagnostics — the body tier re-analyses the # affected closure and serves the rest from the per-file cache. # `nil` (the default) analyzes everything. @analyze_only = analyze_only && Set.new(analyze_only) @file_dependencies = {} @plugin_registry = Plugin::Registry::EMPTY @dependency_source_index = DependencySourceInference::Index::EMPTY @rbs_extended_reporter = RbsExtended::Reporter.new @boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new @source_rbs_synthesis_reporter = Plugin::SourceRbsSynthesisReporter.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. Kept inline (not a helper) so the engine's own # flow analysis sees the ivars established in the constructor. @class_decl_paths_snapshot = {}.freeze @signature_paths_snapshot = [].freeze @synthesized_namespaces_snapshot = [].freeze # `rigor:v1:conforms-to` results, snapshotted from the # per-run RBS env in `analyze_files_sequentially` (gated on # the project declaring `signature_paths:`) and drained by # `conforms_to_diagnostics`. Inline default per the comment # above so the engine's own flow analysis sees it seeded. @conformance_results_snapshot = [].freeze @cached_plugin_prepare_diagnostics = [].freeze @project_discovered_classes = {}.freeze @project_discovered_def_nodes = {}.freeze @project_discovered_def_sources = {}.freeze @project_discovered_superclasses = {}.freeze @project_discovered_includes = {}.freeze @project_discovered_class_sources = {}.freeze @project_discovered_method_visibilities = {}.freeze @project_discovered_methods = {}.freeze @project_data_member_layouts = {}.freeze end |
Instance Attribute Details
#analyzed_files ⇒ Object (readonly)
Returns the value of attribute analyzed_files.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def analyzed_files @analyzed_files end |
#boundary_cross_reporter ⇒ Object (readonly)
Returns the value of attribute boundary_cross_reporter.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 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.
171 172 173 |
# File 'lib/rigor/analysis/runner.rb', line 171 def buffer @buffer end |
#cache_store ⇒ Object (readonly)
Returns the value of attribute cache_store.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def cache_store @cache_store end |
#dependency_source_index ⇒ Object (readonly)
Returns the value of attribute dependency_source_index.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def dependency_source_index @dependency_source_index end |
#file_dependencies ⇒ Object (readonly)
Returns the value of attribute file_dependencies.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def file_dependencies @file_dependencies end |
#plugin_registry ⇒ Object (readonly)
Returns the value of attribute plugin_registry.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def plugin_registry @plugin_registry end |
#rbs_extended_reporter ⇒ Object (readonly)
Returns the value of attribute rbs_extended_reporter.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def rbs_extended_reporter @rbs_extended_reporter end |
#unresolved_self_calls ⇒ Object (readonly)
Returns the value of attribute unresolved_self_calls.
42 43 44 |
# File 'lib/rigor/analysis/runner.rb', line 42 def unresolved_self_calls @unresolved_self_calls end |
Instance Method Details
#analysis_file_set(paths = @configuration.paths) ⇒ Object
ADR-46 — the project file set that a run over ‘paths` would analyze, computed by globbing only (no RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path before deciding whether to build the env at all.
234 235 236 |
# File 'lib/rigor/analysis/runner.rb', line 234 def analysis_file_set(paths = @configuration.paths) (paths).fetch(:files) end |
#analyze_files(files, environment: nil) ⇒ 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.
547 548 549 550 551 552 |
# File 'lib/rigor/analysis/runner.rb', line 547 def analyze_files(files, environment: nil) return [] if files.empty? return dispatch_pool(files) if pool_mode? analyze_files_sequentially(files, environment || resolve_sequential_environment(source_files: files)) end |
#analyze_files_sequentially(files, environment) ⇒ Object
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 |
# File 'lib/rigor/analysis/runner.rb', line 554 def analyze_files_sequentially(files, environment) # Snapshot the small synthesized-namespace name list (NOT the # env — see the method comment) so #run can surface the # malformed-RBS `:info` diagnostic without rebuilding the env. # Gated on the project actually declaring `signature_paths:`: # synthesis only matters for the project's own RBS, and # `#synthesized_namespaces` forces the (otherwise-lazy) RBS env # to build — doing so when there is no project sig set would # warm `.rigor/cache` on a bare `--no-stats` run. @synthesized_namespaces_snapshot = project_signature_paths? ? (environment.rbs_loader&.synthesized_namespaces || []) : [] # `rigor:v1:conforms-to` lives only in the project's own # `signature_paths:` RBS, so gate the scan the same way and # reuse the already-built env (no extra RBS load). @conformance_results_snapshot = project_signature_paths? ? RbsExtended::ConformanceChecker.scan(environment.rbs_loader) : [] 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 |
#analyzed_file_entries(expansion) ⇒ Object
395 396 397 398 399 400 401 402 |
# File 'lib/rigor/analysis/runner.rb', line 395 def analyzed_file_entries(expansion) expansion.fetch(:files).map do |path| physical = @buffer ? @buffer.resolve(path) : path Cache::Descriptor::FileEntry.new( path: physical, comparator: :digest, value: Digest::SHA256.file(physical).hexdigest ) end end |
#assemble_run_diagnostics(expansion, environment: nil) ⇒ Object
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/rigor/analysis/runner.rb', line 323 def assemble_run_diagnostics(expansion, environment: nil) diagnostics = pre_file_diagnostics(expansion) # ADR-46 — record which project files this run actually analyzed # (the `analyze_only` subset, or all of them). The incremental # orchestrator serves every analyzed-but-not-affected file from the # per-file cache, so it needs the full analyzed set to subtract the # affected closure from. targets = target_files(expansion) @analyzed_files = targets diagnostics += analyze_files(targets, environment: environment) diagnostics += rbs_synthesized_namespace_diagnostics diagnostics += conforms_to_diagnostics diagnostics += rbs_extended_reporter_diagnostics diagnostics += boundary_cross_diagnostics diagnostics + source_rbs_synthesis_diagnostics end |
#class_declarations ⇒ Object
ADR-46 slice 3 — per-file set of the qualified class/module names declared in that file. Used to detect a class that appeared in an edit so a subclass whose ancestor was previously undefined (and so recorded a negative class edge) is re-checked. Inverts the project class-source attribution (class → declaring files).
281 282 283 284 285 286 287 |
# File 'lib/rigor/analysis/runner.rb', line 281 def class_declarations result = Hash.new { |hash, key| hash[key] = Set.new } @project_discovered_class_sources.each do |class_name, files| files.each { |file| result[file] << class_name } end result.transform_values(&:freeze).freeze end |
#compute_run_diagnostics(expansion) ⇒ Object
ADR-45 — unchanged-project fast path. Serves the whole run’s (pre-severity-profile) diagnostics from one record-and-validate cache entry when every file the previous run read is unchanged, skipping the dominant per-file inference. The dependency set is collected AFTER the run (so it captures files the plugins read mid-analysis, e.g. a Pundit policy) and re-validated on the next run; the entry is keyed on the inputs known up front (config, gem / engine versions, analyzed-path set).
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/rigor/analysis/runner.rb', line 297 def compute_run_diagnostics(expansion) @run_served_from_cache = false return assemble_run_diagnostics(expansion) unless run_result_cacheable? environment = resolve_sequential_environment(source_files: target_files(expansion)) rbs_descriptor = environment&.rbs_loader ? Cache::RbsDescriptor.build(environment.rbs_loader) : Cache::Descriptor.new key_descriptor = run_key_descriptor(expansion, rbs_descriptor) return assemble_run_diagnostics(expansion, environment: environment) if key_descriptor.nil? computed = false diagnostics = @cache_store.fetch_or_validate( producer_id: "analysis.run-diagnostics", key_descriptor: key_descriptor ) do computed = true diags = assemble_run_diagnostics(expansion, environment: environment) [diags, run_dependency_descriptor(expansion, rbs_descriptor)] end @run_served_from_cache = !computed diagnostics rescue StandardError # The result cache must never break a run. If anything in the # cache path fails, fall back to a direct, uncached analysis. @run_served_from_cache = false assemble_run_diagnostics(expansion) end |
#config_hash_entry(key, payload) ⇒ Object
404 405 406 |
# File 'lib/rigor/analysis/runner.rb', line 404 def config_hash_entry(key, payload) Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload)) end |
#diagnostic_from_hash(hash) ⇒ Object
669 670 671 672 673 674 675 |
# File 'lib/rigor/analysis/runner.rb', line 669 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 |
#file_dependents ⇒ Object
ADR-46 §2 — inverts #file_dependencies into the reverse edge the incremental step walks: ‘dependents = { A : A read a declaration / body from X }`. On an edit to X, the body tier (slice 2) re-analyses `X ∪ dependents` and serves every other file from the per-file cache. Built on demand from the recorded `sources` sets (so it reflects whatever `analyze_file` captured —empty unless the runner was constructed with `record_dependencies: true`). The negative (`missing`) edges are NOT inverted here: they feed the structural tier (slice 3), which re-checks a consumer when a name it looked up and did not resolve later appears.
249 250 251 |
# File 'lib/rigor/analysis/runner.rb', line 249 def file_dependents Incremental.invert(@file_dependencies.transform_values(&:sources)) 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.
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 |
# File 'lib/rigor/analysis/runner.rb', line 652 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).
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 |
# File 'lib/rigor/analysis/runner.rb', line 613 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.
430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/rigor/analysis/runner.rb', line 430 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.
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/rigor/analysis/runner.rb', line 181 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 = [] @synthesized_namespaces_snapshot = [] @conformance_results_snapshot = [] if @prebuilt adopt_prebuilt_project_scan(@prebuilt) else run_project_pre_passes(expansion: expansion) end diagnostics = compute_run_diagnostics(expansion) Result.new( diagnostics: apply_severity_profile(diagnostics), stats: stats_for_run(wall_started_at: wall_started_at, expansion: expansion) ) end |
#run_dependency_descriptor(expansion, rbs_descriptor) ⇒ Object
Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS ‘sig` file (`rbs_descriptor.files`), and every file each plugin read (complete post-run, so reads made mid-analysis are included). Re-digested on the next run by Descriptor#fresh?.
382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/rigor/analysis/runner.rb', line 382 def run_dependency_descriptor(expansion, rbs_descriptor) entries = analyzed_file_entries(expansion) + rbs_descriptor.files @plugin_registry.plugins.each do |plugin| # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` # initializer: plugin instances are frozen after the run, and a # plugin that never built a boundary read no files through it, so # it contributes no dependencies. boundary = plugin.instance_variable_get(:@io_boundary) entries.concat(boundary.cache_descriptor.files) if boundary end Cache::Descriptor.new(files: entries) end |
#run_key_descriptor(expansion, rbs_descriptor) ⇒ Object
Stable cache key inputs — known before the run: a digest of the resolved configuration, the engine + rbs versions + ‘–explain`, and the analyzed-path SET (adding/removing a file changes the key; editing one is caught by dependency validation). nil disables the cache for this run rather than risking a malformed key.
364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/rigor/analysis/runner.rb', line 364 def run_key_descriptor(expansion, rbs_descriptor) Cache::Descriptor.new( gems: rbs_descriptor.gems, configs: rbs_descriptor.configs + [ config_hash_entry("configuration", Marshal.dump(@configuration.to_h)), config_hash_entry("engine", "#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{@explain}"), config_hash_entry("paths", expansion.fetch(:files).sort.join("\n")) ] ) rescue StandardError nil end |
#run_result_cacheable? ⇒ Boolean
Cacheable only for a full sequential project run with a writable cache and no per-buffer / prebuilt override — every other mode has a different result identity (pool workers read in separate processes; editor mode is per-buffer; prebuilt is the LSP path).
354 355 356 357 |
# File 'lib/rigor/analysis/runner.rb', line 354 def run_result_cacheable? !@cache_store.nil? && !@cache_store.read_only? && @buffer.nil? && @prebuilt.nil? && !pool_mode? end |
#run_source(source:, path: "(source).rb") ⇒ Result
Analyze a single source String in memory, without writing it to disk — a clean entry point for embedders (LSP / editor mode) and a faster spec path than the per-call tmpdir + chdir. The source is bound to ‘path` (purely a logical identity carried in diagnostic locations; it need not exist on disk). The full run machinery still runs — environment build, plugin `prepare`, severity profile — so the result matches a one-file disk run; only the cross-file project pre-pass is empty (there is one file, and the per-file indexer self-discovers its own classes / defs).
223 224 225 226 227 228 |
# File 'lib/rigor/analysis/runner.rb', line 223 def run_source(source:, path: "(source).rb") @in_memory_sources = { path => source } run([path]) ensure @in_memory_sources = 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.
635 636 637 638 639 |
# File 'lib/rigor/analysis/runner.rb', line 635 def shared_fact_store return nil if @plugin_registry.nil? || @plugin_registry.empty? @plugin_registry.plugins.first&.services&.fact_store end |
#stats_for_run(wall_started_at:, expansion:) ⇒ Object
A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never gathered — report none rather than the stale snapshot defaults.
343 344 345 346 347 348 |
# File 'lib/rigor/analysis/runner.rb', line 343 def stats_for_run(wall_started_at:, expansion:) return nil unless @collect_stats return nil if @run_served_from_cache build_run_stats(wall_started_at: wall_started_at, expansion: expansion) end |
#symbol_fingerprints ⇒ Object
ADR-46 slice 4 — per-symbol body fingerprints, computed from the project pre-pass def index. Returns a frozen hash of the form:
{ "path/to/file.rb" => { "ClassName#method" => sha256_hex, … }, … }
Used by IncrementalSession to detect which symbols in a changed file actually changed bodies, so only callers of those specific symbols are re-checked. Only meaningful after a run that populated ‘@project_discovered_def_nodes` (i.e. any full or subset analysis); returns an empty frozen hash before the first run.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/rigor/analysis/runner.rb', line 261 def symbol_fingerprints result = Hash.new { |h, k| h[k] = {} } @project_discovered_def_sources.each do |class_name, methods| methods.each do |method_sym, path_line| path = path_line.split(":", 2).first node = @project_discovered_def_nodes.dig(class_name, method_sym) next unless node result[path]["#{class_name}##{method_sym}"] = Digest::SHA256.hexdigest(node.location.slice) end end result.transform_values(&:freeze).freeze 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.
683 684 685 686 687 688 689 690 691 692 693 694 |
# File 'lib/rigor/analysis/runner.rb', line 683 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 |