#[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`, `assumed`, `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>,
#[arg(long, value_name = "N", value_parser = clap::value_parser!(u8).range(0..=4), global = true)]
pub mir_opt_level: Option<u8>,
#[arg(long, global = true)]
pub with_tests: bool,
}
#[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,
#[arg(long, value_enum, default_value = "written", global = true)]
pub generics: Generics,
}
#[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 Generics {
Written,
Instantiated,
}
impl Generics {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Written => "written",
Self::Instantiated => "instantiated",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Closures {
Separate,
Parent,
}
impl Command {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Analyze => "analyze",
Self::Why { .. } => "why",
Self::Check(_) => "check",
Self::Baseline { .. } => "baseline",
Self::Kinds => "kinds",
}
}
}
impl Closures {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Separate => "separate",
Self::Parent => "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 mir_opt_level: Option<u8>,
pub with_tests: bool,
pub generics: Generics,
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 = self.listen.as_deref().map(listen_addr).transpose()?;
let only = self.policy.only.as_deref().map(selector).transpose()?;
let command = self.command.unwrap_or(Command::Analyze);
#[cfg(feature = "serve")]
if listen.is_some() && !matches!(command, Command::Analyze) {
bail!(
"`--listen` serves the graph, so it belongs to the analysis \
rather than to `{}`",
command.name()
);
}
if self.format == Format::Svg && !matches!(command, Command::Analyze) {
bail!(
"`--format svg` draws the whole graph, so it belongs \
to the analysis rather than to `{}`",
command.name()
);
}
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,
mir_opt_level: self.scope.mir_opt_level,
with_tests: self.scope.with_tests,
generics: self.policy.generics,
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"
),
}
}