Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Adding a DoctorCheck

TL;DR: Implement the DoctorCheck trait and register it with DoctorRunner to add structured diagnostics with optional auto-fix to your tool.

Context & Prerequisites

This guide explains how to add diagnostic checks to your tool’s doctor subcommand. Before starting, ensure you have:

Implementing a basic check

Create a struct that implements DoctorCheck:

#![allow(unused)]
fn main() {
use genesis::doctor::{DoctorCheck, DoctorRunner};
use genesis::suite_linter::{LintResult, Severity};
use std::path::Path;

struct ConfigFileCheck;

impl DoctorCheck for ConfigFileCheck {
    fn name(&self) -> &'static str {
        "config-file"
    }

    fn description(&self) -> &'static str {
        "Checks that the tool config file exists and is valid"
    }

    fn run(&self, repo: &Path) -> Result<Vec<LintResult>, Box<dyn std::error::Error>> {
        let config_path = repo.join("my-tool.toml");

        if !config_path.exists() {
            return Ok(vec![LintResult::error(
                "config-file",
                "Config file not found",
                "Run `my-tool init` to create a default config",
            )]);
        }

        Ok(vec![]) // pass — no issues
    }
}
}

Adding auto-fix

Implement fix to provide automatic remediation:

#![allow(unused)]
fn main() {
impl DoctorCheck for ConfigFileCheck {
    // ... name, description, run as above ...

    fn can_fix(&self) -> bool {
        true
    }

    fn fix(&self, repo: &Path) -> Result<Vec<LintResult>, Box<dyn std::error::Error>> {
        let config_path = repo.join("my-tool.toml");

        if config_path.exists() {
            return Ok(vec![]); // already fixed
        }

        std::fs::write(&config_path, "# Default config\nkey = \"value\"\n")?;

        Ok(vec![LintResult::info(
            "config-file",
            "Created default config file",
        )])
    }
}
}

Running checks with DoctorRunner

Register checks and run them:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let runner = DoctorRunner::new(vec![
        Box::new(ConfigFileCheck),
    ]);

    let repo = std::env::current_dir()?;
    let report = runner.run(&repo, false)?; // false = don't fix

    println!("pass={} warn={} fail={}",
        report.summary.pass,
        report.summary.warn,
        report.summary.fail,
    );

    Ok(())
}

Pass true to enable auto-fix:

#![allow(unused)]
fn main() {
let report = runner.run(&repo, true)?; // true = run fixes
}

Integrating with the envelope

DoctorReport is serializable, so you can emit it as a JSON envelope:

#![allow(unused)]
fn main() {
use genesis::doctor::DoctorReport;
use genesis::envelope::Envelope;

let report: DoctorReport = /* ... */;
let envelope = Envelope::from(report);
envelope.print(&mut std::io::stdout())?;
}

Panic isolation

If a DoctorCheck::run() panics, the runner propagates the panic — it does not catch it. Wrap checks in std::panic::catch_unwind if you need panic isolation for individual checks.

Troubleshooting: Common Fail-States

SymptomCauseFix
fix() never calledrun() called with falsePass true as the second argument
LintResult not showing in reportWrong severity levelUse LintResult::error() for failures, LintResult::warn() for warnings, LintResult::info() for informational
DoctorCheck panics on missing repo pathPath doesn’t existCheck repo.exists() before running checks

Further Exploration