Class: Rigor::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/configuration.rb,
lib/rigor/configuration/dependencies.rb,
lib/rigor/configuration/severity_profile.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Modules: SeverityProfile Classes: Dependencies

Constant Summary collapse

DISCOVERY_ORDER =

File-discovery order for ‘Configuration.load(nil)`.

The first file present is loaded; the others are NOT implicitly merged. To extend a base config explicitly the winning file MUST list the base via ‘includes:`.

‘.rigor.yml` is a developer-local override (typically gitignored); `.rigor.dist.yml` is the project default (committed to the repo). When both are present the developer’s local override wins outright — there is no implicit auto-merge.

%w[.rigor.yml .rigor.dist.yml].freeze
DEFAULT_PATH =

Back-compat alias. Keep here so external callers that read ‘Configuration::DEFAULT_PATH` for help text / fixture paths still work; the discovery list is the canonical source.

DISCOVERY_ORDER.first
BUILTIN_EXCLUDES =

Built-in exclusion patterns appended to ‘exclude:` so vendored dependencies, Bundler artefacts, and JavaScript node_modules are never analysed by accident when a directory glob expands. Users cannot disable these defaults; the trade-off is that analysing any of these paths is essentially never what the user wants (they’re build outputs / external dependencies, not source).

We deliberately keep this list narrow. ‘tmp/` and similar directories vary across project layouts (Rails has `tmp/`, libraries usually don’t); user-supplied ‘exclude:` entries in `.rigor.yml` cover the project-specific cases.

%w[
  **/vendor/bundle/**
  **/.bundle/**
  **/node_modules/**
].freeze
DEFAULTS =
{
  "target_ruby" => "4.0",
  "paths" => ["lib"],
  "exclude" => [],
  "plugins" => [],
  "disable" => [],
  "libraries" => [],
  "signature_paths" => nil,
  "fold_platform_specific_paths" => false,
  "cache" => {
    "path" => ".rigor/cache"
  },
  "plugins_io" => {
    "network" => "disabled",
    "allowed_paths" => [],
    "allowed_url_hosts" => []
  },
  "severity_profile" => "balanced",
  "severity_overrides" => {},
  "dependencies" => {
    "source_inference" => [],
    "budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
  },
  "parallel" => {
    # ADR-15 Phase 4c — when greater than zero, `rigor check`
    # dispatches per-file analysis across N Ractor workers
    # built around {Rigor::Analysis::WorkerSession}.
    # `0` (default) keeps the sequential coordinator path
    # bit-for-bit unchanged. The CLI's `--workers=N` flag
    # and the `RIGOR_RACTOR_WORKERS` env var both override
    # this setting; precedence is CLI > env > config > 0.
    "workers" => 0
  },
  "bundler" => {
    # Open item O4 — target-project Bundler awareness.
    # When `bundle_path:` is set (or auto-detected), Rigor
    # walks `<bundle_path>/ruby/*/gems/*/sig/` and adds each
    # gem-shipped sig directory to `signature_paths:`. With
    # O7's failure-memo in place, conflicts (a vendored sig
    # already declares the same constant) degrade gracefully
    # to "no RBS env" with a single-line warning naming the
    # offending file, rather than hanging.
    #
    # `bundle_path:` (String, optional): explicit path to the
    # bundler install root (e.g., "vendor/bundle" or an
    # absolute path). Resolved relative to the project root
    # (`paths:`'s base) when relative.
    #
    # `auto_detect:` (Boolean, default true): when no
    # explicit `bundle_path:` is set, try `.bundle/config`'s
    # `BUNDLE_PATH:` first; fall back to `vendor/bundle/`
    # under the project root if it exists. When neither is
    # found, no extra sigs are added — the analyzer sees
    # only rigor's vendored RBS and the user's
    # `signature_paths:`.
    #
    # O4 Layer 3 keys:
    #
    # `lockfile:` (String, optional): explicit path to a
    # `Gemfile.lock`. Resolved relative to the project root
    # when relative. When set (or auto-detected via the
    # `auto_detect:` flag below) Rigor parses the lockfile
    # and uses it to FILTER the bundle-discovered `sig/`
    # directories: only gems whose `(name, version,
    # platform)` matches a lockfile entry are admitted to
    # `signature_paths:`. Stale or out-of-band gems sitting
    # in the bundle install tree are silently dropped.
    #
    # `auto_detect:` (Boolean, also gates the lockfile
    # search): when true and `lockfile:` is nil, look for
    # `<project_root>/Gemfile.lock`.
    "bundle_path" => nil,
    "auto_detect" => true,
    "lockfile" => nil
  },
  "rbs_collection" => {
    # Open item O4 Layer 3 slice 2 — `rbs collection
    # install` awareness. When the target project has been
    # set up with `rbs collection install`, the resulting
    # `rbs_collection.lock.yaml` carries the resolved (gem,
    # version, source) triples and `.gem_rbs_collection/`
    # holds the downloaded `.rbs` files. Rigor parses the
    # lockfile and auto-feeds each gem's
    # `<collection_root>/<name>/<version>/` directory into
    # `RbsLoader`'s `signature_paths:`. Sources of type
    # `stdlib` are skipped because rigor's bundled
    # `DEFAULT_LIBRARIES` already covers that surface.
    #
    # `lockfile:` (String, optional): explicit path to
    # `rbs_collection.lock.yaml`. Resolved relative to the
    # project root when relative.
    #
    # `auto_detect:` (Boolean, default true): when no
    # explicit `lockfile:` is set, look for
    # `<project_root>/rbs_collection.lock.yaml`.
    "lockfile" => nil,
    "auto_detect" => true
  }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data = DEFAULTS) ⇒ Configuration

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity



