cargo-shear 1.13.0

Detect and fix unused/misplaced dependencies from Cargo.toml
Documentation
use std::{io, io::IsTerminal, str::FromStr};

use crate::{diagnostics::ShearAnalysis, output::miette::MietteRenderer};

pub mod github;
pub mod json;
pub mod miette;

/// Picks the renderer used for diagnostics on stdout.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub enum OutputFormat {
    /// Miette renderer with colors and unicode (resolves to `GitHub` under CI).
    #[default]
    Auto,

    /// Newline-terminated JSON for machine consumers.
    Json,

    /// `::error file=...` workflow commands consumed by GitHub Actions.
    GitHub,
}

impl OutputFormat {
    /// Resolve `Auto` against the environment.
    ///
    /// When running in GitHub Actions (the `GITHUB_ACTIONS` env var is set),
    /// `Auto` switches to `GitHub` so failures show up as PR annotations.
    /// Otherwise it stays as `Auto` (the miette renderer).
    #[must_use]
    pub fn resolve(self) -> Self {
        if matches!(self, Self::Auto) && std::env::var_os("GITHUB_ACTIONS").is_some() {
            Self::GitHub
        } else {
            self
        }
    }
}

impl FromStr for OutputFormat {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "auto" => Ok(Self::Auto),
            "json" => Ok(Self::Json),
            "github" => Ok(Self::GitHub),
            _ => Err(format!("unknown format: {s}, expected: auto, json, github")),
        }
    }
}

/// Whether the terminal renderer should emit ANSI colors.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub enum ColorMode {
    /// Honour `NO_COLOR` and only colourise when stdout is a TTY.
    #[default]
    Auto,

    /// Always emit colors, even when piped.
    Always,

    /// Strip all colors from the output.
    Never,
}

impl ColorMode {
    /// Resolve to a concrete `bool` based on the mode plus the environment.
    #[must_use]
    pub fn enabled(self) -> bool {
        match self {
            Self::Always => true,
            Self::Never => false,
            Self::Auto => {
                if std::env::var_os("NO_COLOR").is_some() {
                    return false;
                }

                std::io::stdout().is_terminal()
            }
        }
    }
}

impl FromStr for ColorMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "auto" => Ok(Self::Auto),
            "always" => Ok(Self::Always),
            "never" => Ok(Self::Never),
            _ => Err(format!("unknown color option: {s}, expected one of: auto, always, never")),
        }
    }
}

pub struct Renderer<W> {
    writer: W,
    format: OutputFormat,
    color: bool,
}

impl<W: io::Write> Renderer<W> {
    pub const fn new(writer: W, format: OutputFormat, color: bool) -> Self {
        Self { writer, format, color }
    }

    pub fn render(&mut self, analysis: &ShearAnalysis) -> io::Result<()> {
        match self.format {
            OutputFormat::Auto => {
                let mut renderer = MietteRenderer::new(&mut self.writer, self.color);
                renderer.render(analysis)
            }
            OutputFormat::Json => {
                let mut renderer = json::JsonRenderer::new(&mut self.writer);
                renderer.render(analysis)
            }
            OutputFormat::GitHub => {
                let mut renderer = github::GitHubRenderer::new(&mut self.writer);
                renderer.render(analysis)
            }
        }
    }
}