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 Some(miette::Severity::Error)
51 }
52
53 fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
54 self.inner.kind.help()
55 }
56
57 fn url<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
58 None
59 }
60
61 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
62 Some(&self.flattened_args)
63 }
64
65 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
66 Some(Box::new(core::iter::once(LabeledSpan::new(
67 Some(self.inner.kind.label()),
68 self.inner.span.start,
69 self.inner.span.len(),
70 ))))
71 }
72
73 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
74 None
75 }
76
77 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
78 None
79 }
80}
81
82#[derive(Debug)]
84pub struct ArgsError {
85 pub span: Span,
87
88 pub kind: ArgsErrorKind,
90}
91
92#[derive(Debug, Clone)]
96#[non_exhaustive]
97pub enum ArgsErrorKind {
98 HelpRequested {
103 help_text: alloc::string::String,
105 },
106
107 UnexpectedPositionalArgument {
109 fields: &'static [Field],
111 },
112
113 NoFields {
116 shape: &'static Shape,
118 },
119
120 EnumWithoutSubcommandAttribute {
123 field: &'static Field,
125 },
126
127 UnknownLongFlag {
129 flag: String,
131 fields: &'static [Field],
133 },
134
135 UnknownShortFlag {
137 flag: String,
139 fields: &'static [Field],
141 precise_span: Option<Span>,
143 },
144
145 MissingArgument {
147 field: &'static Field,
149 },
150
151 ExpectedValueGotEof {
153 shape: &'static Shape,
155 },
156
157 UnknownSubcommand {
159 provided: String,
161 variants: &'static [Variant],
163 },
164
165 MissingSubcommand {
167 variants: &'static [Variant],
169 },
170
171 ReflectError(ReflectError),
173}
174
175impl ArgsErrorKind {
176 pub fn precise_span(&self) -> Option<Span> {
180 match self {
181 ArgsErrorKind::UnknownShortFlag { precise_span, .. } => *precise_span,
182 _ => None,
183 }
184 }
185
186 pub fn code(&self) -> &'static str {
188 match self {
189 ArgsErrorKind::HelpRequested { .. } => "args::help",
190 ArgsErrorKind::UnexpectedPositionalArgument { .. } => "args::unexpected_positional",
191 ArgsErrorKind::NoFields { .. } => "args::no_fields",
192 ArgsErrorKind::EnumWithoutSubcommandAttribute { .. } => {
193 "args::enum_without_subcommand_attribute"
194 }
195 ArgsErrorKind::UnknownLongFlag { .. } => "args::unknown_long_flag",
196 ArgsErrorKind::UnknownShortFlag { .. } => "args::unknown_short_flag",
197 ArgsErrorKind::MissingArgument { .. } => "args::missing_argument",
198 ArgsErrorKind::ExpectedValueGotEof { .. } => "args::expected_value",
199 ArgsErrorKind::UnknownSubcommand { .. } => "args::unknown_subcommand",
200 ArgsErrorKind::MissingSubcommand { .. } => "args::missing_subcommand",
201 ArgsErrorKind::ReflectError(_) => "args::reflect_error",
202 }
203 }
204
205 pub fn label(&self) -> String {
207 match self {
208 ArgsErrorKind::HelpRequested { .. } => "help requested".to_string(),
209 ArgsErrorKind::UnexpectedPositionalArgument { .. } => {
210 "unexpected positional argument".to_string()
211 }
212 ArgsErrorKind::NoFields { shape } => {
213 format!("cannot parse arguments into `{}`", shape.type_identifier)
214 }
215 ArgsErrorKind::EnumWithoutSubcommandAttribute { field } => {
216 format!(
217 "enum field `{}` must be marked with `#[facet(args::subcommand)]` to be used as subcommands",
218 field.name
219 )
220 }
221 ArgsErrorKind::UnknownLongFlag { flag, .. } => {
222 format!("unknown flag `--{flag}`")
223 }
224 ArgsErrorKind::UnknownShortFlag { flag, .. } => {
225 format!("unknown flag `-{flag}`")
226 }
227 ArgsErrorKind::ExpectedValueGotEof { shape } => {
228 let inner_type = unwrap_option_type(shape);
230 format!("expected `{inner_type}` value")
231 }
232 ArgsErrorKind::ReflectError(err) => format_reflect_error(err),
233 ArgsErrorKind::MissingArgument { field } => {
234 let doc_hint = field
235 .doc
236 .first()
237 .map(|d| format!(" ({})", d.trim()))
238 .unwrap_or_default();
239 let positional = field.has_attr(Some("args"), "positional");
240 let arg_name = if positional {
241 format!("<{}>", field.name.to_kebab_case())
242 } else {
243 format!("--{}", field.name.to_kebab_case())
244 };
245 format!("missing required argument `{arg_name}`{doc_hint}")
246 }
247 ArgsErrorKind::UnknownSubcommand { provided, .. } => {
248 format!("unknown subcommand `{provided}`")
249 }
250 ArgsErrorKind::MissingSubcommand { .. } => "expected a subcommand".to_string(),
251 }
252 }
253
254 pub fn help(&self) -> Option<Box<dyn core::fmt::Display + '_>> {
256 match self {
257 ArgsErrorKind::UnexpectedPositionalArgument { fields } => {
258 if fields.is_empty() {
259 return Some(Box::new(
260 "this command does not accept positional arguments",
261 ));
262 }
263
264 if let Some(enum_field) = fields.iter().find(|f| {
266 matches!(f.shape().ty, Type::User(UserType::Enum(_)))
267 && !f.has_attr(Some("args"), "subcommand")
268 }) {
269 return Some(Box::new(format!(
270 "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.",
271 format_available_flags(fields),
272 enum_field.name
273 )));
274 }
275
276 let flags = format_available_flags(fields);
277 Some(Box::new(format!("available options:\n{flags}")))
278 }
279 ArgsErrorKind::UnknownLongFlag { flag, fields } => {
280 if let Some(suggestion) = find_similar_flag(flag, fields) {
282 return Some(Box::new(format!("did you mean `--{suggestion}`?")));
283 }
284 if fields.is_empty() {
285 return None;
286 }
287 let flags = format_available_flags(fields);
288 Some(Box::new(format!("available options:\n{flags}")))
289 }
290 ArgsErrorKind::UnknownShortFlag { flag, fields, .. } => {
291 let short_char = flag.chars().next();
293 if let Some(field) = fields.iter().find(|f| get_short_flag(f) == short_char) {
294 return Some(Box::new(format!(
295 "`-{}` is `--{}`",
296 flag,
297 field.name.to_kebab_case()
298 )));
299 }
300 if fields.is_empty() {
301 return None;
302 }
303 let flags = format_available_flags(fields);
304 Some(Box::new(format!("available options:\n{flags}")))
305 }
306 ArgsErrorKind::MissingArgument { field } => {
307 let kebab = field.name.to_kebab_case();
308 let type_name = field.shape().type_identifier;
309 let positional = field.has_attr(Some("args"), "positional");
310 if positional {
311 Some(Box::new(format!("provide a value for `<{kebab}>`")))
312 } else {
313 Some(Box::new(format!(
314 "provide a value with `--{kebab} <{type_name}>`"
315 )))
316 }
317 }
318 ArgsErrorKind::UnknownSubcommand { provided, variants } => {
319 if variants.is_empty() {
320 return None;
321 }
322 if let Some(suggestion) = find_similar_subcommand(provided, variants) {
324 return Some(Box::new(format!("did you mean `{suggestion}`?")));
325 }
326 let cmds = format_available_subcommands(variants);
327 Some(Box::new(format!("available subcommands:\n{cmds}")))
328 }
329 ArgsErrorKind::MissingSubcommand { variants } => {
330 if variants.is_empty() {
331 return None;
332 }
333 let cmds = format_available_subcommands(variants);
334 Some(Box::new(format!("available subcommands:\n{cmds}")))
335 }
336 ArgsErrorKind::ExpectedValueGotEof { .. } => {
337 Some(Box::new("provide a value after the flag"))
338 }
339 ArgsErrorKind::HelpRequested { .. }
340 | ArgsErrorKind::NoFields { .. }
341 | ArgsErrorKind::EnumWithoutSubcommandAttribute { .. }
342 | ArgsErrorKind::ReflectError(_) => None,
343 }
344 }
345
346 pub fn is_help_request(&self) -> bool {
348 matches!(self, ArgsErrorKind::HelpRequested { .. })
349 }
350
351 pub fn help_text(&self) -> Option<&str> {
353 match self {
354 ArgsErrorKind::HelpRequested { help_text } => Some(help_text),
355 _ => None,
356 }
357 }
358}
359
360fn format_two_column_list(
362 items: impl IntoIterator<Item = (String, Option<&'static str>)>,
363) -> String {
364 use core::fmt::Write;
365
366 let items: Vec<_> = items.into_iter().collect();
367
368 let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
370
371 let mut lines = Vec::new();
372 for (name, doc) in items {
373 let mut line = String::new();
374 write!(line, " {name}").unwrap();
375
376 let padding = max_width.saturating_sub(name.len());
378 for _ in 0..padding {
379 line.push(' ');
380 }
381
382 if let Some(doc) = doc {
383 write!(line, " {}", doc.trim()).unwrap();
384 }
385
386 lines.push(line);
387 }
388 lines.join("\n")
389}
390
391fn format_available_flags(fields: &'static [Field]) -> String {
393 let items = fields.iter().filter_map(|field| {
394 if field.has_attr(Some("args"), "subcommand") {
395 return None;
396 }
397
398 let short = get_short_flag(field);
399 let positional = field.has_attr(Some("args"), "positional");
400 let kebab = field.name.to_kebab_case();
401
402 let name = if positional {
403 match short {
404 Some(s) => format!("-{s}, <{kebab}>"),
405 None => format!(" <{kebab}>"),
406 }
407 } else {
408 match short {
409 Some(s) => format!("-{s}, --{kebab}"),
410 None => format!(" --{kebab}"),
411 }
412 };
413
414 Some((name, field.doc.first().copied()))
415 });
416
417 format_two_column_list(items)
418}
419
420fn format_available_subcommands(variants: &'static [Variant]) -> String {
422 let items = variants.iter().map(|variant| {
423 let name = variant
424 .get_builtin_attr("rename")
425 .and_then(|attr| attr.get_as::<&str>())
426 .map(|s| (*s).to_string())
427 .unwrap_or_else(|| variant.name.to_kebab_case());
428
429 (name, variant.doc.first().copied())
430 });
431
432 format_two_column_list(items)
433}
434
435fn get_short_flag(field: &Field) -> Option<char> {
437 field
438 .get_attr(Some("args"), "short")
439 .and_then(|attr| attr.get_as::<crate::Attr>())
440 .and_then(|attr| {
441 if let crate::Attr::Short(c) = attr {
442 c.or_else(|| field.name.chars().next())
444 } else {
445 None
446 }
447 })
448}
449
450fn find_similar_flag(input: &str, fields: &'static [Field]) -> Option<String> {
452 for field in fields {
453 let kebab = field.name.to_kebab_case();
454 if is_similar(input, &kebab) {
455 return Some(kebab);
456 }
457 }
458 None
459}
460
461fn find_similar_subcommand(input: &str, variants: &'static [Variant]) -> Option<String> {
463 for variant in variants {
464 let name = variant
466 .get_builtin_attr("rename")
467 .and_then(|attr| attr.get_as::<&str>())
468 .map(|s| (*s).to_string())
469 .unwrap_or_else(|| variant.name.to_kebab_case());
470 if is_similar(input, &name) {
471 return Some(name);
472 }
473 }
474 None
475}
476
477fn is_similar(a: &str, b: &str) -> bool {
479 if a == b {
480 return true;
481 }
482 let len_diff = (a.len() as isize - b.len() as isize).abs();
483 if len_diff > 2 {
484 return false;
485 }
486
487 let mut diffs = 0;
489 let a_chars: Vec<char> = a.chars().collect();
490 let b_chars: Vec<char> = b.chars().collect();
491
492 for (ac, bc) in a_chars.iter().zip(b_chars.iter()) {
493 if ac != bc {
494 diffs += 1;
495 }
496 }
497 diffs += len_diff as usize;
498 diffs <= 2
499}
500
501fn unwrap_option_type(shape: &'static Shape) -> &'static str {
503 match shape.def {
504 facet_core::Def::Option(opt_def) => opt_def.t.type_identifier,
505 _ => shape.type_identifier,
506 }
507}
508
509fn format_reflect_error(err: &ReflectError) -> String {
511 use facet_reflect::ReflectError::*;
512 match err {
513 OperationFailed { shape, operation } => {
514 let inner_type = unwrap_option_type(shape);
517
518 if operation.starts_with("Subcommands must be provided") {
520 return operation.to_string();
521 }
522
523 match *operation {
524 "Type does not support parsing from string" => {
525 format!("`{inner_type}` cannot be parsed from a string value")
526 }
527 "Failed to parse string value" => {
528 format!("invalid value for `{inner_type}`")
529 }
530 _ => format!("`{inner_type}`: {operation}"),
531 }
532 }
533 UninitializedField { shape, field_name } => {
534 format!(
535 "field `{}` of `{}` was not provided",
536 field_name, shape.type_identifier
537 )
538 }
539 WrongShape { expected, actual } => {
540 format!(
541 "expected `{}`, got `{}`",
542 expected.type_identifier, actual.type_identifier
543 )
544 }
545 _ => format!("{err}"),
546 }
547}
548
549impl core::fmt::Display for ArgsErrorKind {
550 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
551 write!(f, "{}", self.label())
552 }
553}
554
555impl From<ReflectError> for ArgsErrorKind {
556 fn from(error: ReflectError) -> Self {
557 ArgsErrorKind::ReflectError(error)
558 }
559}
560
561impl ArgsError {
562 pub fn new(kind: ArgsErrorKind, span: Span) -> Self {
564 Self { span, kind }
565 }
566}
567
568impl fmt::Display for ArgsError {
569 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570 fmt::Debug::fmt(self, f)
571 }
572}
573
574pub(crate) fn get_variants_from_shape(shape: &'static Shape) -> &'static [Variant] {
576 if let Type::User(UserType::Enum(enum_type)) = shape.ty {
577 enum_type.variants
578 } else {
579 &[]
580 }
581}