#[cfg(feature = "serve")]
use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
#[cfg(feature = "serve")]
use anyhow::Context;
use anyhow::{Result, bail};
use clap::{Args as Group, Parser, Subcommand, ValueEnum};
use crate::{CategorySet, StdMode, parse_selector};
const SELECTOR_HELP: &str = "\
A category LIST is comma separated. It accepts category names plus the group
aliases `oom`, `default`, and `all`. Run `panicgraph kinds` for the names.
Allocation failure, capacity overflow, and standard library precondition
checks are assumed impossible by default: every growable collection reaches
the first two, and the third only exists in a standard library built with
undefined behaviour checks enabled. Pass `--suppress ''` to see everything.
EXIT CODES
0 nothing to report
1 findings, or a failed check
2 the tool could not complete
";
#[derive(Debug, Parser)]
#[command(name = "panicgraph", version, about, long_about = None)]
#[command(after_help = SELECTOR_HELP)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub scope: Scope,
#[command(flatten)]
pub policy: Policy,
#[cfg(feature = "serve")]
#[arg(short = 'l', long, value_name = "PORT|HOST:PORT", global = true)]
pub listen: Option<String>,
#[arg(long, value_enum, default_value_t = Format::Human, global = true)]
pub format: Format,
}
#[derive(Debug, Clone, Group)]
pub struct Scope {
#[arg(long, value_name = "DIR", global = true)]
pub manifest_dir: Option<PathBuf>,
#[arg(short, long, value_name = "PKG", global = true)]
pub package: Option<String>,
#[arg(long, default_value = "release", global = true)]
pub profile: String,
#[arg(long = "std", value_enum, global = true)]
pub std_mode: Option<Std>,
}
#[derive(Debug, Clone, Group)]
#[allow(
clippy::struct_excessive_bools,
reason = "each flag is an independent, orthogonal policy choice"
)]
pub struct Policy {
#[arg(long, value_name = "LIST", default_value = "default", global = true)]
pub suppress: String,
#[arg(long, value_name = "LIST", global = true)]
pub only: Option<String>,
#[arg(long, global = true)]
pub static_only: bool,
#[arg(long, global = true)]
pub candidates: bool,
#[arg(long, global = true)]
pub verify: bool,
#[arg(long, value_enum, default_value = "separate", global = true)]
pub closures: Closures,
#[arg(long, global = true)]
pub all_crates: bool,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
Analyze,
Why {
function: String,
},
Check(Check),
Baseline {
#[arg(value_name = "FILE")]
file: PathBuf,
},
Kinds,
}
#[derive(Debug, Clone, Default, Group)]
pub struct Check {
#[arg(long, value_name = "REGEX")]
pub forbid: Vec<String>,
#[arg(long, value_name = "REGEX")]
pub allow: Vec<String>,
#[arg(long, value_name = "N")]
pub max: Option<usize>,
#[arg(long, value_name = "FILE")]
pub baseline: Option<PathBuf>,
#[arg(long)]
pub fail_on_unknown: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Closures {
Separate,
Parent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
Human,
Json,
Github,
#[cfg(feature = "svg")]
Svg,
}
const fn default_std(command: &Command) -> StdMode {
match command {
Command::Check(_) | Command::Baseline { .. } => StdMode::Full,
_ => StdMode::Shipped,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Std {
Shipped,
Full,
}
impl From<Std> for StdMode {
fn from(value: Std) -> Self {
match value {
Std::Shipped => Self::Shipped,
Std::Full => Self::Full,
}
}
}
#[derive(Debug, Clone)]
#[allow(
clippy::struct_excessive_bools,
reason = "each flag is an independent, orthogonal policy choice"
)]
pub struct Args {
pub command: Command,
pub suppress: CategorySet,
pub only: Option<CategorySet>,
pub profile: String,
pub std_mode: StdMode,
pub format: Format,
pub static_only: bool,
pub candidates: bool,
pub verify: bool,
pub closures: Closures,
pub all_crates: bool,
pub manifest_dir: Option<PathBuf>,
pub package: Option<String>,
#[cfg(feature = "serve")]
pub listen: Option<SocketAddr>,
}
impl Cli {
pub fn resolve(self) -> Result<Args> {
#[cfg(feature = "serve")]
let listen = match self.listen {
Some(text) => Some(listen_addr(&text)?),
None => None,
};
let only = match self.policy.only {
Some(text) => Some(selector(&text)?),
None => None,
};
let command = self.command.unwrap_or(Command::Analyze);
let std_mode = self
.scope
.std_mode
.map_or_else(|| default_std(&command), StdMode::from);
Ok(Args {
command,
suppress: selector(&self.policy.suppress)?,
only,
profile: self.scope.profile,
std_mode,
format: self.format,
static_only: self.policy.static_only,
candidates: self.policy.candidates,
verify: self.policy.verify,
closures: self.policy.closures,
all_crates: self.policy.all_crates,
manifest_dir: self.scope.manifest_dir,
package: self.scope.package,
#[cfg(feature = "serve")]
listen,
})
}
}
pub fn parse<I, S>(input: I) -> Result<Args>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString> + Clone,
{
let mut argv: Vec<std::ffi::OsString> = vec!["panicgraph".into()];
argv.extend(input.into_iter().map(Into::into));
Cli::try_parse_from(argv)?.resolve()
}
#[cfg(feature = "serve")]
fn listen_addr(text: &str) -> Result<SocketAddr> {
if let Ok(port) = text.parse::<u16>() {
return Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port));
}
let mut resolved = text
.to_socket_addrs()
.with_context(|| format!("could not resolve `{text}`"))?;
match resolved.next() {
Some(addr) => Ok(addr),
None => bail!("`{text}` resolved to no address"),
}
}
fn selector(text: &str) -> Result<CategorySet> {
match parse_selector(text) {
Ok(set) => Ok(set),
Err(bad) => bail!(
"unknown panic category `{bad}`; run `panicgraph kinds` for \
the list"
),
}
}