1use crate::span::Span;
2use facet_core::{Field, Shape, Type, UserType, Variant};
3use facet_reflect::ReflectError;
4use heck::ToKebabCase;
5use std::fmt;
6
7#[derive(Debug)]
9pub struct ArgsErrorWithInput {
10 pub(crate) inner: ArgsError,
12
13 #[allow(unused)]
15 pub(crate) flattened_args: String,
16}
17
18#[derive(Clone, Copy, Debug)]
19pub struct ShapeDiagnostics {
20 pub shape: &'static Shape,
21 pub field: &'static Field,
22}
23
24impl ArgsErrorWithInput {
25 pub const fn is_help_request(&self) -> bool {
27 self.inner.kind.is_help_request()
28 }
29
30 pub fn help_text(&self) -> Option<&str> {
32 self.inner.kind.help_text()
33 }
34
35 pub fn shape_diagnostics(&self) -> Option<ShapeDiagnostics> {
37 match self.inner.kind {
38 ArgsErrorKind::MissingArgsAnnotation { shape, field } => {
39 Some(ShapeDiagnostics { shape, field })
40 }
41 _ => None,
42 }
43 }
44}
45
46impl core::fmt::Display for ArgsErrorWithInput {
47 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48 if let Some(help) = self.help_text() {
50 return write!(f, "{}", help);
51 }
52
53 write!(f, "error: {}", self.inner.kind.label())?;
55
56 if let Some(help) = self.inner.kind.help() {
58 write!(f, "\n\n{help}")?;
59 }
60
61 Ok(())
62 }
63}
64
65impl core::error::Error for ArgsErrorWithInput {}
66
67#[derive(Debug)]
69pub struct ArgsError {
70 #[allow(unused)]
72 pub span: Span,
73
74 pub kind: ArgsErrorKind,
76}
77
78#[derive(Debug, Clone)]
82#[non_exhaustive]
83pub enum ArgsErrorKind {
84 HelpRequested {
89 help_text: String,
91 },
92
93 VersionRequested {
95 version_text: String,
97 },
98
99 CompletionsRequested {
101 script: String,
103 },
104
105 UnexpectedPositionalArgument {
107 fields: &'static [Field],
109 },
110
111 NoFields {
114 shape: &'static Shape,
116 },
117
118 EnumWithoutSubcommandAttribute {
121 field: &'static Field,
123 },
124
125 MissingArgsAnnotation {
127 field: &'static Field,
129 shape: &'static Shape,
131 },
132
133 UnknownLongFlag {
135 flag: String,
137 fields: &'static [Field],
139 },
140
141 UnknownShortFlag {
143 flag: String,
145 fields: &'static [Field],
147 precise_span: Option<Span>,
149 },
150
151 MissingArgument {
153 field: &'static Field,
155 },
156
157 ExpectedValueGotEof {
159 shape: &'static Shape,
161 },
162
163 UnknownSubcommand {
165 provided: String,
167 variants: &'static [Variant],
169 },
170
171 MissingSubcommand {
173 variants: &'static [Variant],
175 },
176
177 ReflectError(ReflectError),
179}
180
181impl ArgsErrorKind {
182 pub const fn precise_span(&self) -> Option<Span> {
186 match self {
187 ArgsErrorKind::UnknownShortFlag { precise_span, .. } => *precise_span,
188 _ => None,
189 }
190 }
191
192 pub const fn code(&self) -> &'static str {
194 match self {
195 ArgsErrorKind::HelpRequested { .. } => "args::help",
196 ArgsErrorKind::VersionRequested { .. } => "args::version",
197 ArgsErrorKind::CompletionsRequested { .. } => "args::completions",
198 ArgsErrorKind::UnexpectedPositionalArgument { .. } => "args::unexpected_positional",
199 ArgsErrorKind::NoFields { .. } => "args::no_fields",
200 ArgsErrorKind::EnumWithoutSubcommandAttribute { .. } => {
201 "args::enum_without_subcommand_attribute"
202 }
203 ArgsErrorKind::MissingArgsAnnotation { .. } => "args::missing_args_annotation",
204 ArgsErrorKind::UnknownLongFlag { .. } => "args::unknown_long_flag",
205 ArgsErrorKind::UnknownShortFlag { .. } => "args::unknown_short_flag",
206 ArgsErrorKind::MissingArgument { .. } => "args::missing_argument",
207 ArgsErrorKind::ExpectedValueGotEof { .. } => "args::expected_value",
208 ArgsErrorKind::UnknownSubcommand { .. } => "args::unknown_subcommand",
209 ArgsErrorKind::MissingSubcommand { .. } => "args::missing_subcommand",
210 ArgsErrorKind::ReflectError(_) => "args::reflect_error",
211 }
212 }
213
214 pub fn label(&self) -> String {
216 match self {
217 ArgsErrorKind::HelpRequested { .. } => "help requested".to_string(),
218 ArgsErrorKind::VersionRequested { .. } => "version requested".to_string(),
219 ArgsErrorKind::CompletionsRequested { .. } => "completions requested".to_string(),
220 ArgsErrorKind::UnexpectedPositionalArgument { .. } => {
221 "unexpected positional argument".to_string()
222 }
223 ArgsErrorKind::NoFields { shape } => {
224 format!("cannot parse arguments into `{}`", shape.type_identifier)
225 }
226 ArgsErrorKind::EnumWithoutSubcommandAttribute { field } => {
227 format!(
228 "enum field `{}` must be marked with `#[facet(args::subcommand)]` to be used as subcommands",
229 field.name
230 )
231 }
232 ArgsErrorKind::MissingArgsAnnotation { field, shape } => {
233 format!(
234 "field `{}` in `{}` is missing a `#[facet(args::...)]` annotation",
235 field.name, shape.type_identifier
236 )
237 }
238 ArgsErrorKind::UnknownLongFlag { flag, .. } => {
239 format!("unknown flag `--{flag}`")
240 }
241 ArgsErrorKind::UnknownShortFlag { flag, .. } => {
242 format!("unknown flag `-{flag}`")
243 }
244 ArgsErrorKind::ExpectedValueGotEof { shape } => {
245 let inner_type = unwrap_option_type(shape);
247 format!("expected `{inner_type}` value")
248 }
249 ArgsErrorKind::ReflectError(err) => format_reflect_error(err),
250 ArgsErrorKind::MissingArgument { field } => {
251 let doc_hint = field
252 .doc
253 .first()
254 .map(|d| format!(" ({})", d.trim()))
255 .unwrap_or_default();
256 let positional = field.has_attr(Some("args"), "positional");
257 let arg_name = if positional {
258 format!("<{}>", field.name.to_kebab_case())
259 } else {
260 format!("--{}", field.name.to_kebab_case())
261 };
262 format!("missing required argument `{arg_name}`{doc_hint}")
263 }
264 ArgsErrorKind::UnknownSubcommand { provided, .. } => {
265 format!("unknown subcommand `{provided}`")
266 }
267 ArgsErrorKind::MissingSubcommand { .. } => "expected a subcommand".to_string(),
268 }
269 }
270
271 pub fn help(&self) -> Option<Box<dyn core::fmt::Display + '_>> {
273 match self {
274 ArgsErrorKind::UnexpectedPositionalArgument { fields } => {
275 if fields.is_empty() {
276 return Some(Box::new(
277 "this command does not accept positional arguments",
278 ));
279 }
280
281 if let Some(enum_field) = fields.iter().find(|f| {
283 matches!(f.shape().ty, Type::User(UserType::Enum(_)))
284 && !f.has_attr(Some("args"), "subcommand")
285 }) {
286 return Some(Box::new(format!(
287 "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.",
288 format_available_flags(fields),
289 enum_field.name
290 )));
291 }
292
293 let flags = format_available_flags(fields);
294 Some(Box::new(format!("available options:\n{flags}")))
295 }
296 ArgsErrorKind::UnknownLongFlag { flag, fields } => {
297 if let Some(suggestion) = find_similar_flag(flag, fields) {
299 return Some(Box::new(format!("did you mean `--{suggestion}`?")));
300 }
301 if fields.is_empty() {
302 return None;
303 }
304 let flags = format_available_flags(fields);
305 Some(Box::new(format!("available options:\n{flags}")))
306 }
307 ArgsErrorKind::UnknownShortFlag { flag, fields, .. } => {
308 let short_char = flag.chars().next();
310 if let Some(field) = fields.iter().find(|f| get_short_flag(f) == short_char) {
311 return Some(Box::new(format!(
312 "`-{}` is `--{}`",
313 flag,
314 field.name.to_kebab_case()
315 )));
316 }
317 if fields.is_empty() {
318 return None;
319 }
320 let flags = format_available_flags(fields);
321 Some(Box::new(format!("available options:\n{flags}")))
322 }
323 ArgsErrorKind::MissingArgument { field } => {
324 let kebab = field.name.to_kebab_case();
325 let type_name = field.shape().type_identifier;
326 let positional = field.has_attr(Some("args"), "positional");
327 if positional {
328 Some(Box::new(format!("provide a value for `<{kebab}>`")))
329 } else {
330 Some(Box::new(format!(
331 "provide a value with `--{kebab} <{type_name}>`"
332 )))
333 }
334 }
335 ArgsErrorKind::UnknownSubcommand { provided, variants } => {
336 if variants.is_empty() {
337 return None;
338 }
339 if let Some(suggestion) = find_similar_subcommand(provided, variants) {
341 return Some(Box::new(format!("did you mean `{suggestion}`?")));
342 }
343 let cmds = format_available_subcommands(variants);
344 Some(Box::new(format!("available subcommands:\n{cmds}")))
345 }
346 ArgsErrorKind::MissingSubcommand { variants } => {
347 if variants.is_empty() {
348 return None;
349 }
350 let cmds = format_available_subcommands(variants);
351 Some(Box::new(format!("available subcommands:\n{cmds}")))
352 }
353 ArgsErrorKind::ExpectedValueGotEof { .. } => {
354 Some(Box::new("provide a value after the flag"))
355 }
356 ArgsErrorKind::HelpRequested { .. }
357 | ArgsErrorKind::VersionRequested { .. }
358 | ArgsErrorKind::CompletionsRequested { .. }
359 | ArgsErrorKind::NoFields { .. }
360 | ArgsErrorKind::EnumWithoutSubcommandAttribute { .. }
361 | ArgsErrorKind::MissingArgsAnnotation { .. }
362 | ArgsErrorKind::ReflectError(_) => None,
363 }
364 }
365
366 pub const fn is_help_request(&self) -> bool {
368 matches!(self, ArgsErrorKind::HelpRequested { .. })
369 }
370
371 pub fn help_text(&self) -> Option<&str> {
373 match self {
374 ArgsErrorKind::HelpRequested { help_text } => Some(help_text),
375 _ => None,
376 }
377 }
378}
379
380fn format_two_column_list(
382 items: impl IntoIterator<Item = (String, Option<&'static str>)>,
383) -> String {
384 use std::fmt::Write;
385
386 let items: Vec<_> = items.into_iter().collect();
387
388 let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
390
391 let mut lines = Vec::new();
392 for (name, doc) in items {
393 let mut line = String::new();
394 write!(line, " {name}").unwrap();
395
396 let padding = max_width.saturating_sub(name.len());
398 for _ in 0..padding {
399 line.push(' ');
400 }
401
402 if let Some(doc) = doc {
403 write!(line, " {}", doc.trim()).unwrap();
404 }
405
406 lines.push(line);
407 }
408 lines.join("\n")
409}
410
411fn format_available_flags(fields: &'static [Field]) -> String {
413 let items = fields.iter().filter_map(|field| {
414 if field.has_attr(Some("args"), "subcommand") {
415 return None;
416 }
417
418 let short = get_short_flag(field);
419 let positional = field.has_attr(Some("args"), "positional");
420 let kebab = field.name.to_kebab_case();
421
422 let name = if positional {
423 match short {
424 Some(s) => format!("-{s}, <{kebab}>"),
425 None => format!(" <{kebab}>"),
426 }
427 } else {
428 match short {
429 Some(s) => format!("-{s}, --{kebab}"),
430 None => format!(" --{kebab}"),
431 }
432 };
433
434 Some((name, field.doc.first().copied()))
435 });
436
437 format_two_column_list(items)
438}
439
440fn format_available_subcommands(variants: &'static [Variant]) -> String {
442 let items = variants.iter().map(|variant| {
443 let name = variant
444 .get_builtin_attr("rename")
445 .and_then(|attr| attr.get_as::<&str>())
446 .map(|s| (*s).to_string())
447 .unwrap_or_else(|| variant.name.to_kebab_case());
448
449 (name, variant.doc.first().copied())
450 });
451
452 format_two_column_list(items)
453}
454
455fn get_short_flag(field: &Field) -> Option<char> {
457 field
458 .get_attr(Some("args"), "short")
459 .and_then(|attr| attr.get_as::<crate::Attr>())
460 .and_then(|attr| {
461 if let crate::Attr::Short(c) = attr {
462 c.or_else(|| field.name.chars().next())
464 } else {
465 None
466 }
467 })
468}
469
470fn find_similar_flag(input: &str, fields: &'static [Field]) -> Option<String> {
472 for field in fields {
473 let kebab = field.name.to_kebab_case();
474 if is_similar(input, &kebab) {
475 return Some(kebab);
476 }
477 }
478 None
479}
480
481fn find_similar_subcommand(input: &str, variants: &'static [Variant]) -> Option<String> {
483 for variant in variants {
484 let name = variant
486 .get_builtin_attr("rename")
487 .and_then(|attr| attr.get_as::<&str>())
488 .map(|s| (*s).to_string())
489 .unwrap_or_else(|| variant.name.to_kebab_case());
490 if is_similar(input, &name) {
491 return Some(name);
492 }
493 }
494 None
495}
496
497fn is_similar(a: &str, b: &str) -> bool {
499 if a == b {
500 return true;
501 }
502 let len_diff = (a.len() as isize - b.len() as isize).abs();
503 if len_diff > 2 {
504 return false;
505 }
506
507 let mut diffs = 0;
509 let a_chars: Vec<char> = a.chars().collect();
510 let b_chars: Vec<char> = b.chars().collect();
511
512 for (ac, bc) in a_chars.iter().zip(b_chars.iter()) {
513 if ac != bc {
514 diffs += 1;
515 }
516 }
517 diffs += len_diff as usize;
518 diffs <= 2
519}
520
521const fn unwrap_option_type(shape: &'static Shape) -> &'static str {
523 match shape.def {
524 facet_core::Def::Option(opt_def) => opt_def.t.type_identifier,
525 _ => shape.type_identifier,
526 }
527}
528
529fn format_reflect_error(err: &ReflectError) -> String {
531 use facet_reflect::ReflectErrorKind::*;
532 match &err.kind {
533 ParseFailed { shape, .. } => {
534 let inner_type = unwrap_option_type(shape);
536 format!("invalid value for `{inner_type}`")
537 }
538 OperationFailed { shape, operation } => {
539 let inner_type = unwrap_option_type(shape);
542
543 if operation.starts_with("Subcommands must be provided") {
545 return operation.to_string();
546 }
547
548 match *operation {
549 "Type does not support parsing from string" => {
550 format!("`{inner_type}` cannot be parsed from a string value")
551 }
552 "Failed to parse string value" => {
553 format!("invalid value for `{inner_type}`")
554 }
555 _ => format!("`{inner_type}`: {operation}"),
556 }
557 }
558 UninitializedField { shape, field_name } => {
559 format!(
560 "field `{}` of `{}` was not provided",
561 field_name, shape.type_identifier
562 )
563 }
564 WrongShape { expected, actual } => {
565 format!(
566 "expected `{}`, got `{}`",
567 expected.type_identifier, actual.type_identifier
568 )
569 }
570 _ => {
572 if err.path.is_empty() {
573 format!("{}", err.kind)
574 } else {
575 format!("{} at {}", err.kind, err.path)
576 }
577 }
578 }
579}
580
581impl core::fmt::Display for ArgsErrorKind {
582 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583 write!(f, "{}", self.label())
584 }
585}
586
587impl From<ReflectError> for ArgsErrorKind {
588 fn from(error: ReflectError) -> Self {
589 ArgsErrorKind::ReflectError(error)
590 }
591}
592
593impl ArgsError {
594 #[cfg(test)]
596 pub const fn new(kind: ArgsErrorKind, span: Span) -> Self {
597 Self { span, kind }
598 }
599}
600
601impl fmt::Display for ArgsError {
602 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603 fmt::Debug::fmt(self, f)
604 }
605}
606
607mod ariadne_impl {
608 use super::*;
609 use crate::color::should_use_color;
610 use ariadne::{Color, Config, Label, Report, ReportKind, Source};
611 use facet_pretty::{PathSegment, format_shape_with_spans};
612 use std::borrow::Cow;
613
614 impl ArgsErrorWithInput {
615 pub fn to_ariadne_report(&self) -> Report<'static, core::ops::Range<usize>> {
620 let should_use_color = should_use_color();
621
622 if self.is_help_request() {
624 return Report::build(ReportKind::Custom("Help", Color::Cyan), 0..0)
625 .with_config(Config::default().with_color(should_use_color))
626 .with_message(self.help_text().unwrap_or(""))
627 .finish();
628 }
629
630 if let Some(diag) = self.shape_diagnostics() {
631 let formatted = format_shape_with_spans(diag.shape);
632 let missing_path = vec![PathSegment::Field(Cow::Borrowed(diag.field.name))];
633
634 if let Some(field_span) = formatted.spans.get(&missing_path) {
635 let span = field_span.key.0..field_span.value.1;
636
637 let mut builder = Report::build(ReportKind::Error, span.clone())
638 .with_config(Config::default().with_color(should_use_color))
639 .with_code(self.inner.kind.code())
640 .with_message(self.inner.kind.label());
641
642 let def_end_span = formatted.type_end_span.map(|(start, end)| start..end);
643 if let Some(type_name_span) = formatted.type_name_span {
644 let type_label_span = type_name_span.0..type_name_span.1;
645
646 let source_label = diag
647 .shape
648 .source_file
649 .zip(diag.shape.source_line)
650 .map(|(file, line)| format!("defined at {file}:{line}"))
651 .unwrap_or_else(|| {
652 "definition location unavailable (enable facet/doc)".to_string()
653 });
654
655 let mut label = Label::new(type_label_span).with_message(source_label);
656 if should_use_color {
657 label = label.with_color(Color::Blue);
658 }
659
660 builder = builder.with_label(label);
661 }
662
663 let mut label = Label::new(span)
664 .with_message("THIS IS WHERE YOU FORGOT A facet(args::) annotation");
665 if should_use_color {
666 label = label.with_color(Color::Red);
667 }
668 builder = builder.with_label(label);
669
670 if let Some(def_end_span) = def_end_span {
671 let mut label = Label::new(def_end_span).with_message("end of definition");
672 if should_use_color {
673 label = label.with_color(Color::Blue);
674 }
675 builder = builder.with_label(label);
676 }
677
678 return builder.finish();
679 }
680 }
681
682 let span = self.inner.kind.precise_span().unwrap_or(self.inner.span);
684 let range = span.start..(span.start + span.len);
685
686 let mut builder = Report::build(ReportKind::Error, range.clone())
687 .with_config(Config::default().with_color(should_use_color))
688 .with_code(self.inner.kind.code())
689 .with_message(self.inner.kind.label());
690
691 let mut label = Label::new(range).with_message(self.inner.kind.label());
693 if should_use_color {
694 label = label.with_color(Color::Red);
695 }
696 builder = builder.with_label(label);
697
698 if let Some(help) = self.inner.kind.help() {
700 builder = builder.with_help(help.to_string());
701 }
702
703 builder.finish()
704 }
705
706 pub fn write_ariadne(&self, writer: impl std::io::Write) -> std::io::Result<()> {
711 if let Some(diag) = self.shape_diagnostics() {
712 let formatted = format_shape_with_spans(diag.shape);
713 let source = Source::from(&formatted.text);
714 return self.to_ariadne_report().write(source, writer);
715 }
716
717 let source = Source::from(&self.flattened_args);
718 self.to_ariadne_report().write(source, writer)
719 }
720
721 pub fn to_ariadne_string(&self) -> String {
726 let mut buf = Vec::new();
727 self.write_ariadne(&mut buf).expect("write to Vec failed");
729 String::from_utf8(buf).expect("ariadne output is valid UTF-8")
730 }
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use crate as args;
738 use facet::Facet;
739
740 #[test]
741 fn debug_missing_args_annotation_example() {
742 #[derive(Facet)]
743 struct App {
744 #[facet(args::named)]
745 verbose: bool,
746 config_path: String,
747 }
748
749 let shape = App::SHAPE;
750 let field = match &shape.ty {
751 Type::User(UserType::Struct(s)) => s
752 .fields
753 .iter()
754 .find(|f| f.name == "config_path")
755 .expect("config_path field"),
756 _ => panic!("expected struct shape"),
757 };
758
759 let err = ArgsErrorWithInput {
760 inner: ArgsError::new(
761 ArgsErrorKind::MissingArgsAnnotation { field, shape },
762 Span::new(0, 0),
763 ),
764 flattened_args: String::new(),
765 };
766
767 let rendered = err.to_ariadne_string();
769 assert!(
770 rendered.contains("config_path"),
771 "error should mention the field name: {}",
772 rendered
773 );
774 assert!(
775 rendered.contains("args::"),
776 "error should mention args annotation: {}",
777 rendered
778 );
779 }
780}