1extern crate alloc;
2
3use crate::span::Span;
4use core::fmt;
5use facet_core::{Field, Shape, Type, UserType, Variant};
6use facet_reflect::ReflectError;
7use heck::ToKebabCase;
8
9#[derive(Debug)]
11pub struct ArgsErrorWithInput {
12 pub(crate) inner: ArgsError,
14
15 #[allow(unused)]
17 pub(crate) flattened_args: String,
18}
19
20impl ArgsErrorWithInput {
21 pub const fn is_help_request(&self) -> bool {
23 self.inner.kind.is_help_request()
24 }
25
26 pub fn help_text(&self) -> Option<&str> {
28 self.inner.kind.help_text()
29 }
30}
31
32impl core::fmt::Display for ArgsErrorWithInput {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 if let Some(help) = self.help_text() {
36 return write!(f, "{}", help);
37 }
38
39 write!(f, "error: {}", self.inner.kind.label())?;
41
42 if let Some(help) = self.inner.kind.help() {
44 write!(f, "\n\n{help}")?;
45 }
46
47 Ok(())
48 }
49}
50
51impl core::error::Error for ArgsErrorWithInput {}
52
53#[derive(Debug)]
55pub struct ArgsError {
56 #[allow(unused)]
58 pub span: Span,
59
60 pub kind: ArgsErrorKind,
62}
63
64#[derive(Debug, Clone)]
68#[non_exhaustive]
69pub enum ArgsErrorKind {
70 HelpRequested {
75 help_text: alloc::string::String,
77 },
78
79 UnexpectedPositionalArgument {
81 fields: &'static [Field],
83 },
84
85 NoFields {
88 shape: &'static Shape,
90 },
91
92 EnumWithoutSubcommandAttribute {
95 field: &'static Field,
97 },
98
99 UnknownLongFlag {
101 flag: String,
103 fields: &'static [Field],
105 },
106
107 UnknownShortFlag {
109 flag: String,
111 fields: &'static [Field],
113 precise_span: Option<Span>,
115 },
116
117 MissingArgument {
119 field: &'static Field,
121 },
122
123 ExpectedValueGotEof {
125 shape: &'static Shape,
127 },
128
129 UnknownSubcommand {
131 provided: String,
133 variants: &'static [Variant],
135 },
136
137 MissingSubcommand {
139 variants: &'static [Variant],
141 },
142
143 ReflectError(ReflectError),
145}
146
147impl ArgsErrorKind {
148 pub const fn precise_span(&self) -> Option<Span> {
152 match self {
153 ArgsErrorKind::UnknownShortFlag { precise_span, .. } => *precise_span,
154 _ => None,
155 }
156 }
157
158 pub const fn code(&self) -> &'static str {
160 match self {
161 ArgsErrorKind::HelpRequested { .. } => "args::help",
162 ArgsErrorKind::UnexpectedPositionalArgument { .. } => "args::unexpected_positional",
163 ArgsErrorKind::NoFields { .. } => "args::no_fields",
164 ArgsErrorKind::EnumWithoutSubcommandAttribute { .. } => {
165 "args::enum_without_subcommand_attribute"
166 }
167 ArgsErrorKind::UnknownLongFlag { .. } => "args::unknown_long_flag",
168 ArgsErrorKind::UnknownShortFlag { .. } => "args::unknown_short_flag",
169 ArgsErrorKind::MissingArgument { .. } => "args::missing_argument",
170 ArgsErrorKind::ExpectedValueGotEof { .. } => "args::expected_value",
171 ArgsErrorKind::UnknownSubcommand { .. } => "args::unknown_subcommand",
172 ArgsErrorKind::MissingSubcommand { .. } => "args::missing_subcommand",
173 ArgsErrorKind::ReflectError(_) => "args::reflect_error",
174 }
175 }
176
177 pub fn label(&self) -> String {
179 match self {
180 ArgsErrorKind::HelpRequested { .. } => "help requested".to_string(),
181 ArgsErrorKind::UnexpectedPositionalArgument { .. } => {
182 "unexpected positional argument".to_string()
183 }
184 ArgsErrorKind::NoFields { shape } => {
185 format!("cannot parse arguments into `{}`", shape.type_identifier)
186 }
187 ArgsErrorKind::EnumWithoutSubcommandAttribute { field } => {
188 format!(
189 "enum field `{}` must be marked with `#[facet(args::subcommand)]` to be used as subcommands",
190 field.name
191 )
192 }
193 ArgsErrorKind::UnknownLongFlag { flag, .. } => {
194 format!("unknown flag `--{flag}`")
195 }
196 ArgsErrorKind::UnknownShortFlag { flag, .. } => {
197 format!("unknown flag `-{flag}`")
198 }
199 ArgsErrorKind::ExpectedValueGotEof { shape } => {
200 let inner_type = unwrap_option_type(shape);
202 format!("expected `{inner_type}` value")
203 }
204 ArgsErrorKind::ReflectError(err) => format_reflect_error(err),
205 ArgsErrorKind::MissingArgument { field } => {
206 let doc_hint = field
207 .doc
208 .first()
209 .map(|d| format!(" ({})", d.trim()))
210 .unwrap_or_default();
211 let positional = field.has_attr(Some("args"), "positional");
212 let arg_name = if positional {
213 format!("<{}>", field.name.to_kebab_case())
214 } else {
215 format!("--{}", field.name.to_kebab_case())
216 };
217 format!("missing required argument `{arg_name}`{doc_hint}")
218 }
219 ArgsErrorKind::UnknownSubcommand { provided, .. } => {
220 format!("unknown subcommand `{provided}`")
221 }
222 ArgsErrorKind::MissingSubcommand { .. } => "expected a subcommand".to_string(),
223 }
224 }
225
226 pub fn help(&self) -> Option<Box<dyn core::fmt::Display + '_>> {
228 match self {
229 ArgsErrorKind::UnexpectedPositionalArgument { fields } => {
230 if fields.is_empty() {
231 return Some(Box::new(
232 "this command does not accept positional arguments",
233 ));
234 }
235
236 if let Some(enum_field) = fields.iter().find(|f| {
238 matches!(f.shape().ty, Type::User(UserType::Enum(_)))
239 && !f.has_attr(Some("args"), "subcommand")
240 }) {
241 return Some(Box::new(format!(
242 "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.",
243 format_available_flags(fields),
244 enum_field.name
245 )));
246 }
247
248 let flags = format_available_flags(fields);
249 Some(Box::new(format!("available options:\n{flags}")))
250 }
251 ArgsErrorKind::UnknownLongFlag { flag, fields } => {
252 if let Some(suggestion) = find_similar_flag(flag, fields) {
254 return Some(Box::new(format!("did you mean `--{suggestion}`?")));
255 }
256 if fields.is_empty() {
257 return None;
258 }
259 let flags = format_available_flags(fields);
260 Some(Box::new(format!("available options:\n{flags}")))
261 }
262 ArgsErrorKind::UnknownShortFlag { flag, fields, .. } => {
263 let short_char = flag.chars().next();
265 if let Some(field) = fields.iter().find(|f| get_short_flag(f) == short_char) {
266 return Some(Box::new(format!(
267 "`-{}` is `--{}`",
268 flag,
269 field.name.to_kebab_case()
270 )));
271 }
272 if fields.is_empty() {
273 return None;
274 }
275 let flags = format_available_flags(fields);
276 Some(Box::new(format!("available options:\n{flags}")))
277 }
278 ArgsErrorKind::MissingArgument { field } => {
279 let kebab = field.name.to_kebab_case();
280 let type_name = field.shape().type_identifier;
281 let positional = field.has_attr(Some("args"), "positional");
282 if positional {
283 Some(Box::new(format!("provide a value for `<{kebab}>`")))
284 } else {
285 Some(Box::new(format!(
286 "provide a value with `--{kebab} <{type_name}>`"
287 )))
288 }
289 }
290 ArgsErrorKind::UnknownSubcommand { provided, variants } => {
291 if variants.is_empty() {
292 return None;
293 }
294 if let Some(suggestion) = find_similar_subcommand(provided, variants) {
296 return Some(Box::new(format!("did you mean `{suggestion}`?")));
297 }
298 let cmds = format_available_subcommands(variants);
299 Some(Box::new(format!("available subcommands:\n{cmds}")))
300 }
301 ArgsErrorKind::MissingSubcommand { variants } => {
302 if variants.is_empty() {
303 return None;
304 }
305 let cmds = format_available_subcommands(variants);
306 Some(Box::new(format!("available subcommands:\n{cmds}")))
307 }
308 ArgsErrorKind::ExpectedValueGotEof { .. } => {
309 Some(Box::new("provide a value after the flag"))
310 }
311 ArgsErrorKind::HelpRequested { .. }
312 | ArgsErrorKind::NoFields { .. }
313 | ArgsErrorKind::EnumWithoutSubcommandAttribute { .. }
314 | ArgsErrorKind::ReflectError(_) => None,
315 }
316 }
317
318 pub const fn is_help_request(&self) -> bool {
320 matches!(self, ArgsErrorKind::HelpRequested { .. })
321 }
322
323 pub fn help_text(&self) -> Option<&str> {
325 match self {
326 ArgsErrorKind::HelpRequested { help_text } => Some(help_text),
327 _ => None,
328 }
329 }
330}
331
332fn format_two_column_list(
334 items: impl IntoIterator<Item = (String, Option<&'static str>)>,
335) -> String {
336 use core::fmt::Write;
337
338 let items: Vec<_> = items.into_iter().collect();
339
340 let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
342
343 let mut lines = Vec::new();
344 for (name, doc) in items {
345 let mut line = String::new();
346 write!(line, " {name}").unwrap();
347
348 let padding = max_width.saturating_sub(name.len());
350 for _ in 0..padding {
351 line.push(' ');
352 }
353
354 if let Some(doc) = doc {
355 write!(line, " {}", doc.trim()).unwrap();
356 }
357
358 lines.push(line);
359 }
360 lines.join("\n")
361}
362
363fn format_available_flags(fields: &'static [Field]) -> String {
365 let items = fields.iter().filter_map(|field| {
366 if field.has_attr(Some("args"), "subcommand") {
367 return None;
368 }
369
370 let short = get_short_flag(field);
371 let positional = field.has_attr(Some("args"), "positional");
372 let kebab = field.name.to_kebab_case();
373
374 let name = if positional {
375 match short {
376 Some(s) => format!("-{s}, <{kebab}>"),
377 None => format!(" <{kebab}>"),
378 }
379 } else {
380 match short {
381 Some(s) => format!("-{s}, --{kebab}"),
382 None => format!(" --{kebab}"),
383 }
384 };
385
386 Some((name, field.doc.first().copied()))
387 });
388
389 format_two_column_list(items)
390}
391
392fn format_available_subcommands(variants: &'static [Variant]) -> String {
394 let items = variants.iter().map(|variant| {
395 let name = variant
396 .get_builtin_attr("rename")
397 .and_then(|attr| attr.get_as::<&str>())
398 .map(|s| (*s).to_string())
399 .unwrap_or_else(|| variant.name.to_kebab_case());
400
401 (name, variant.doc.first().copied())
402 });
403
404 format_two_column_list(items)
405}
406
407fn get_short_flag(field: &Field) -> Option<char> {
409 field
410 .get_attr(Some("args"), "short")
411 .and_then(|attr| attr.get_as::<crate::Attr>())
412 .and_then(|attr| {
413 if let crate::Attr::Short(c) = attr {
414 c.or_else(|| field.name.chars().next())
416 } else {
417 None
418 }
419 })
420}
421
422fn find_similar_flag(input: &str, fields: &'static [Field]) -> Option<String> {
424 for field in fields {
425 let kebab = field.name.to_kebab_case();
426 if is_similar(input, &kebab) {
427 return Some(kebab);
428 }
429 }
430 None
431}
432
433fn find_similar_subcommand(input: &str, variants: &'static [Variant]) -> Option<String> {
435 for variant in variants {
436 let name = variant
438 .get_builtin_attr("rename")
439 .and_then(|attr| attr.get_as::<&str>())
440 .map(|s| (*s).to_string())
441 .unwrap_or_else(|| variant.name.to_kebab_case());
442 if is_similar(input, &name) {
443 return Some(name);
444 }
445 }
446 None
447}
448
449fn is_similar(a: &str, b: &str) -> bool {
451 if a == b {
452 return true;
453 }
454 let len_diff = (a.len() as isize - b.len() as isize).abs();
455 if len_diff > 2 {
456 return false;
457 }
458
459 let mut diffs = 0;
461 let a_chars: Vec<char> = a.chars().collect();
462 let b_chars: Vec<char> = b.chars().collect();
463
464 for (ac, bc) in a_chars.iter().zip(b_chars.iter()) {
465 if ac != bc {
466 diffs += 1;
467 }
468 }
469 diffs += len_diff as usize;
470 diffs <= 2
471}
472
473const fn unwrap_option_type(shape: &'static Shape) -> &'static str {
475 match shape.def {
476 facet_core::Def::Option(opt_def) => opt_def.t.type_identifier,
477 _ => shape.type_identifier,
478 }
479}
480
481fn format_reflect_error(err: &ReflectError) -> String {
483 use facet_reflect::ReflectError::*;
484 match err {
485 ParseFailed { shape, .. } => {
486 let inner_type = unwrap_option_type(shape);
488 format!("invalid value for `{inner_type}`")
489 }
490 OperationFailed { shape, operation } => {
491 let inner_type = unwrap_option_type(shape);
494
495 if operation.starts_with("Subcommands must be provided") {
497 return operation.to_string();
498 }
499
500 match *operation {
501 "Type does not support parsing from string" => {
502 format!("`{inner_type}` cannot be parsed from a string value")
503 }
504 "Failed to parse string value" => {
505 format!("invalid value for `{inner_type}`")
506 }
507 _ => format!("`{inner_type}`: {operation}"),
508 }
509 }
510 UninitializedField { shape, field_name } => {
511 format!(
512 "field `{}` of `{}` was not provided",
513 field_name, shape.type_identifier
514 )
515 }
516 WrongShape { expected, actual } => {
517 format!(
518 "expected `{}`, got `{}`",
519 expected.type_identifier, actual.type_identifier
520 )
521 }
522 _ => format!("{err}"),
523 }
524}
525
526impl core::fmt::Display for ArgsErrorKind {
527 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
528 write!(f, "{}", self.label())
529 }
530}
531
532impl From<ReflectError> for ArgsErrorKind {
533 fn from(error: ReflectError) -> Self {
534 ArgsErrorKind::ReflectError(error)
535 }
536}
537
538impl ArgsError {
539 pub const fn new(kind: ArgsErrorKind, span: Span) -> Self {
541 Self { span, kind }
542 }
543}
544
545impl fmt::Display for ArgsError {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547 fmt::Debug::fmt(self, f)
548 }
549}
550
551pub(crate) const fn get_variants_from_shape(shape: &'static Shape) -> &'static [Variant] {
553 if let Type::User(UserType::Enum(enum_type)) = shape.ty {
554 enum_type.variants
555 } else {
556 &[]
557 }
558}
559
560#[cfg(feature = "ariadne")]
561mod ariadne_impl {
562 use super::*;
563 use ariadne::{Color, Label, Report, ReportKind, Source};
564
565 impl ArgsErrorWithInput {
566 pub fn to_ariadne_report(&self) -> Report<'static, core::ops::Range<usize>> {
571 if self.is_help_request() {
573 return Report::build(ReportKind::Custom("Help", Color::Cyan), 0..0)
574 .with_message(self.help_text().unwrap_or(""))
575 .finish();
576 }
577
578 let span = self.inner.kind.precise_span().unwrap_or(self.inner.span);
580 let range = span.start..(span.start + span.len);
581
582 let mut builder = Report::build(ReportKind::Error, range.clone())
583 .with_code(self.inner.kind.code())
584 .with_message(self.inner.kind.label());
585
586 builder = builder.with_label(
588 Label::new(range)
589 .with_message(self.inner.kind.label())
590 .with_color(Color::Red),
591 );
592
593 if let Some(help) = self.inner.kind.help() {
595 builder = builder.with_help(help.to_string());
596 }
597
598 builder.finish()
599 }
600
601 pub fn write_ariadne(&self, writer: impl std::io::Write) -> std::io::Result<()> {
606 let source = Source::from(&self.flattened_args);
607 self.to_ariadne_report().write(source, writer)
608 }
609
610 pub fn to_ariadne_string(&self) -> String {
615 let mut buf = Vec::new();
616 self.write_ariadne(&mut buf).expect("write to Vec failed");
618 String::from_utf8(buf).expect("ariadne output is valid UTF-8")
619 }
620 }
621}