cargo-mend 0.7.0

Opinionated visibility auditing for Rust crates and workspaces
#![feature(rustc_private)]

extern crate rustc_driver;
extern crate rustc_hir;
extern crate rustc_interface;
extern crate rustc_middle;
extern crate rustc_span;

mod cargo_json;
mod cli;
mod compiler;
mod config;
mod constants;
mod diagnostics;
mod fix_support;
mod imports;
mod inline_path_qualified_type;
mod module_paths;
mod narrow_pub_crate;
mod outcome;
mod prefer_module_import;
mod pub_use_fixes;
mod render;
mod run_mode;
mod runner;
mod selection;

use std::io::IsTerminal;
use std::process::ExitCode;
use std::time::Instant;

use anyhow::Result;
use config::DiagnosticsConfig;
use constants::EXIT_CODE_ERROR;
use constants::EXIT_CODE_WARNING;
use outcome::MendFailure;
use render::OutputFormat;
use run_mode::OperationMode;
use runner::MendRunner;

fn main() -> ExitCode {
    if std::env::var_os("MEND_DRIVER").is_some() {
        return compiler::driver_main();
    }

    match run() {
        Ok(code) => code,
        Err(err) => {
            eprintln!("mend: {err}");
            MendFailure::exit_code()
        },
    }
}

fn build_diagnostics_help(diagnostics: &DiagnosticsConfig) -> String {
    let config_path = config::global_config_path()
        .map_or_else(|| "(unavailable)".to_string(), |p| p.display().to_string());

    let mut lines = vec![String::new(), "Diagnostics:".to_string()];
    for (code, enabled) in diagnostics.entries() {
        let name = code.as_str();
        let status = if enabled { "enabled" } else { "disabled" };
        lines.push(format!("  {name:<40} {status}"));
    }
    lines.push(String::new());
    lines.push(format!("Config: {config_path}"));
    lines.join("\n")
}

fn build_info_text() -> String {
    let version = env!("CARGO_PKG_VERSION");
    let git_hash = option_env!("MEND_GIT_HASH").unwrap_or("unknown");
    let build_id = option_env!("MEND_BUILD_ID").unwrap_or("unknown");
    let build_sysroot = option_env!("MEND_BUILD_SYSROOT").unwrap_or("unknown");
    format!(
        "cargo-mend {version}\n\
         git_hash: {git_hash}\n\
         build_id: {build_id}\n\
         build_sysroot: {build_sysroot}"
    )
}

fn run() -> Result<ExitCode, MendFailure> {
    let global_diagnostics = config::load_global_diagnostics();
    let after_help = build_diagnostics_help(&global_diagnostics);
    let cli = cli::parse(&after_help);
    if cli.build_info {
        println!("{}", build_info_text());
        return Ok(ExitCode::SUCCESS);
    }
    let selection = selection::resolve_cargo_selection(cli.cargo.explicit_manifest_path())
        .map_err(MendFailure::Unexpected)?;
    let cargo_plan = selection::build_cargo_check_plan(&selection, &cli.cargo);
    let config = config::load_config(
        selection.manifest_dir.as_path(),
        selection.workspace_root.as_path(),
        cli.manifest.config.as_deref(),
        &global_diagnostics,
    )
    .map_err(MendFailure::Unexpected)?;
    let operation_mode = OperationMode::from_cli(&cli.fix);
    let color = color_mode();
    let output = if cli.json {
        OutputFormat::Json
    } else {
        OutputFormat::Human
    };
    let start = Instant::now();
    let outcome =
        MendRunner::new(&selection, &cargo_plan, &config, color, output).run(operation_mode)?;

    let fix_compiler_duration = if cli.fix.fix_compiler() || cli.fix.fix_all() {
        Some(compiler::run_cargo_fix(&cargo_plan, color).map_err(MendFailure::Unexpected)?)
    } else {
        None
    };

    let total_duration = start.elapsed();
    let check_duration = outcome.check_duration + fix_compiler_duration.unwrap_or_default();
    let mend_duration = total_duration.saturating_sub(check_duration);

    let compiler_stats = render::CompilerStats {
        warning_count: outcome.compiler_warning_count,
        fixable_count: outcome.compiler_fixable_count,
    };

    match output {
        OutputFormat::Json => {
            print!(
                "{}",
                cargo_json::render_report(&outcome.report, &selection)
                    .map_err(MendFailure::Unexpected)?
            );
        },
        OutputFormat::Human => {
            print!(
                "{}",
                render::render_human_report(&outcome.report, &compiler_stats, color)
            );
        },
    }

    if output == OutputFormat::Human {
        eprintln!(
            "{}",
            render::render_timing(total_duration, check_duration, mend_duration, color)
        );
    }

    if let Some(notice) = outcome.notice {
        eprintln!("{}", notice.render());
    }

    if outcome.report.has_errors() {
        return Ok(ExitCode::from(EXIT_CODE_ERROR));
    }

    if cli.fail_on_warn && outcome.report.has_warnings() {
        return Ok(ExitCode::from(EXIT_CODE_WARNING));
    }

    Ok(ExitCode::SUCCESS)
}

