ExQuality.Config (ExQuality v0.13.0)

View Source

Loads and merges configuration from multiple sources.

Configuration is resolved in the following order (later wins):

  1. Defaults
  2. Auto-detected tool availability
  3. Project config file (.quality.exs, read from the project root)
  4. The selected profile, if --profile named one
  5. CLI arguments

Example .quality.exs

[
  # Global options
  quick: false,

  # Override auto-detection: force disable dialyzer
  dialyzer: [enabled: false],

  # Credo options (enabled: :auto by default)
  credo: [
    strict: true,
    all: false
  ],

  # Doctor options
  doctor: [
    summary_only: true
  ],

  # Named bundles, selected with `mix quality --profile loop`
  profiles: [
    loop: [stages: [:format, :compile, :credo], test: [scope: :changed]],
    gate: []
  ]
]

Configuration Options

Global Options

  • quick - Quick mode: skip dialyzer and coverage enforcement (default: false)
  • profiles - Named option bundles, see "Profiles" below (default: [])

Stage Options

Each stage supports:

  • enabled - :auto (use auto-detection) | true (force enable) | false (force disable)

Stage-specific options:

  • compile.warnings_as_errors - Treat warnings as errors (default: true)
  • compile.force - Recompile from scratch (default: false)
  • credo.strict - Use strict mode (default: true)
  • credo.all - Check all files (default: false)
  • credo.configs - Names of the .credo.exs configs to run, in order (default: nil, meaning one run with no --config-name)
  • dependencies.check_unused - Check for unused dependencies (default: true)
  • dependencies.audit - Run security audit if available (default: :auto)
  • doctor.summary_only - Show only summary (default: false)
  • gettext.source_locale - The locale the source is written in, whose .po files are not checked (default: "en")
  • gettext.exclude - Basenames to skip (default: ["errors.po"])
  • gettext.extract - Run mix gettext.extract --merge first, which writes to the repository and recompiles the project (default: false)
  • sobelow.exit - Confidence level that blocks, when .sobelow-conf sets no exit: of its own (default: "medium")
  • sobelow.show_informational - Render findings below that level as well as counting them (default: false)
  • test.coverage - :auto (measure when the project's config asks for it) | true (always measure) | false (never measure) (default: :auto)
  • test.scope - :all (the whole suite):changed (only the test files
    covering changed code)a glob string (default: :all)
  • test.base_ref - What scope: :changed is measured against (default: the repository's default branch)

Profiles

A profile is a named bundle of the options above, so the fast path a project wants its agents to use has a name its docs can point at:

profiles: [
  loop: [stages: [:format, :compile, :credo], test: [scope: :changed]],
  gate: []
]

mix quality --profile loop merges the profile over the config file and under the CLI, so a switch still wins over the profile that a run selected.

stages: is the allow-list of stage keys for the profile. Every other stage, built-in or custom, is reported as skipped naming the profile. A profile with no stages: key narrows nothing and only carries options, which is what an empty gate: [] is for.

An invocation with no --profile behaves exactly as it does without any profiles configured. An unknown profile name fails the run: falling back to "run everything" would turn a typo into a slow green, and falling back to the profile's intent would turn one into a fast green over nothing.

Summary

Functions

Merges the named profile into a config, or returns it unchanged for nil.

Returns the path of the .quality.exs that applies here, or nil.

Returns the .quality.exs under project_root, or under umbrella_root when the project has none of its own, or nil when neither has one.

Loads configuration with auto-detection and overrides.

Returns the name of the profile a loaded config was resolved with, or nil.

Returns why a stage will not run, or nil when it will.

Determines if a stage should run based on config.

Functions

apply_profile(config, name)

@spec apply_profile(
  keyword(),
  String.t() | nil
) :: keyword()

Merges the named profile into a config, or returns it unchanged for nil.

Called by load/1 between the config file and the CLI. name is a string because it comes from a switch, and the profile keys it is matched against are atoms because they come from a config file.

iex> ExQuality.Config.apply_profile([credo: [strict: true]], nil)
[credo: [strict: true]]

iex> config = [profiles: [loop: [test: [scope: :changed]]]]
iex> config |> ExQuality.Config.apply_profile("loop") |> Keyword.take([:profile, :test])
[profile: :loop, test: [scope: :changed]]

config_path()

@spec config_path() :: String.t() | nil

Returns the path of the .quality.exs that applies here, or nil.

The file belongs to the project, not to wherever the shell happened to be when mix quality was run. An umbrella child with no file of its own falls back to the umbrella root's, because the settings describe the tree.

Finding the umbrella root needs Mix.Project.parent_umbrella_project_file/0, which arrived in Elixir 1.15. On 1.14 only the current project's root is looked at.

config_path(project_root, umbrella_root)

@spec config_path(String.t() | nil, String.t() | nil) :: String.t() | nil

Returns the .quality.exs under project_root, or under umbrella_root when the project has none of its own, or nil when neither has one.

Either root may be nil, meaning there is no such directory to look in.

load(cli_opts \\ [])

@spec load(keyword()) :: keyword()

Loads configuration with auto-detection and overrides.

Resolution order (later wins):

  1. Defaults
  2. Auto-detected tool availability
  3. .quality.exs file
  4. The profile named by --profile, if any
  5. CLI arguments

Examples

# Load with CLI options
config = ExQuality.Config.load(quick: true, skip_dialyzer: true)

# Load with defaults only
config = ExQuality.Config.load()

profile(config)

@spec profile(keyword()) :: atom() | nil

Returns the name of the profile a loaded config was resolved with, or nil.

iex> ExQuality.Config.profile(ExQuality.Config.load())
nil

skip_reason(config, stage)

@spec skip_reason(
  keyword(),
  atom()
) :: String.t() | nil

Returns why a stage will not run, or nil when it will.

The reason is meant to be shown to the reader, because a stage that is silently omitted reads as a stage that passed.

Examples

config = ExQuality.Config.load(skip_credo: true)
ExQuality.Config.skip_reason(config, :credo)
#=> "--skip-credo"

config = ExQuality.Config.load()
ExQuality.Config.skip_reason(config, :doctor)
#=> ":doctor not installed"

stage_enabled?(config, stage)

@spec stage_enabled?(
  keyword(),
  atom()
) :: boolean()

Determines if a stage should run based on config.

  • enabled: :auto → use detected availability
  • enabled: true → force enable (will error if tool missing)
  • enabled: false → force disable

Examples

config = ExQuality.Config.load()
ExQuality.Config.stage_enabled?(config, :credo)
#=> true (if credo is installed)

config = ExQuality.Config.load(skip_credo: true)
ExQuality.Config.stage_enabled?(config, :credo)
#=> false