stackwise 0.1.0

Drop-in Rust stack usage analysis with JSON reports and an interactive local UI
Documentation
use camino::Utf8PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use stackwise_core::{BuildInfo, ExactMode};

#[derive(Debug, Parser)]
#[command(name = "stackwise")]
#[command(about = "Drop-in Rust stack analyzer for emitted artifacts")]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    #[arg(long)]
    pub release: bool,

    #[arg(long)]
    pub profile: Option<String>,

    #[arg(long)]
    pub target: Option<String>,

    #[arg(long)]
    pub features: Vec<String>,

    #[arg(long)]
    pub all_features: bool,

    #[arg(long)]
    pub no_default_features: bool,

    #[arg(long)]
    pub package: Option<String>,

    #[arg(long)]
    pub bin: Option<String>,

    #[arg(long)]
    pub example: Option<String>,

    #[arg(long)]
    pub workspace: bool,

    #[arg(long)]
    pub open: bool,

    #[arg(long, conflicts_with = "open")]
    pub serve: bool,

    #[arg(long)]
    pub json: Option<Utf8PathBuf>,

    #[arg(long)]
    pub no_build: bool,

    #[arg(
        long,
        value_enum,
        default_value_t = ExactModeArg::Auto,
        default_missing_value = "required",
        num_args = 0..=1,
        require_equals = false
    )]
    pub exact: ExactModeArg,
}

#[derive(Debug, Subcommand)]
pub enum Commands {
    Analyze(AnalyzeCommand),
    Open(OpenCommand),
    Check(CheckCommand),
    Doctor,
    Schema(SchemaCommand),
    Init,
}

#[derive(Debug, Args)]
pub struct AnalyzeCommand {
    pub artifact: Utf8PathBuf,

    #[arg(long)]
    pub json: Option<Utf8PathBuf>,

    #[arg(long)]
    pub workspace_root: Option<Utf8PathBuf>,

    #[arg(long)]
    pub profile: Option<String>,

    #[arg(long)]
    pub target: Option<String>,
}

impl AnalyzeCommand {
    pub fn build_info(&self) -> Option<BuildInfo> {
        if self.workspace_root.is_none() && self.profile.is_none() && self.target.is_none() {
            return None;
        }

        Some(BuildInfo {
            workspace_root: self.workspace_root.as_ref().map(ToString::to_string),
            package: None,
            profile: self.profile.clone(),
            target: self.target.clone(),
            features: Vec::new(),
            exact_mode: ExactMode::Off,
        })
    }
}

#[derive(Debug, Args)]
pub struct OpenCommand {
    pub report: Utf8PathBuf,

    #[arg(long)]
    pub serve: bool,
}

#[derive(Debug, Args)]
pub struct CheckCommand {
    pub report: Utf8PathBuf,

    #[arg(long)]
    pub max_own_frame: Option<u64>,

    #[arg(long = "max-measured-path", alias = "max-known-path")]
    pub max_measured_path: Option<u64>,

    #[arg(long = "fail-on-unmeasured", alias = "fail-on-unknown")]
    pub fail_on_unmeasured: bool,
}

#[derive(Debug, Args)]
pub struct SchemaCommand {
    #[arg(long)]
    pub json: bool,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ExactModeArg {
    Off,
    Auto,
    Required,
}

#[cfg(test)]
mod tests {
    use super::{Cli, Commands};
    use clap::Parser;

    #[test]
    fn cargo_style_serve_mode_parses_without_opening() {
        let cli = Cli::try_parse_from(["stackwise", "--release", "--serve"]).unwrap();

        assert!(cli.release);
        assert!(cli.serve);
        assert!(!cli.open);
    }

    #[test]
    fn cargo_style_serve_conflicts_with_open() {
        assert!(Cli::try_parse_from(["stackwise", "--open", "--serve"]).is_err());
    }

    #[test]
    fn open_command_serve_mode_parses_without_opening() {
        let cli = Cli::try_parse_from(["stackwise", "open", "report.json", "--serve"]).unwrap();
        let Some(Commands::Open(command)) = cli.command else {
            panic!("expected open command");
        };

        assert_eq!(command.report.as_str(), "report.json");
        assert!(command.serve);
    }
}