fn color_mode() -> render::ColorMode {
    if let Ok(choice) = std::env::var("CLICOLOR_FORCE")
        && choice != "0"
    {
        return render::ColorMode::Enabled;
    }

    if let Ok(choice) = std::env::var("CARGO_TERM_COLOR") {
        match choice.to_ascii_lowercase().as_str() {
            "never" => return render::ColorMode::Disabled,
            "always" => return render::ColorMode::Enabled,
            _ => {},
        }
    }

    if let Ok(choice) = std::env::var("CLICOLOR")
        && choice == "0"
    {
        return render::ColorMode::Disabled;
    }

    if std::io::stdout().is_terminal() || std::io::stderr().is_terminal() {
        return render::ColorMode::Enabled;
    }

    if std::env::var("TERM").is_ok_and(|term| term != "dumb") {
        render::ColorMode::Enabled
    } else {
        render::ColorMode::Disabled
    }
}

#[cfg(test)]
#[allow(
    clippy::used_underscore_binding,
    reason = "RAII guards use _ prefix but are held for Drop"
)]
mod tests {
    use std::ffi::OsString;

    use super::build_info_text;
    use super::color_mode;
    use super::render::ColorMode;

    struct EnvGuard {
        key:      &'static str,
        previous: Option<OsString>,
    }

    impl EnvGuard {
        fn set(key: &'static str, value: &'static str) -> Self {
            let previous = std::env::var_os(key);
            unsafe { std::env::set_var(key, value) };
            Self { key, previous }
        }

        fn remove(key: &'static str) -> Self {
            let previous = std::env::var_os(key);
            unsafe { std::env::remove_var(key) };
            Self { key, previous }
        }
    }

    impl Drop for EnvGuard {
        fn drop(&mut self) {
            if let Some(previous) = &self.previous {
                unsafe { std::env::set_var(self.key, previous) };
            } else {
                unsafe { std::env::remove_var(self.key) };
            }
        }
    }

    #[test]
    fn cargo_term_color_never_disables_color() {
        let _guard = EnvGuard::set("CARGO_TERM_COLOR", "never");
        assert!(matches!(color_mode(), ColorMode::Disabled));
    }

    #[test]
    fn cargo_term_color_always_enables_color() {
        let _guard = EnvGuard::set("CARGO_TERM_COLOR", "always");
        assert!(matches!(color_mode(), ColorMode::Enabled));
    }

    #[test]
    fn clicolor_zero_disables_color() {
        let _guard = EnvGuard::set("CLICOLOR", "0");
        let _cargo_term_color = EnvGuard::remove("CARGO_TERM_COLOR");
        assert!(matches!(color_mode(), ColorMode::Disabled));
    }

    #[test]
    fn term_enables_color_when_terminal_detection_is_unavailable() {
        let _cargo_term_color = EnvGuard::remove("CARGO_TERM_COLOR");
        let _clicolor = EnvGuard::remove("CLICOLOR");
        let _clicolor_force = EnvGuard::remove("CLICOLOR_FORCE");
        let _term = EnvGuard::set("TERM", "xterm-256color");
        assert!(matches!(color_mode(), ColorMode::Enabled));
    }

    #[test]
    fn build_info_contains_expected_fields() {
        let build_info = build_info_text();

        assert!(build_info.starts_with(&format!("cargo-mend {}", env!("CARGO_PKG_VERSION"))));
        assert!(build_info.contains("\ngit_hash: "));
        assert!(build_info.contains("\nbuild_id: "));
        assert!(build_info.contains("\nbuild_sysroot: "));
    }
}