#[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};
#[cfg(feature = "svg")]
use crate::palette::Theme;
use crate::{CategorySet, StdMode, parse_selector, select::Selection};
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
";
const HELP_TEMPLATE: &str = "\
{before-help}{name} {version}
{about-with-newline}
{usage-heading} {usage}
{all-args}{after-help}";
#[derive(Debug, Parser)]
#[command(name = "panicgraph", version, about, long_about = None)]
#[command(after_help = SELECTOR_HELP, help_template = HELP_TEMPLATE)]
#[command(disable_version_flag = true, propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(
short = 'v',
short_alias = 'V',
long,
action = clap::ArgAction::Version,
global = true
)]
pub version: (),
#[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,
#[arg(long, global = true, conflicts_with = "format")]
pub json: bool,
#[cfg(feature = "svg")]
#[arg(long, global = true, conflicts_with_all = ["format", "json"])]
pub svg: bool,
#[cfg(feature = "svg")]
#[arg(long, value_enum, value_name = "NAME", global = true)]
pub theme: Option<Theme>,
#[cfg(feature = "svg")]
#[arg(long, global = true, conflicts_with = "theme")]
pub dark: bool,
}
#[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, value_name = "LIST", global = true)]
pub features: Option<String>,
#[arg(long, global = true)]
pub all_features: bool,
#[arg(long, global = true)]
pub no_default_features: bool,
#[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, PartialEq, Eq, Default)]
pub struct Features {
pub named: Vec<String>,
pub all: bool,
pub no_default: bool,
}
impl Features {
fn new(list: Option<&str>, all: bool, no_default: bool) -> Self {
let mut named: Vec<String> = list
.unwrap_or_default()
.split([',', ' '])
.filter(|name| !name.is_empty())
.map(str::to_owned)
.collect();
named.sort();
named.dedup();
Self {
named,
all,
no_default,
}
}
#[must_use]
pub const fn is_default(&self) -> bool {
self.named.is_empty() && !self.all && !self.no_default
}
#[must_use]
pub fn describe(&self) -> String {
let mut parts: Vec<&str> = Vec::new();
if self.all {
parts.push("all");
}
if self.no_default {
parts.push("no-default");
}
parts.extend(self.named.iter().map(String::as_str));
if parts.is_empty() {
return "default".to_owned();
}
parts.join("+")
}
}
#[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 features: Features,
pub with_tests: bool,
pub generics: Generics,
pub format: Format,
#[cfg(feature = "svg")]
pub theme: Theme,
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 Args {
#[must_use]
pub const fn selection(&self) -> Selection {
Selection {
all_crates: self.all_crates,
closures: self.closures,
generics: self.generics,
only: self.only,
}
}
}
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 format = self.rendering();
#[cfg(feature = "svg")]
let theme = self.colours(format)?;
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()
);
}
#[cfg(feature = "svg")]
if format == Format::Svg && !matches!(command, Command::Analyze) {
bail!(
"`--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,
features: Features::new(
self.scope.features.as_deref(),
self.scope.all_features,
self.scope.no_default_features,
),
with_tests: self.scope.with_tests,
generics: self.policy.generics,
format,
#[cfg(feature = "svg")]
theme,
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,
})
}
}
impl Cli {
const fn rendering(&self) -> Format {
#[cfg(feature = "svg")]
if self.svg {
return Format::Svg;
}
if self.json { Format::Json } else { self.format }
}
#[cfg(feature = "svg")]
fn colours(&self, format: Format) -> Result<Theme> {
let asked = if self.dark {
Some("--dark")
} else if self.theme.is_some() {
Some("--theme")
} else {
None
};
if let Some(flag) = asked
&& format != Format::Svg
{
bail!("`{flag}` colours the flame graph, so it goes with `--svg`");
}
Ok(if self.dark {
Theme::Dark
} else {
self.theme.unwrap_or_default()
})
}
}
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"
),
}
}