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;
8use miette::{Diagnostic, LabeledSpan};
9
10#[derive(Debug)]
12pub struct ArgsErrorWithInput {
13 pub(crate) inner: ArgsError,
15
16 pub(crate) flattened_args: String,
18}
19
20impl ArgsErrorWithInput {
21 pub 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 write!(f, "Could not parse CLI arguments")
39 }
40}
41
42impl core::error::Error for ArgsErrorWithInput {}
43
44impl Diagnostic for ArgsErrorWithInput {
45 fn code<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
46 Some(Box::new(self.inner.kind.code()))
47 }
48
49 fn severity(&self) -> Option<miette::Severity> {
50 if self.is_help_request() {
51 Some(miette::Severity::Advice)
52 } else {
53 Some(miette::Severity::Error)
54 }
55 }
56
57 fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
58 self.inner.kind.help()
59 }
60
61 fn url<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
62 None
63 }
64
65 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
66 if self.is_help_request() {
68 None
69 } else if self.flattened_args.is_empty() {
70 None
72 } else {
73 Some(&self.flattened_args)
74 }
75 }
76
77 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
78 if self.is_help_request() {
80 None
81 } else {
82 Some(Box::new(core::iter::once(LabeledSpan::new(
83 Some(self.inner.kind.label()),
84 self.inner.span.start,
85 self.inner.span.len(),
86 ))))
87 }
88 }
89
90 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
91 None
92 }
93
94 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
95 None
96 }
97}
98
99#[derive(Debug)]
101pub struct ArgsError {
102 pub span: Span,
104
105 pub kind: ArgsErrorKind,
107}
108
109#[derive(Debug, Clone)]
113#[non_exhaustive]
114pub enum ArgsErrorKind {
115 HelpRequested {
120 help_text: alloc::string::String,
122 },
123
124 UnexpectedPositionalArgument {
126 fields: &'static [Field],
128 },
129
130 NoFields {
133 shape: &'static Shape,
135 },
136
137 EnumWithoutSubcommandAttribute {
140 field: &'static Field,
142 },
143
144 UnknownLongFlag {
146 flag: String,
148 fields: &'static [Field],
150 },
151
152 UnknownShortFlag {
154 flag: String,
156 fields: &'static [Field],
158 precise_span: Option<Span>,
160 },
161
162 MissingArgument {
164 field: &'static Field,
166 },
167
168 ExpectedValueGotEof {
170 shape: &'static Shape,
172 },
173
174 UnknownSubcommand {
176 provided: String,
178 variants: &'static [Variant],
180 },
181
182 MissingSubcommand {
184 variants: &'static [Variant],
186 },
187
188 ReflectError(ReflectError),
190}
191
192impl ArgsErrorKind {
193 pub fn precise_span(&self) -> Option<Span> {
197 match self {
198 ArgsErrorKind::UnknownShortFlag { precise_span, .. } => *precise_span,
199 _ => None,
200 }
201 }
202
203 pub fn code(&self) -> &'static str {
205 match self {
206 ArgsErrorKind::HelpRequested { .. } => "args::help",
207 ArgsErrorKind::UnexpectedPositionalArgument { .. } => "args::unexpected_positional",
208 ArgsErrorKind::NoFields { .. } => "args::no_fields",
209 ArgsErrorKind::EnumWithoutSubcommandAttribute { .. } => {
210 "args::enum_without_subcommand_attribute"
211 }
212 ArgsErrorKind::UnknownLongFlag { .. } => "args::unknown_long_flag",
213 ArgsErrorKind::UnknownShortFlag { .. } => "args::unknown_short_flag",
214 ArgsErrorKind::MissingArgument { .. } => "args::missing_argument",
215 ArgsErrorKind::ExpectedValueGotEof { .. } => "args::expected_value",
216 ArgsErrorKind::UnknownSubcommand { .. } => "args::unknown_subcommand",
217 ArgsErrorKind::MissingSubcommand { .. } => "args::missing_subcommand",
218 ArgsErrorKind::ReflectError(_) => "args::reflect_error",
219 }
220 }
221
222 pub fn label(&self) -> String {
224 match self {
225 ArgsErrorKind::HelpRequested { .. } => "help requested".to_string(),
226 ArgsErrorKind::UnexpectedPositionalArgument { .. } => {
227 "unexpected positional argument".to_string()
228 }
229 ArgsErrorKind::NoFields { shape } => {
230 format!("cannot parse arguments into `{}`", shape.type_identifier)
231 }
232 ArgsErrorKind::EnumWithoutSubcommandAttribute { field } => {
233 format!(
234 "enum field `{}` must be marked with `#[facet(args::subcommand)]` to be used as subcommands",
235 field.name
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::NoFields { .. }
358 | ArgsErrorKind::EnumWithoutSubcommandAttribute { .. }
359 | ArgsErrorKind::ReflectError(_) => None,
360 }
361 }
362
363 pub fn is_help_request(&self) -> bool {
365 matches!(self, ArgsErrorKind::HelpRequested { .. })
366 }
367
368 pub fn help_text(&self) -> Option<&str> {
370 match self {
371 ArgsErrorKind::HelpRequested { help_text } => Some(help_text),
372 _ => None,
373 }
374 }
375}
376
377fn format_two_column_list(
379 items: impl IntoIterator<Item = (String, Option<&'static str>)>,
380) -> String {
381 use core::fmt::Write;
382
383 let items: Vec<_> = items.into_iter().collect();
384
385 let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
387
388 let mut lines = Vec::new();
389 for (name, doc) in items {
390 let mut line = String::new();
391 write!(line, " {name}").unwrap();
392
393 let padding = max_width.saturating_sub(name.len());
395 for _ in 0..padding {
396 line.push(' ');
397 }
398
399 if let Some(doc) = doc {
400 write!(line, " {}", doc.trim()).unwrap();
401 }
402
403 lines.push(line);
404 }
405 lines.join("\n")
406}
407
408fn format_available_flags(fields: &'static [Field]) -> String {
410 let items = fields.iter().filter_map(|field| {
411 if field.has_attr(Some("args"), "subcommand") {
412 return None;
413 }
414
415 let short = get_short_flag(field);
416 let positional = field.has_attr(Some("args"), "positional");
417 let kebab = field.name.to_kebab_case();
418
419 let name = if positional {
420 match short {
421 Some(s) => format!("-{s}, <{kebab}>"),
422 None => format!(" <{kebab}>"),
423 }
424 } else {
425 match short {
426 Some(s) => format!("-{s}, --{kebab}"),
427 None => format!(" --{kebab}"),
428 }
429 };
430
431 Some((name, field.doc.first().copied()))
432 });
433
434 format_two_column_list(items)
435}
436
437fn format_available_subcommands(variants: &'static [Variant]) -> String {
439 let items = variants.iter().map(|variant| {
440 let name = variant
441 .get_builtin_attr("rename")
442 .and_then(|attr| attr.get_as::<&str>())
443 .map(|s| (*s).to_string())
444 .unwrap_or_else(|| variant.name.to_kebab_case());
445
446 (name, variant.doc.first().copied())
447 });
448
449 format_two_column_list(items)
450}
451
452fn get_short_flag(field: &Field) -> Option<char> {
454 field
455 .get_attr(Some("args"), "short")
456 .and_then(|attr| attr.get_as::<crate::Attr>())
457 .and_then(|attr| {
458 if let crate::Attr::Short(c) = attr {
459 c.or_else(|| field.name.chars().next())
461 } else {
462 None
463 }
464 })
465}
466
467fn find_similar_flag(input: &str, fields: &'static [Field]) -> Option<String> {
469 for field in fields {
470 let kebab = field.name.to_kebab_case();
471 if is_similar(input, &kebab) {
472 return Some(kebab);
473 }
474 }
475 None
476}
477
478fn find_similar_subcommand(input: &str, variants: &'static [Variant]) -> Option<String> {
480 for variant in variants {
481 let name = variant
483 .get_builtin_attr("rename")
484 .and_then(|attr| attr.get_as::<&str>())
485 .map(|s| (*s).to_string())
486 .unwrap_or_else(|| variant.name.to_kebab_case());
487 if is_similar(input, &name) {
488 return Some(name);
489 }
490 }
491 None
492}
493
494fn is_similar(a: &str, b: &str) -> bool {
496 if a == b {
497 return true;
498 }
499 let len_diff = (a.len() as isize - b.len() as isize).abs();
500 if len_diff > 2 {
501 return false;
502 }
503
504 let mut diffs = 0;
506 let a_chars: Vec<char> = a.chars().collect();
507 let b_chars: Vec<char> = b.chars().collect();
508
509 for (ac, bc) in a_chars.iter().zip(b_chars.iter()) {
510 if ac != bc {
511 diffs += 1;
512 }
513 }
514 diffs += len_diff as usize;
515 diffs <= 2
516}
517
518fn unwrap_option_type(shape: &'static Shape) -> &'static str {
520 match shape.def {
521 facet_core::Def::Option(opt_def) => opt_def.t.type_identifier,
522 _ => shape.type_identifier,
523 }
524}
525
526fn format_reflect_error(err: &ReflectError) -> String {
528 use facet_reflect::ReflectError::*;
529 match err {
530 OperationFailed { shape, operation } => {
531 let inner_type = unwrap_option_type(shape);
534
535 if operation.starts_with("Subcommands must be provided") {
537 return operation.to_string();
538 }
539
540 match *operation {
541 "Type does not support parsing from string" => {
542 format!("`{inner_type}` cannot be parsed from a string value")
543 }
544 "Failed to parse string value" => {
545 format!("invalid value for `{inner_type}`")
546 }
547 _ => format!("`{inner_type}`: {operation}"),
548 }
549 }
550 UninitializedField { shape, field_name } => {
551 format!(
552 "field `{}` of `{}` was not provided",
553 field_name, shape.type_identifier
554 )
555 }
556 WrongShape { expected, actual } => {
557 format!(
558 "expected `{}`, got `{}`",
559 expected.type_identifier, actual.type_identifier
560 )
561 }
562 _ => format!("{err}"),
563 }
564}
565
566impl core::fmt::Display for ArgsErrorKind {
567 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
568 write!(f, "{}", self.label())
569 }
570}
571
572impl From<ReflectError> for ArgsErrorKind {
573 fn from(error: ReflectError) -> Self {
574 ArgsErrorKind::ReflectError(error)
575 }
576}
577
578impl ArgsError {
579 pub fn new(kind: ArgsErrorKind, span: Span) -> Self {
581 Self { span, kind }
582 }
583}
584
585impl fmt::Display for ArgsError {
586 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587 fmt::Debug::fmt(self, f)
588 }
589}
590
591pub(crate) fn get_variants_from_shape(shape: &'static Shape) -> &'static [Variant] {
593 if let Type::User(UserType::Enum(enum_type)) = shape.ty {
594 enum_type.variants
595 } else {
596 &[]
597 }
598}