Skip to main content

usage/
error.rs

1use crate::kdl;
2use crate::miette::{NamedSource, SourceSpan};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6#[error("invalid enum value `{0}`")]
7pub struct EnumParseError(pub String);
8
9/// Everything that can go wrong reading a spec or a command line against one.
10///
11/// `#[non_exhaustive]`, so a caller matching on it needs a `_` arm. That is the point:
12/// this enum grows every time the spec learns to say something new — `MissingGroup`
13/// arrived with groups, `ArgRequiresDoubleDash` with `double_dash` — and without this
14/// each one is a major release for everyone downstream.
15///
16/// With the `miette` feature enabled, this implements `miette::Diagnostic` so applications can
17/// pass it directly to their existing miette reporter. The feature is disabled by default.
18#[derive(Error, Debug)]
19#[non_exhaustive]
20pub enum UsageErr {
21    #[error("Invalid flag `{token}`: {reason}")]
22    InvalidFlag {
23        token: String,
24        reason: String,
25        span: SourceSpan,
26        input: String,
27    },
28
29    #[error("Missing required flag: --{0} <{0}>")]
30    MissingFlag(String),
31
32    #[error("Flag --{0} cannot be used multiple times")]
33    DuplicateFlag(String),
34
35    /// A required group had none of its members given.
36    ///
37    /// Its own variant rather than a [`UsageErr::MissingFlag`] holding a sentence,
38    /// because there is no one flag to name: the group is the thing that was not
39    /// satisfied, and a caller that renders errors itself needs the members as members.
40    #[error("Missing one of the required flags in group {group}: {members}")]
41    MissingGroup { group: String, members: String },
42
43    #[error("Invalid usage config")]
44    InvalidInput(String, SourceSpan, NamedSource<String>),
45
46    #[error("Invalid usage spec: {0}")]
47    InvalidSpec(String),
48
49    #[error("Missing required arg: <{0}>")]
50    MissingArg(String),
51
52    #[error("Missing required arg <{arg}> in clause {clause} instance {instance}")]
53    MissingClauseArg {
54        clause: String,
55        instance: usize,
56        arg: String,
57    },
58
59    /// A command that declares `subcommand_required` was given none.
60    ///
61    /// The spec could say this and the parser did not read it, so `mise generate` — which
62    /// declares it — parsed as though it were a complete invocation. usage-argv and clap both
63    /// refuse it.
64    #[error("`{0}` needs a subcommand: one of {1}")]
65    MissingSubcommand(String, String),
66
67    #[error("Argument <{0}> can only be set after a `--` separator")]
68    ArgRequiresDoubleDash(String),
69
70    #[error("{0}")]
71    Help(String),
72
73    #[error("{0}")]
74    Version(String),
75
76    #[error("Invalid usage config: {0}")]
77    Miette(#[from] crate::miette::MietteError),
78
79    #[error(transparent)]
80    IO(#[from] std::io::Error),
81
82    #[error(transparent)]
83    Strum(#[from] EnumParseError),
84
85    #[error(transparent)]
86    FromUtf8Error(#[from] std::string::FromUtf8Error),
87
88    #[cfg(feature = "tera")]
89    #[error(transparent)]
90    TeraError(#[from] tera::Error),
91
92    #[error(transparent)]
93    KdlError(#[from] kdl::KdlError),
94
95    /// A file the spec model was asked to read could not be read.
96    ///
97    /// Carries the path as well as the io error: "No such file or directory" on its own
98    /// names nothing, and this is reported for spec files given on a command line.
99    #[error("{0}\nFile: {1}")]
100    FileError(std::io::Error, std::path::PathBuf),
101
102    /// A `run=` script could not be run, exited non-zero, or produced output usage
103    /// could not read. The message names the shell and the script.
104    #[error("{0}")]
105    ShellError(String),
106
107    #[error("Variadic argument <{name}> requires at least {min} value(s), got {got}")]
108    VarArgTooFew {
109        name: String,
110        min: usize,
111        got: usize,
112    },
113
114    #[error("Variadic argument <{name}> accepts at most {max} value(s), got {got}")]
115    VarArgTooMany {
116        name: String,
117        max: usize,
118        got: usize,
119    },
120
121    #[error("Variadic flag --{name} requires at least {min} value(s), got {got}")]
122    VarFlagTooFew {
123        name: String,
124        min: usize,
125        got: usize,
126    },
127
128    #[error("Variadic flag --{name} accepts at most {max} value(s), got {got}")]
129    VarFlagTooMany {
130        name: String,
131        max: usize,
132        got: usize,
133    },
134
135    #[error("Invalid file path: {0}")]
136    InvalidPath(String),
137
138    #[error("Invalid spec view: {0}")]
139    InvalidView(String),
140
141    /// A command's `output`/`select` declarations do not agree with each other, or with
142    /// the flags around them. Spanless like [`UsageErr::InvalidView`], because selection
143    /// is resolved once the whole document is read — a `select` may name a flag declared
144    /// on an ancestor, so the node spans are long gone by the time it can be checked.
145    #[error("Invalid output declaration: {0}")]
146    InvalidOutput(String),
147
148    #[error("Invalid value for {name}: {value}: {reason}")]
149    InvalidValue {
150        name: String,
151        value: String,
152        reason: String,
153    },
154
155    #[error("Unsupported shell: {0}")]
156    UnsupportedShell(String),
157
158    #[error("No injected output was provided for mount command: {0}")]
159    MissingMountOutput(String),
160}
161pub type Result<T> = std::result::Result<T, UsageErr>;
162
163impl UsageErr {
164    fn code_name(&self) -> Option<&'static str> {
165        match self {
166            Self::FileError(..) => Some("usage::file"),
167            Self::ShellError(..) => Some("usage::shell"),
168            _ => None,
169        }
170    }
171
172    pub(crate) fn render(&self) -> String {
173        let rendered = match self {
174            Self::InvalidInput(message, span, source) => crate::miette::render_source(
175                "Invalid usage config",
176                source.name(),
177                source.inner(),
178                *span,
179                message,
180                None,
181            ),
182            Self::InvalidFlag {
183                reason,
184                span,
185                input,
186                ..
187            } => crate::miette::render_source(&self.to_string(), "", input, *span, reason, None),
188            Self::KdlError(error) => error.render(),
189            _ => self.to_string(),
190        };
191        if let Some(code) = self.code_name() {
192            format!("  {code}\n\n{rendered}")
193        } else {
194            rendered
195        }
196    }
197}
198
199#[cfg(feature = "miette")]
200impl ::miette::Diagnostic for UsageErr {
201    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
202        self.code_name()
203            .map(|code| Box::new(code) as Box<dyn std::fmt::Display>)
204    }
205
206    fn source_code(&self) -> Option<&dyn ::miette::SourceCode> {
207        match self {
208            Self::InvalidInput(_, _, source) => Some(source),
209            Self::InvalidFlag { input, .. } => Some(input),
210            _ => None,
211        }
212    }
213
214    fn labels(&self) -> Option<Box<dyn Iterator<Item = ::miette::LabeledSpan> + '_>> {
215        let (span, label) = match self {
216            Self::InvalidInput(message, span, _) => (*span, message.as_str()),
217            Self::InvalidFlag { reason, span, .. } => (*span, reason.as_str()),
218            _ => return None,
219        };
220        let label = ::miette::LabeledSpan::at(span.offset()..span.offset() + span.len(), label);
221        Some(Box::new(std::iter::once(label)))
222    }
223
224    fn related<'a>(
225        &'a self,
226    ) -> Option<Box<dyn Iterator<Item = &'a dyn ::miette::Diagnostic> + 'a>> {
227        match self {
228            Self::KdlError(error) => Some(Box::new(
229                error
230                    .diagnostics
231                    .iter()
232                    .map(|diagnostic| diagnostic as &dyn ::miette::Diagnostic),
233            )),
234            _ => None,
235        }
236    }
237}
238
239#[macro_export]
240macro_rules! bail_parse {
241    ($ctx:expr, $span:expr, $fmt:literal) => {{
242        let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
243        let msg = format!($fmt);
244        let err = $ctx.build_err(msg, span);
245        return std::result::Result::Err(err);
246    }};
247    ($ctx:expr, $span:expr, $fmt:literal, $($arg:tt)*) => {{
248        let span: $crate::miette::SourceSpan = ($span.offset(), $span.len()).into();
249        let msg = format!($fmt, $($arg)*);
250        let err = $ctx.build_err(msg, span);
251        return std::result::Result::Err(err);
252    }};
253}
254
255#[cfg(test)]
256mod tests {
257    use super::UsageErr;
258
259    #[test]
260    fn native_renderer_preserves_diagnostic_codes() {
261        let file = UsageErr::FileError(
262            std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
263            "missing.kdl".into(),
264        );
265        assert!(file.render().contains("usage::file"));
266        assert!(UsageErr::ShellError("failed".into())
267            .render()
268            .contains("usage::shell"));
269    }
270
271    #[cfg(feature = "miette")]
272    #[test]
273    fn miette_interop_preserves_diagnostic_codes() {
274        use miette::Diagnostic;
275
276        let error = UsageErr::ShellError("failed".into());
277        assert_eq!(error.code().unwrap().to_string(), "usage::shell");
278    }
279}