295
296
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/rigor/configuration.rb', line 295

def initialize(data = DEFAULTS)
  cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))

  @target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
  @paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
  user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
  @exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
  @plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
    coerce_plugin_entry(entry)
  end.freeze
  @disabled_rules = Array(data.fetch("disable", DEFAULTS.fetch("disable"))).map(&:to_s).freeze
  @libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
  sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
  @signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
  @fold_platform_specific_paths = data.fetch(
    "fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
  ) == true
  @cache_path = cache.fetch("path").to_s
  @plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
  @plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
  @plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
  @severity_profile = coerce_severity_profile(
    data.fetch("severity_profile", DEFAULTS.fetch("severity_profile"))
  )
  @severity_overrides = coerce_severity_overrides(
    data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
  )
  @dependencies = Dependencies.from_h(
    data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
  )
  parallel = DEFAULTS.fetch("parallel").merge(data.fetch("parallel", {}))
  @parallel_workers = coerce_parallel_workers(parallel.fetch("workers"))
  bundler = DEFAULTS.fetch("bundler").merge(data.fetch("bundler", {}))
  bp = bundler.fetch("bundle_path")
  @bundler_bundle_path = bp.nil? ? nil : bp.to_s.dup.freeze
  @bundler_auto_detect = bundler.fetch("auto_detect") == true
  lf = bundler.fetch("lockfile")
  @bundler_lockfile = lf.nil? ? nil : lf.to_s.dup.freeze
  rbs_collection = DEFAULTS.fetch("rbs_collection").merge(data.fetch("rbs_collection", {}))
  rclf = rbs_collection.fetch("lockfile")
  @rbs_collection_lockfile = rclf.nil? ? nil : rclf.to_s.dup.freeze
  @rbs_collection_auto_detect = rbs_collection.fetch("auto_detect") == true
  # Ractor migration Phase 2a: deep-freeze the
  # Configuration so it is `Ractor.shareable?`. Every
  # ivar above is now either a frozen value (Symbol /
  # nil / Boolean) or an explicitly frozen
  # collection / value object; freezing `self` makes the
  # whole carrier safe to send across Ractor boundaries
  # (and catches accidental post-init mutation in any
  # caller). See
  # `docs/design/20260514-ractor-migration.md`.
  freeze
end

Instance Attribute Details

#bundler_auto_detectObject (readonly)

Returns the value of attribute bundler_auto_detect.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def bundler_auto_detect
  @bundler_auto_detect
end

#bundler_bundle_pathObject (readonly)

Returns the value of attribute bundler_bundle_path.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def bundler_bundle_path
  @bundler_bundle_path
end

#bundler_lockfileObject (readonly)

Returns the value of attribute bundler_lockfile.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def bundler_lockfile
  @bundler_lockfile
end

#cache_pathObject (readonly)

Returns the value of attribute cache_path.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def cache_path
  @cache_path
end

#dependenciesObject (readonly)

Returns the value of attribute dependencies.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def dependencies
  @dependencies
end

#disabled_rulesObject (readonly)

Returns the value of attribute disabled_rules.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def disabled_rules
  @disabled_rules
end

#exclude_patternsObject (readonly)

Returns the value of attribute exclude_patterns.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def exclude_patterns
  @exclude_patterns
end

#fold_platform_specific_pathsObject (readonly)

Returns the value of attribute fold_platform_specific_paths.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def fold_platform_specific_paths
  @fold_platform_specific_paths
end

#librariesObject (readonly)

Returns the value of attribute libraries.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def libraries
  @libraries
end

#parallel_workersObject (readonly)

Returns the value of attribute parallel_workers.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def parallel_workers
  @parallel_workers
end

#pathsObject (readonly)

Returns the value of attribute paths.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def paths
  @paths
end

#pluginsObject (readonly)

Returns the value of attribute plugins.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def plugins
  @plugins
end

#plugins_io_allowed_pathsObject (readonly)

