target_spec/
errors.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Errors returned by `target-spec`.
5
6use std::{borrow::Cow, error, fmt, string::FromUtf8Error};
7
8/// An error that happened during `target-spec` parsing or evaluation.
9#[derive(Clone, Debug)]
10#[non_exhaustive]
11pub enum Error {
12    /// A `cfg()` expression was invalid and could not be parsed.
13    InvalidExpression(ExpressionParseError),
14    /// The provided plain string (in the position that a `cfg()` expression would be) was unknown.
15    InvalidTargetSpecString(PlainStringParseError),
16    /// The provided platform triple was unknown.
17    UnknownPlatformTriple(TripleParseError),
18    /// Deprecated: this variant is no longer used.
19    #[deprecated(
20        since = "3.3.0",
21        note = "this variant is no longer returned: instead, use CustomPlatformCreate"
22    )]
23    #[doc(hidden)]
24    CustomTripleCreate(CustomTripleCreateError),
25    /// An error occurred while creating a custom platform.
26    CustomPlatformCreate(CustomTripleCreateError),
27    /// An error occurred while parsing `rustc -vV` output.
28    RustcVersionVerboseParse(RustcVersionVerboseParseError),
29}
30
31impl fmt::Display for Error {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Error::InvalidExpression(_) => write!(f, "invalid cfg() expression"),
35            Error::InvalidTargetSpecString(_) => {
36                write!(f, "failed to parse target spec as a plain string")
37            }
38            Error::UnknownPlatformTriple(_) => {
39                write!(f, "unknown platform triple")
40            }
41            #[allow(deprecated)]
42            Error::CustomTripleCreate(_) => write!(f, "error creating custom triple"),
43            Error::CustomPlatformCreate(_) => {
44                write!(f, "error creating custom platform")
45            }
46            Error::RustcVersionVerboseParse(_) => {
47                write!(f, "error parsing `rustc -vV` output")
48            }
49        }
50    }
51}
52
53impl error::Error for Error {
54    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
55        match self {
56            Error::InvalidExpression(err) => Some(err),
57            Error::InvalidTargetSpecString(err) => Some(err),
58            Error::UnknownPlatformTriple(err) => Some(err),
59            #[allow(deprecated)]
60            Error::CustomTripleCreate(err) => Some(err),
61            Error::CustomPlatformCreate(err) => Some(err),
62            Error::RustcVersionVerboseParse(err) => Some(err),
63        }
64    }
65}
66
67// Note: ExpressionParseError is a duplicate of cfg_expr::error::ParseError, and is copied here
68// because we don't want to expose that in a stable (1.0+) API.
69
70/// An error returned in case a `TargetExpression` cannot be parsed.
71#[derive(Clone, Debug, PartialEq, Eq)]
72#[non_exhaustive]
73pub struct ExpressionParseError {
74    /// The string we tried to parse.
75    pub input: String,
76
77    /// The range of characters in the original string that resulted
78    /// in this error.
79    pub span: std::ops::Range<usize>,
80
81    /// The kind of error that occurred.
82    pub kind: ExpressionParseErrorKind,
83}
84
85impl ExpressionParseError {
86    pub(crate) fn new(input: &str, error: cfg_expr::ParseError) -> Self {
87        // The error returned by cfg_expr::ParseError does not include the leading 'cfg('. Use the
88        // original input and add 4 which is the length of 'cfg('.
89        let span = if input.starts_with("cfg(") && input.ends_with(')') {
90            (error.span.start + 4)..(error.span.end + 4)
91        } else {
92            error.span
93        };
94        Self {
95            input: input.to_owned(),
96            span,
97            kind: ExpressionParseErrorKind::from_cfg_expr(error.reason),
98        }
99    }
100}
101
102impl fmt::Display for ExpressionParseError {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(f, "error parsing cfg() expression")
105    }
106}
107
108impl error::Error for ExpressionParseError {}
109
110/// The kind of [`ExpressionParseError`] that occurred.
111#[derive(Clone, Debug, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum ExpressionParseErrorKind {
114    /// not() takes exactly 1 predicate, unlike all() and any()
115    InvalidNot(usize),
116    /// The characters are not valid in an cfg expression
117    InvalidCharacters,
118    /// An opening parens was unmatched with a closing parens
119    UnclosedParens,
120    /// A closing parens was unmatched with an opening parens
121    UnopenedParens,
122    /// An opening quotes was unmatched with a closing quotes
123    UnclosedQuotes,
124    /// A closing quotes was unmatched with an opening quotes
125    UnopenedQuotes,
126    /// The expression does not contain any valid terms
127    Empty,
128    /// Found an unexpected term, which wasn't one of the expected terms that
129    /// is listed
130    Unexpected {
131        /// The list of expected terms.
132        expected: &'static [&'static str],
133    },
134    /// Failed to parse an integer value
135    InvalidInteger,
136    /// The root cfg() may only contain a single predicate
137    MultipleRootPredicates,
138    /// A `target_has_atomic` predicate didn't correctly parse.
139    InvalidHasAtomic,
140    /// An element was not part of the builtin information in rustc
141    UnknownBuiltin,
142}
143
144impl ExpressionParseErrorKind {
145    fn from_cfg_expr(reason: cfg_expr::error::Reason) -> Self {
146        use cfg_expr::error::Reason::*;
147
148        match reason {
149            InvalidCharacters => Self::InvalidCharacters,
150            UnclosedParens => Self::UnclosedParens,
151            UnopenedParens => Self::UnopenedParens,
152            UnclosedQuotes => Self::UnclosedQuotes,
153            UnopenedQuotes => Self::UnopenedQuotes,
154            Empty => Self::Empty,
155            Unexpected(expected) => Self::Unexpected { expected },
156            InvalidNot(np) => Self::InvalidNot(np),
157            InvalidInteger => Self::InvalidInteger,
158            MultipleRootPredicates => Self::MultipleRootPredicates,
159            InvalidHasAtomic => Self::InvalidHasAtomic,
160            UnknownBuiltin => Self::UnknownBuiltin,
161        }
162    }
163}
164
165impl fmt::Display for ExpressionParseErrorKind {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        use ExpressionParseErrorKind::*;
168
169        match self {
170            InvalidCharacters => f.write_str("invalid character(s)"),
171            UnclosedParens => f.write_str("unclosed parens"),
172            UnopenedParens => f.write_str("unopened parens"),
173            UnclosedQuotes => f.write_str("unclosed quotes"),
174            UnopenedQuotes => f.write_str("unopened quotes"),
175            Empty => f.write_str("empty expression"),
176            Unexpected { expected } => {
177                if expected.len() > 1 {
178                    f.write_str("expected one of ")?;
179
180                    for (i, exp) in expected.iter().enumerate() {
181                        f.write_fmt(format_args!("{}`{exp}`", if i > 0 { ", " } else { "" }))?;
182                    }
183                    f.write_str(" here")
184                } else if !expected.is_empty() {
185                    f.write_fmt(format_args!("expected a `{}` here", expected[0]))
186                } else {
187                    f.write_str("the term was not expected here")
188                }
189            }
190            InvalidNot(np) => f.write_fmt(format_args!("not() takes 1 predicate, found {np}")),
191            InvalidInteger => f.write_str("invalid integer"),
192            MultipleRootPredicates => f.write_str("multiple root predicates"),
193            InvalidHasAtomic => f.write_str("expected integer or \"ptr\""),
194            UnknownBuiltin => f.write_str("unknown built-in"),
195        }
196    }
197}
198
199/// An error that occurred while parsing a [`TargetSpecPlainString`](crate::TargetSpecPlainString).
200#[derive(Clone, Debug, PartialEq, Eq)]
201#[non_exhaustive]
202pub struct PlainStringParseError {
203    /// The input we failed to parse.
204    pub input: String,
205
206    /// The character index (in bytes) at which the input failed to parse.
207    pub char_index: usize,
208
209    /// The character that failed to parse.
210    pub character: char,
211}
212
213impl PlainStringParseError {
214    pub(crate) fn new(input: String, char_index: usize, character: char) -> Self {
215        Self {
216            input,
217            char_index,
218            character,
219        }
220    }
221
222    /// Returns the range of characters in the input that resulted in this error.
223    pub fn span(&self) -> std::ops::Range<usize> {
224        let end = self.char_index + self.character.len_utf8();
225        self.char_index..end
226    }
227}
228
229impl fmt::Display for PlainStringParseError {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(
232            f,
233            "failed to parse `{}` at index {}: character \
234             must be alphanumeric, `-`, `_` or `.`",
235            self.input, self.char_index,
236        )
237    }
238}
239
240impl error::Error for PlainStringParseError {
241    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
242        None
243    }
244}
245
246/// An error returned while parsing a single target.
247///
248/// This is produced when both of the following are true:
249///
250/// 1. The triple is not in the builtin set.
251/// 2. If heuristic parsing is enabled, it failed.
252#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct TripleParseError {
254    triple_str: Cow<'static, str>,
255    kind: TripleParseErrorKind,
256}
257
258impl TripleParseError {
259    pub(crate) fn new(
260        triple_str: Cow<'static, str>,
261        lexicon_err: cfg_expr::target_lexicon::ParseError,
262    ) -> Self {
263        Self {
264            triple_str,
265            kind: TripleParseErrorKind::Lexicon(lexicon_err),
266        }
267    }
268
269    pub(crate) fn new_strict(triple_str: Cow<'static, str>) -> Self {
270        Self {
271            triple_str,
272            kind: TripleParseErrorKind::LexiconDisabled,
273        }
274    }
275
276    /// Returns the triple string that could not be parsed.
277    pub fn triple_str(&self) -> &str {
278        &self.triple_str
279    }
280}
281
282impl fmt::Display for TripleParseError {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        write!(f, "unknown triple string: {}", self.triple_str)
285    }
286}
287
288impl error::Error for TripleParseError {
289    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
290        Some(&self.kind)
291    }
292}
293
294#[derive(Clone, Debug, PartialEq, Eq)]
295enum TripleParseErrorKind {
296    Lexicon(cfg_expr::target_lexicon::ParseError),
297    LexiconDisabled,
298}
299
300impl fmt::Display for TripleParseErrorKind {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        match self {
303            Self::Lexicon(_) => write!(
304                f,
305                "triple not in builtin platforms and heuristic parsing failed"
306            ),
307            Self::LexiconDisabled => write!(
308                f,
309                "triple not in builtin platforms and heuristic parsing disabled"
310            ),
311        }
312    }
313}
314
315impl error::Error for TripleParseErrorKind {
316    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
317        match self {
318            Self::Lexicon(error) => Some(error),
319            Self::LexiconDisabled => None,
320        }
321    }
322}
323
324/// An error returned while creating a custom platform.
325#[derive(Clone, Debug)]
326#[non_exhaustive]
327pub enum CustomTripleCreateError {
328    #[cfg(feature = "custom")]
329    /// Deprecated, and no longer used: instead, use [`Self::DeserializeJson`].
330    #[deprecated(
331        since = "3.3.0",
332        note = "this variant is no longer returned: instead, \
333                use DeserializeJson which also includes the input string"
334    )]
335    #[doc(hidden)]
336    Deserialize {
337        /// The specified triple.
338        triple: String,
339
340        /// The deserialization error that occurred.
341        error: std::sync::Arc<serde_json::Error>,
342    },
343
344    /// A custom platform was asked to be created, but the `custom` feature is currently disabled.
345    ///
346    /// Currently, this can only happen if a custom platform is deserialized from a
347    /// [`PlatformSummary`](crate::summaries::PlatformSummary),
348    Unavailable,
349
350    #[cfg(feature = "custom")]
351    /// An error occurred while deserializing serde data.
352    DeserializeJson {
353        /// The specified triple.
354        triple: String,
355
356        /// The input string that caused the error.
357        input: String,
358
359        /// The deserialization error that occurred.
360        error: std::sync::Arc<serde_json::Error>,
361    },
362}
363
364impl CustomTripleCreateError {
365    /// Returns the provided input that caused the error, if available.
366    #[inline]
367    pub fn input(&self) -> Option<&str> {
368        self.input_string().map(String::as_str)
369    }
370
371    /// A version of [`Self::input`] that returns a `&String` rather than a
372    /// `&str`.
373    ///
374    /// This is a workaround for a miette limitation -- `&str` can't be cast to
375    /// `&dyn SourceCode`, but `&String` can.
376    pub fn input_string(&self) -> Option<&String> {
377        match self {
378            #[cfg(feature = "custom")]
379            Self::DeserializeJson { input, .. } => Some(input),
380            #[cfg(feature = "custom")]
381            #[allow(deprecated)]
382            Self::Deserialize { .. } => None,
383            Self::Unavailable => None,
384        }
385    }
386
387    /// Returns the line and column number that caused the error, if available
388    /// and the error is not an I/O error.
389    ///
390    /// The line and column number are 1-based, though the column number can be
391    /// 0 if the error occurred between lines.
392    #[inline]
393    pub fn line_and_column(&self) -> Option<(usize, usize)> {
394        match self {
395            #[cfg(feature = "custom")]
396            Self::DeserializeJson { error, .. } => Some((error.line(), error.column())),
397            #[cfg(feature = "custom")]
398            #[allow(deprecated)]
399            Self::Deserialize { .. } => None,
400            Self::Unavailable => None,
401        }
402    }
403
404    /// Returns a label suitable for the error message to label at
405    /// [`Self::line_and_column`].
406    ///
407    /// This label drops line and column information if available.
408    pub fn label(&self) -> Option<String> {
409        match self {
410            #[cfg(feature = "custom")]
411            Self::DeserializeJson { error, .. } => {
412                let label = error.to_string();
413                // serde_json appends " at line M column N" -- remove it.
414                let trimmed = match label.rfind(" at line ") {
415                    Some(idx) => label[..idx].to_string(),
416                    None => label,
417                };
418                Some(trimmed)
419            }
420            #[cfg(feature = "custom")]
421            #[allow(deprecated)]
422            Self::Deserialize { .. } => None,
423            Self::Unavailable => None,
424        }
425    }
426}
427
428impl fmt::Display for CustomTripleCreateError {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        match self {
431            #[cfg(feature = "custom")]
432            #[allow(deprecated)]
433            Self::DeserializeJson { triple, .. } | Self::Deserialize { triple, .. } => {
434                write!(f, "error deserializing custom target JSON for `{triple}`")
435            }
436            Self::Unavailable => {
437                write!(
438                    f,
439                    "custom platforms are currently unavailable: \
440                     to enable them, add the `custom` feature to target-spec"
441                )
442            }
443        }
444    }
445}
446
447impl error::Error for CustomTripleCreateError {
448    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
449        match self {
450            #[cfg(feature = "custom")]
451            #[allow(deprecated)]
452            Self::DeserializeJson { error, .. } | Self::Deserialize { error, .. } => Some(error),
453            Self::Unavailable => None,
454        }
455    }
456}
457
458/// An error occurred while parsing `rustc -vV` output.
459///
460/// Returned by [`Platform::from_rustc_version_verbose`](crate::Platform::from_rustc_version_verbose).
461#[derive(Clone, Debug)]
462#[non_exhaustive]
463pub enum RustcVersionVerboseParseError {
464    /// The output was invalid UTF-8.
465    InvalidUtf8(FromUtf8Error),
466
467    /// The output did not contain a `host: ` line.
468    MissingHostLine {
469        /// The output that was parsed.
470        output: String,
471    },
472}
473
474impl fmt::Display for RustcVersionVerboseParseError {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        match self {
477            RustcVersionVerboseParseError::InvalidUtf8(_) => {
478                write!(f, "output from `rustc -vV` was not valid UTF-8")
479            }
480            RustcVersionVerboseParseError::MissingHostLine { output } => {
481                write!(
482                    f,
483                    "output from `rustc -vV` did not contain a `host: ` line; output:\n---\n{}---",
484                    output
485                )
486            }
487        }
488    }
489}
490
491impl error::Error for RustcVersionVerboseParseError {
492    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
493        match self {
494            RustcVersionVerboseParseError::InvalidUtf8(err) => Some(err),
495            RustcVersionVerboseParseError::MissingHostLine { .. } => None,
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use crate::{TargetSpecExpression, TargetSpecPlainString};
503    use test_case::test_case;
504
505    #[test_case("cfg()", 4..4; "empty expression results in span inside cfg")]
506    #[test_case("target_os = \"macos", 12..18; "unclosed quote specified without cfg")]
507    fn test_expression_parse_error_span(input: &str, expected_span: std::ops::Range<usize>) {
508        let err = TargetSpecExpression::new(input).unwrap_err();
509        assert_eq!(err.span, expected_span);
510    }
511
512    #[test_case("foobar$", 6..7; "dollar sign at end of string")]
513    #[test_case("my🛑triple", 2..6; "multibyte character")]
514    fn test_plain_string_parse_error_span(input: &str, expected_span: std::ops::Range<usize>) {
515        let err = TargetSpecPlainString::new(input.to_owned()).unwrap_err();
516        assert_eq!(err.span(), expected_span);
517    }
518}