Current section
Files
Jump to
Current section
Files
lib/mix/tasks/quality.ex
defmodule Mix.Tasks.Quality do
@shortdoc "Runs parallel code quality checks with auto-fix"
@moduledoc """
Runs code quality checks in parallel with actionable feedback.
Automatically fixes formatting issues, then runs all analysis stages
in parallel with streaming output.
## Execution Phases
1. **Auto-fix** - Runs `mix format` to fix formatting
2. **Compile** - Compiles dev + test environments in parallel
3. **Analysis** - Runs enabled checks in parallel (credo, dialyzer, doctor, tests)
## Usage
mix quality
## Options
- `--quick` - Quick mode for development: skips dialyzer and coverage enforcement
- `--skip-dialyzer` - Skip Dialyzer type checking
- `--skip-credo` - Skip Credo static analysis
- `--skip-doctor` - Skip Doctor documentation checks
- `--skip-gettext` - Skip Gettext translation checks
- `--skip-dependencies` - Skip dependency checks (unused deps and security audit)
- `--verbose` - Show full output even on success
## Passing Test Options
You can pass extra arguments to `mix test` or `mix coveralls` using `--`:
mix quality -- --only integration
mix quality --quick -- --include slow --seed 0
Arguments after `--` are passed directly to the test command.
Alternatively, configure test args in `.quality.exs`:
test: [
args: ["--only", "integration"]
]
CLI args (after `--`) override config file args (no merge).
## Auto-Detection
Stages are automatically enabled based on installed dependencies:
- `:credo` → enables Credo stage
- `:dialyxir` → enables Dialyzer stage
- `:doctor` → enables Doctor stage
- `:gettext` → enables Gettext translation checks
- `:mix_audit` → enables security audit in Dependencies stage
- `:excoveralls` → uses `mix coveralls` instead of `mix test`
## Quick Mode
Use `--quick` during active development when you haven't finished all
implementation tasks (like writing tests). Quick mode:
- Skips Dialyzer (slow)
- Runs `mix test` instead of `mix coveralls` (tests must pass, but
coverage threshold is not enforced)
This lets you iterate quickly while still catching obvious issues.
## Configuration
Create `.quality.exs` in your project root to customize behavior
or override auto-detection. See `Config` for options.
## Example Output
Running quality checks...
✓ Format: No changes needed (0.1s)
✓ Compile: dev + test compiled (warnings as errors) (1.8s)
Running analysis stages in parallel...
✓ Credo: No issues (1.2s)
✓ Tests: 248 passed, 0 failed, 87.3% coverage (5.2s)
✓ Dialyzer: No warnings (32.1s)
✅ All quality checks passed!
"""
use Mix.Task
alias ExQuality.Config
alias ExQuality.Printer
alias ExQuality.Stages.Compile
alias ExQuality.Stages.Credo
alias ExQuality.Stages.Dependencies
alias ExQuality.Stages.Dialyzer
alias ExQuality.Stages.Doctor
alias ExQuality.Stages.Format
alias ExQuality.Stages.Gettext
alias ExQuality.Stages.Test
@switches [
quick: :boolean,
skip_dialyzer: :boolean,
skip_credo: :boolean,
skip_doctor: :boolean,
skip_gettext: :boolean,
skip_dependencies: :boolean,
verbose: :boolean
]
@doc """
Runs the quality check task.
"""
def run(args) do
{opts, remaining} = OptionParser.parse!(args, switches: @switches)
opts = if remaining != [], do: Keyword.put(opts, :test_args, remaining), else: opts
config = Config.load(opts)
Mix.shell().info("Running quality checks...\n")
# Phase 1: Auto-fix (format)
format_result = Format.run(config)
display_phase_result(format_result)
# Phase 2: Compile (blocking gate)
compile_result = Compile.run(config)
display_phase_result(compile_result)
if compile_result.status == :error do
Mix.shell().info("")
display_failure_details([compile_result])
Mix.raise("Compilation failed")
end
# Phase 3: Analysis (parallel with streaming)
Mix.shell().info("\nRunning analysis stages in parallel...\n")
analysis_results = run_analysis_stages(config)
# Show results and check for failures
all_results = [format_result, compile_result | analysis_results]
failures = Enum.filter(all_results, &(&1.status == :error))
if failures != [] do
Mix.shell().info("")
display_failure_details(failures)
Mix.raise("#{length(failures)} quality check(s) failed")
else
Mix.shell().info("\n✅ All quality checks passed!")
end
end
defp run_analysis_stages(config) do
{:ok, _pid} = Printer.start_link()
try do
stages = build_analysis_stages(config)
tasks =
Enum.map(stages, fn {_name, module} ->
Task.async(fn ->
result = module.run(config)
Printer.print_result(result)
result
end)
end)
Enum.map(tasks, &Task.await(&1, :infinity))
after
Printer.stop()
end
end
defp build_analysis_stages(config) do
quick_mode = Keyword.get(config, :quick, false)
stages = []
# Add Credo if enabled
stages =
if Config.stage_enabled?(config, :credo) do
[{:credo, Credo} | stages]
else
stages
end
# Add Dialyzer if enabled and not in quick mode
stages =
if Config.stage_enabled?(config, :dialyzer) and not quick_mode do
[{:dialyzer, Dialyzer} | stages]
else
stages
end
# Add Doctor if enabled
stages =
if Config.stage_enabled?(config, :doctor) do
[{:doctor, Doctor} | stages]
else
stages
end
# Add Gettext if enabled
stages =
if Config.stage_enabled?(config, :gettext) do
[{:gettext, Gettext} | stages]
else
stages
end
# Add Dependencies if enabled
stages =
if Config.stage_enabled?(config, :dependencies) do
[{:dependencies, Dependencies} | stages]
else
stages
end
# Tests always run (but coverage enforcement skipped in quick mode)
[{:test, Test} | stages]
end
@dialyzer {:nowarn_function, display_phase_result: 1}
@spec display_phase_result(ExQuality.Stage.result()) :: :ok
defp display_phase_result(result) do
case result.status do
:ok ->
Mix.shell().info(
"✓ #{result.name}: #{result.summary} (#{format_duration(result.duration_ms)})"
)
:error ->
Mix.shell().error(
"✗ #{result.name}: #{result.summary} (#{format_duration(result.duration_ms)})"
)
:skipped ->
Mix.shell().info("○ #{result.name}: Skipped (#{format_duration(result.duration_ms)})")
end
end
defp display_failure_details(failures) do
Enum.each(failures, fn failure ->
Mix.shell().info(String.duplicate("─", 60))
Mix.shell().error("#{failure.name} - FAILED")
Mix.shell().info(String.duplicate("─", 60))
if failure.output != "" do
Mix.shell().info(failure.output)
end
Mix.shell().info("")
end)
end
defp format_duration(ms) when ms < 1000, do: "#{ms}ms"
defp format_duration(ms), do: "#{Float.round(ms / 1000, 1)}s"
end