Returns the value of attribute plugins_io_allowed_paths.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def plugins_io_allowed_paths
  @plugins_io_allowed_paths
end

#plugins_io_allowed_url_hostsObject (readonly)

Returns the value of attribute plugins_io_allowed_url_hosts.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def plugins_io_allowed_url_hosts
  @plugins_io_allowed_url_hosts
end

#plugins_io_networkObject (readonly)

Returns the value of attribute plugins_io_network.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def plugins_io_network
  @plugins_io_network
end

#rbs_collection_auto_detectObject (readonly)

Returns the value of attribute rbs_collection_auto_detect.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def rbs_collection_auto_detect
  @rbs_collection_auto_detect
end

#rbs_collection_lockfileObject (readonly)

Returns the value of attribute rbs_collection_lockfile.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def rbs_collection_lockfile
  @rbs_collection_lockfile
end

#severity_overridesObject (readonly)

Returns the value of attribute severity_overrides.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def severity_overrides
  @severity_overrides
end

#severity_profileObject (readonly)

Returns the value of attribute severity_profile.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def severity_profile
  @severity_profile
end

#signature_pathsObject (readonly)

Returns the value of attribute signature_paths.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def signature_paths
  @signature_paths
end

#target_rubyObject (readonly)

Returns the value of attribute target_ruby.



151
152
153
# File 'lib/rigor/configuration.rb', line 151

def target_ruby
  @target_ruby
end

Class Method Details

.discoverObject

Returns the path to the config file Rigor would load under auto-discovery, or ‘nil` when neither candidate exists. Public so the CLI / spec drift checks can introspect the resolved file.



187
188
189
# File 'lib/rigor/configuration.rb', line 187

def self.discover
  DISCOVERY_ORDER.find { |candidate| File.exist?(candidate) }
end

.load(path = nil) ⇒ Object

Loads a configuration file.

‘path == nil` triggers auto-discovery against DISCOVERY_ORDER. The first present file in that list is loaded; if none exist the built-in DEFAULTS are used.

When a path is supplied (whether by auto-discovery or by the caller) the YAML body is processed for ‘includes:` recursively, and every relative path inside path-bearing keys (`paths:`, `signature_paths:`, `plugins_io.allowed_paths:`, `includes:`) is resolved against THAT file’s directory. The resolution is per-file: an included file’s relative paths resolve against the included file’s directory, not the top-level file. Path resolution mirrors [PHPStan](phpstan.org/config-reference#paths).



175
176
177
178
179
180
181
# File 'lib/rigor/configuration.rb', line 175

def self.load(path = nil)
  resolved = path || discover
  return new(DEFAULTS) if resolved.nil? || !File.exist?(resolved)

  data = load_with_includes(resolved)
  new(DEFAULTS.merge(data))
end

.resolve_path_key!(out, key, base_dir) ⇒ Object



240
241
242
243
244
# File 'lib/rigor/configuration.rb', line 240

def self.resolve_path_key!(out, key, base_dir)
  return unless out.key?(key) && !out[key].nil?

  out[key] = Array(out[key]).map { |p| File.expand_path(p.to_s, base_dir) }
end

.resolve_plugins_io_paths!(out, base_dir) ⇒ Object



246
247
248
249
250
251
252
253
# File 'lib/rigor/configuration.rb', line 246

def self.resolve_plugins_io_paths!(out, base_dir)
  plugins_io = out["plugins_io"]
  return unless plugins_io.is_a?(Hash) && plugins_io["allowed_paths"]

  duped = plugins_io.dup
  duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| File.expand_path(p.to_s, base_dir) }
  out["plugins_io"] = duped
end

Instance Method Details

#to_hObject

rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/rigor/configuration.rb', line 351

def to_h # rubocop:disable Metrics/MethodLength
  {
    "target_ruby" => target_ruby,
    "paths" => paths,
    "exclude" => exclude_patterns - BUILTIN_EXCLUDES,
    "plugins" => plugins,
    "disable" => disabled_rules,
    "libraries" => libraries,
    "signature_paths" => signature_paths,
    "fold_platform_specific_paths" => fold_platform_specific_paths,
    "cache" => {
      "path" => cache_path
    },
    "plugins_io" => {
      "network" => plugins_io_network.to_s,
      "allowed_paths" => plugins_io_allowed_paths,
      "allowed_url_hosts" => plugins_io_allowed_url_hosts
    },
    "severity_profile" => severity_profile.to_s,
    "severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
    "dependencies" => dependencies.to_h,
    "parallel" => {
      "workers" => parallel_workers
    },
    "bundler" => {
      "bundle_path" => bundler_bundle_path,
      "auto_detect" => bundler_auto_detect,
      "lockfile" => bundler_lockfile
    },
    "rbs_collection" => {
      "lockfile" => rbs_collection_lockfile,
      "auto_detect" => rbs_collection_auto_detect
    }
  }
end