extern crate alloc;
use crate::span::Span;
use core::fmt;
use facet_core::{Field, Shape, Type, UserType, Variant};
use facet_reflect::ReflectError;
use heck::ToKebabCase;
#[derive(Debug)]
pub struct ArgsErrorWithInput {
pub(crate) inner: ArgsError,
#[allow(unused)]
pub(crate) flattened_args: String,
}
impl ArgsErrorWithInput {
pub const fn is_help_request(&self) -> bool {
self.inner.kind.is_help_request()
}
pub fn help_text(&self) -> Option<&str> {
self.inner.kind.help_text()
}
}
impl core::fmt::Display for ArgsErrorWithInput {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if let Some(help) = self.help_text() {
return write!(f, "{}", help);
}
write!(f, "error: {}", self.inner.kind.label())?;
if let Some(help) = self.inner.kind.help() {
write!(f, "\n\n{help}")?;
}
Ok(())
}
}
impl core::error::Error for ArgsErrorWithInput {}
#[derive(Debug)]
pub struct ArgsError {
#[allow(unused)]
pub span: Span,
pub kind: ArgsErrorKind,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ArgsErrorKind {
HelpRequested {
help_text: alloc::string::String,
},
UnexpectedPositionalArgument {
fields: &'static [Field],
},
NoFields {
shape: &'static Shape,
},
EnumWithoutSubcommandAttribute {
field: &'static Field,
},
UnknownLongFlag {
flag: String,
fields: &'static [Field],
},
UnknownShortFlag {
flag: String,
fields: &'static [Field],
precise_span: Option<Span>,
},
MissingArgument {
field: &'static Field,
},
ExpectedValueGotEof {
shape: &'static Shape,
},
UnknownSubcommand {
provided: String,
variants: &'static [Variant],
},
MissingSubcommand {
variants: &'static [Variant],
},
ReflectError(ReflectError),
}
impl ArgsErrorKind {
pub const fn precise_span(&self) -> Option<Span> {
match self {
ArgsErrorKind::UnknownShortFlag { precise_span, .. } => *precise_span,
_ => None,
}
}
pub const fn code(&self) -> &'static str {
match self {
ArgsErrorKind::HelpRequested { .. } => "args::help",
ArgsErrorKind::UnexpectedPositionalArgument { .. } => "args::unexpected_positional",
ArgsErrorKind::NoFields { .. } => "args::no_fields",
ArgsErrorKind::EnumWithoutSubcommandAttribute { .. } => {
"args::enum_without_subcommand_attribute"
}
ArgsErrorKind::UnknownLongFlag { .. } => "args::unknown_long_flag",
ArgsErrorKind::UnknownShortFlag { .. } => "args::unknown_short_flag",
ArgsErrorKind::MissingArgument { .. } => "args::missing_argument",
ArgsErrorKind::ExpectedValueGotEof { .. } => "args::expected_value",
ArgsErrorKind::UnknownSubcommand { .. } => "args::unknown_subcommand",
ArgsErrorKind::MissingSubcommand { .. } => "args::missing_subcommand",
ArgsErrorKind::ReflectError(_) => "args::reflect_error",
}
}
pub fn label(&self) -> String {
match self {
ArgsErrorKind::HelpRequested { .. } => "help requested".to_string(),
ArgsErrorKind::UnexpectedPositionalArgument { .. } => {
"unexpected positional argument".to_string()
}
ArgsErrorKind::NoFields { shape } => {
format!("cannot parse arguments into `{}`", shape.type_identifier)
}
ArgsErrorKind::EnumWithoutSubcommandAttribute { field } => {
format!(
"enum field `{}` must be marked with `#[facet(args::subcommand)]` to be used as subcommands",
field.name
)
}
ArgsErrorKind::UnknownLongFlag { flag, .. } => {
format!("unknown flag `--{flag}`")
}
ArgsErrorKind::UnknownShortFlag { flag, .. } => {
format!("unknown flag `-{flag}`")
}
ArgsErrorKind::ExpectedValueGotEof { shape } => {
let inner_type = unwrap_option_type(shape);
format!("expected `{inner_type}` value")
}
ArgsErrorKind::ReflectError(err) => format_reflect_error(err),
ArgsErrorKind::MissingArgument { field } => {
let doc_hint = field
.doc
.first()
.map(|d| format!(" ({})", d.trim()))
.unwrap_or_default();
let positional = field.has_attr(Some("args"), "positional");
let arg_name = if positional {
format!("<{}>", field.name.to_kebab_case())
} else {
format!("--{}", field.name.to_kebab_case())
};
format!("missing required argument `{arg_name}`{doc_hint}")
}
ArgsErrorKind::UnknownSubcommand { provided, .. } => {
format!("unknown subcommand `{provided}`")
}
ArgsErrorKind::MissingSubcommand { .. } => "expected a subcommand".to_string(),
}
}
pub fn help(&self) -> Option<Box<dyn core::fmt::Display + '_>> {
match self {
ArgsErrorKind::UnexpectedPositionalArgument { fields } => {
if fields.is_empty() {
return Some(Box::new(
"this command does not accept positional arguments",
));
}
if let Some(enum_field) = fields.iter().find(|f| {
matches!(f.shape().ty, Type::User(UserType::Enum(_)))
&& !f.has_attr(Some("args"), "subcommand")
}) {
return Some(Box::new(format!(
"available options:\n{}\n\nnote: field `{}` is an enum but missing `#[facet(args::subcommand)]` attribute. Enums must be marked as subcommands to accept positional arguments.",
format_available_flags(fields),
enum_field.name
)));
}
let flags = format_available_flags(fields);
Some(Box::new(format!("available options:\n{flags}")))
}
ArgsErrorKind::UnknownLongFlag { flag, fields } => {
if let Some(suggestion) = find_similar_flag(flag, fields) {
return Some(Box::new(format!("did you mean `--{suggestion}`?")));
}
if fields.is_empty() {
return None;
}
let flags = format_available_flags(fields);
Some(Box::new(format!("available options:\n{flags}")))
}
ArgsErrorKind::UnknownShortFlag { flag, fields, .. } => {
let short_char = flag.chars().next();
if let Some(field) = fields.iter().find(|f| get_short_flag(f) == short_char) {
return Some(Box::new(format!(
"`-{}` is `--{}`",
flag,
field.name.to_kebab_case()
)));
}
if fields.is_empty() {
return None;
}
let flags = format_available_flags(fields);
Some(Box::new(format!("available options:\n{flags}")))
}
ArgsErrorKind::MissingArgument { field } => {
let kebab = field.name.to_kebab_case();
let type_name = field.shape().type_identifier;
let positional = field.has_attr(Some("args"), "positional");
if positional {
Some(Box::new(format!("provide a value for `<{kebab}>`")))
} else {
Some(Box::new(format!(
"provide a value with `--{kebab} <{type_name}>`"
)))
}
}
ArgsErrorKind::UnknownSubcommand { provided, variants } => {
if variants.is_empty() {
return None;
}
if let Some(suggestion) = find_similar_subcommand(provided, variants) {
return Some(Box::new(format!("did you mean `{suggestion}`?")));
}
let cmds = format_available_subcommands(variants);
Some(Box::new(format!("available subcommands:\n{cmds}")))
}
ArgsErrorKind::MissingSubcommand { variants } => {
if variants.is_empty() {
return None;
}
let cmds = format_available_subcommands(variants);
Some(Box::new(format!("available subcommands:\n{cmds}")))
}
ArgsErrorKind::ExpectedValueGotEof { .. } => {
Some(Box::new("provide a value after the flag"))
}
ArgsErrorKind::HelpRequested { .. }
| ArgsErrorKind::NoFields { .. }
| ArgsErrorKind::EnumWithoutSubcommandAttribute { .. }
| ArgsErrorKind::ReflectError(_) => None,
}
}
pub const fn is_help_request(&self) -> bool {
matches!(self, ArgsErrorKind::HelpRequested { .. })
}
pub fn help_text(&self) -> Option<&str> {
match self {
ArgsErrorKind::HelpRequested { help_text } => Some(help_text),
_ => None,
}
}
}
fn format_two_column_list(
items: impl IntoIterator<Item = (String, Option<&'static str>)>,
) -> String {
use core::fmt::Write;
let items: Vec<_> = items.into_iter().collect();
let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
let mut lines = Vec::new();
for (name, doc) in items {
let mut line = String::new();
write!(line, " {name}").unwrap();
let padding = max_width.saturating_sub(name.len());
for _ in 0..padding {
line.push(' ');
}
if let Some(doc) = doc {
write!(line, " {}", doc.trim()).unwrap();
}
lines.push(line);
}
lines.join("\n")
}
fn format_available_flags(fields: &'static [Field]) -> String {
let items = fields.iter().filter_map(|field| {
if field.has_attr(Some("args"), "subcommand") {
return None;
}
let short = get_short_flag(field);
let positional = field.has_attr(Some("args"), "positional");
let kebab = field.name.to_kebab_case();
let name = if positional {
match short {
Some(s) => format!("-{s}, <{kebab}>"),
None => format!(" <{kebab}>"),
}
} else {
match short {
Some(s) => format!("-{s}, --{kebab}"),
None => format!(" --{kebab}"),
}
};
Some((name, field.doc.first().copied()))
});
format_two_column_list(items)
}
fn format_available_subcommands(variants: &'static [Variant]) -> String {
let items = variants.iter().map(|variant| {
let name = variant
.get_builtin_attr("rename")
.and_then(|attr| attr.get_as::<&str>())
.map(|s| (*s).to_string())
.unwrap_or_else(|| variant.name.to_kebab_case());
(name, variant.doc.first().copied())
});
format_two_column_list(items)
}
fn get_short_flag(field: &Field) -> Option<char> {
field
.get_attr(Some("args"), "short")
.and_then(|attr| attr.get_as::<crate::Attr>())
.and_then(|attr| {
if let crate::Attr::Short(c) = attr {
c.or_else(|| field.name.chars().next())
} else {
None
}
})
}
fn find_similar_flag(input: &str, fields: &'static [Field]) -> Option<String> {
for field in fields {
let kebab = field.name.to_kebab_case();
if is_similar(input, &kebab) {
return Some(kebab);
}
}
None
}
fn find_similar_subcommand(input: &str, variants: &'static [Variant]) -> Option<String> {
for variant in variants {
let name = variant
.get_builtin_attr("rename")
.and_then(|attr| attr.get_as::<&str>())
.map(|s| (*s).to_string())
.unwrap_or_else(|| variant.name.to_kebab_case());
if is_similar(input, &name) {
return Some(name);
}
}
None
}
fn is_similar(a: &str, b: &str) -> bool {
if a == b {
return true;
}
let len_diff = (a.len() as isize - b.len() as isize).abs();
if len_diff > 2 {
return false;
}
let mut diffs = 0;
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
for (ac, bc) in a_chars.iter().zip(b_chars.iter()) {
if ac != bc {
diffs += 1;
}
}
diffs += len_diff as usize;
diffs <= 2
}
const fn unwrap_option_type(shape: &'static Shape) -> &'static str {
match shape.def {
facet_core::Def::Option(opt_def) => opt_def.t.type_identifier,
_ => shape.type_identifier,
}
}
fn format_reflect_error(err: &ReflectError) -> String {
use facet_reflect::ReflectError::*;
match err {
ParseFailed { shape, .. } => {
let inner_type = unwrap_option_type(shape);
format!("invalid value for `{inner_type}`")
}
OperationFailed { shape, operation } => {
let inner_type = unwrap_option_type(shape);
if operation.starts_with("Subcommands must be provided") {
return operation.to_string();
}
match *operation {
"Type does not support parsing from string" => {
format!("`{inner_type}` cannot be parsed from a string value")
}
"Failed to parse string value" => {
format!("invalid value for `{inner_type}`")
}
_ => format!("`{inner_type}`: {operation}"),
}
}
UninitializedField { shape, field_name } => {
format!(
"field `{}` of `{}` was not provided",
field_name, shape.type_identifier
)
}
WrongShape { expected, actual } => {
format!(
"expected `{}`, got `{}`",
expected.type_identifier, actual.type_identifier
)
}
_ => format!("{err}"),
}
}
impl core::fmt::Display for ArgsErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.label())
}
}
impl From<ReflectError> for ArgsErrorKind {
fn from(error: ReflectError) -> Self {
ArgsErrorKind::ReflectError(error)
}
}
impl ArgsError {
pub const fn new(kind: ArgsErrorKind, span: Span) -> Self {
Self { span, kind }
}
}
impl fmt::Display for ArgsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
pub(crate) const fn get_variants_from_shape(shape: &'static Shape) -> &'static [Variant] {
if let Type::User(UserType::Enum(enum_type)) = shape.ty {
enum_type.variants
} else {
&[]
}
}
#[cfg(feature = "ariadne")]
mod ariadne_impl {
use super::*;
use ariadne::{Color, Label, Report, ReportKind, Source};
impl ArgsErrorWithInput {
pub fn to_ariadne_report(&self) -> Report<'static, core::ops::Range<usize>> {
if self.is_help_request() {
return Report::build(ReportKind::Custom("Help", Color::Cyan), 0..0)
.with_message(self.help_text().unwrap_or(""))
.finish();
}
let span = self.inner.kind.precise_span().unwrap_or(self.inner.span);
let range = span.start..(span.start + span.len);
let mut builder = Report::build(ReportKind::Error, range.clone())
.with_code(self.inner.kind.code())
.with_message(self.inner.kind.label());
builder = builder.with_label(
Label::new(range)
.with_message(self.inner.kind.label())
.with_color(Color::Red),
);
if let Some(help) = self.inner.kind.help() {
builder = builder.with_help(help.to_string());
}
builder.finish()
}
pub fn write_ariadne(&self, writer: impl std::io::Write) -> std::io::Result<()> {
let source = Source::from(&self.flattened_args);
self.to_ariadne_report().write(source, writer)
}
pub fn to_ariadne_string(&self) -> String {
let mut buf = Vec::new();
self.write_ariadne(&mut buf).expect("write to Vec failed");
String::from_utf8(buf).expect("ariadne output is valid UTF-8")
}
}
}