Skip to main content

nextest_filtering/
errors.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// rust nightly 2025-10-12 complains that "value assigned to `kind` is never
5// read", and this is the nearest location this works in. Maybe a miette issue?
6#![allow(unused_assignments)]
7
8use crate::expression::FiltersetKind;
9use miette::{Diagnostic, SourceSpan};
10use std::fmt;
11use thiserror::Error;
12
13/// A set of errors that occurred while parsing a filterset.
14#[derive(Clone, Debug)]
15#[non_exhaustive]
16pub struct FiltersetParseErrors {
17    /// The input string.
18    pub input: String,
19
20    /// The parse errors returned.
21    pub errors: Vec<ParseSingleError>,
22}
23
24impl FiltersetParseErrors {
25    pub(crate) fn new(input: impl Into<String>, errors: Vec<ParseSingleError>) -> Self {
26        Self {
27            input: input.into(),
28            errors,
29        }
30    }
31}
32
33/// An individual error that occurred while parsing a filterset.
34#[derive(Clone, Debug, Error, Diagnostic, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum ParseSingleError {
37    /// An invalid regex was encountered.
38    #[error("invalid regex")]
39    InvalidRegex {
40        /// The part of the input that failed.
41        #[label("{}", message)]
42        span: SourceSpan,
43
44        /// A message indicating the failure.
45        message: String,
46    },
47
48    /// An invalid glob pattern was encountered.
49    #[error("invalid glob")]
50    InvalidGlob {
51        /// The part of the input that failed.
52        #[label("{}", error)]
53        span: SourceSpan,
54
55        /// The underlying error.
56        error: GlobConstructError,
57    },
58
59    /// A banned predicate was encountered.
60    #[error("predicate not allowed in `{kind}` expressions")]
61    BannedPredicate {
62        /// The kind of expression.
63        kind: FiltersetKind,
64
65        /// The span of the banned predicate.
66        #[label("{reason}")]
67        span: SourceSpan,
68
69        /// The reason why the predicate is banned.
70        reason: BannedPredicateReason,
71    },
72
73    /// An invalid regex was encountered but we couldn't determine a better error message.
74    #[error("invalid regex")]
75    InvalidRegexWithoutMessage(#[label("invalid regex")] SourceSpan),
76
77    /// A regex string was not closed.
78    #[error("expected close regex")]
79    ExpectedCloseRegex(#[label("missing `/`")] SourceSpan),
80
81    /// An unexpected OR operator was found.
82    #[error("invalid OR operator")]
83    InvalidOrOperator(#[label("expected `|`, `+`, or `or`")] SourceSpan),
84
85    /// An unexpected AND operator was found.
86    #[error("invalid AND operator")]
87    InvalidAndOperator(#[label("expected `&` or `and`")] SourceSpan),
88
89    /// An unexpected argument was found.
90    #[error("unexpected argument")]
91    UnexpectedArgument(#[label("this set doesn't take an argument")] SourceSpan),
92
93    /// An unexpected comma was found.
94    #[error("unexpected comma")]
95    UnexpectedComma(#[label("this set doesn't take multiple arguments")] SourceSpan),
96
97    /// An invalid string was found.
98    #[error("invalid string")]
99    InvalidString(#[label("invalid string")] SourceSpan),
100
101    /// An open parenthesis `(` was expected but not found.
102    #[error("expected open parenthesis")]
103    ExpectedOpenParenthesis(#[label("missing `(`")] SourceSpan),
104
105    /// A close parenthesis `)` was expected but not found.
106    #[error("expected close parenthesis")]
107    ExpectedCloseParenthesis(#[label("missing `)`")] SourceSpan),
108
109    /// An invalid escape character was found.
110    #[error("invalid escape character")]
111    InvalidEscapeCharacter(#[label("invalid escape character")] SourceSpan),
112
113    /// An expression was expected in this position but not found.
114    #[error("expected expression")]
115    ExpectedExpr(#[label("missing expression")] SourceSpan),
116
117    /// A binary operator keyword or sigil was used in a position where an
118    /// expression was expected.
119    ///
120    /// Filterset binary operators are infix operators, not prefix ones. The
121    /// `suggest` field is the canonical recognized form of the operator, used
122    /// to nudge the user toward valid syntax in a single step: for `AND` it's
123    /// `and`, for `&&` it's `&`, and so on.
124    #[error("expected expression, found `{op}`")]
125    #[diagnostic(help("use `<expr> {suggest} <expr>` instead"))]
126    ExprFoundBinaryOp {
127        /// The matched operator, exactly as it appeared in the input.
128        op: &'static str,
129
130        /// The canonical recognized form to suggest in the help.
131        suggest: &'static str,
132
133        /// The span of the operator.
134        #[label("`{suggest}` is a binary operator")]
135        span: SourceSpan,
136    },
137
138    /// A unary-operator-shaped token was used in a syntax filtersets don't
139    /// recognize, e.g. uppercase `NOT` instead of `not`.
140    ///
141    /// The `suggest` field is the canonical recognized form.
142    #[error("expected expression, found `{op}`")]
143    #[diagnostic(help("use `{suggest} <expr>` instead"))]
144    ExprFoundUnaryOp {
145        /// The matched token, exactly as it appeared in the input.
146        op: &'static str,
147
148        /// The canonical recognized form to suggest in the help.
149        suggest: &'static str,
150
151        /// The span of the token.
152        #[label("`{op}` is not a recognized operator")]
153        span: SourceSpan,
154    },
155
156    /// The expression was expected to end here but some extra text was found.
157    #[error("expected end of expression")]
158    ExpectedEndOfExpression(#[label("unparsed input")] SourceSpan),
159
160    /// This matcher didn't match any packages.
161    #[error("operator didn't match any packages")]
162    NoPackageMatch(#[label("no packages matched this")] SourceSpan),
163
164    /// This matcher didn't match any test groups.
165    #[error("operator didn't match any test groups")]
166    NoGroupMatch(#[label("no test groups matched this")] SourceSpan),
167
168    /// This matcher didn't match any binary IDs.
169    #[error("operator didn't match any binary IDs")]
170    NoBinaryIdMatch(#[label("no binary IDs matched this")] SourceSpan),
171
172    /// This matcher didn't match any binary names.
173    #[error("operator didn't match any binary names")]
174    NoBinaryNameMatch(#[label("no binary names matched this")] SourceSpan),
175
176    /// Expected "host" or "target" for a `platform()` predicate.
177    #[error("invalid argument for platform")]
178    InvalidPlatformArgument(#[label("expected \"target\" or \"host\"")] SourceSpan),
179
180    /// Contained an unsupported expression.
181    #[error("unsupported expression")]
182    UnsupportedExpression(#[label("contained an unsupported expression")] SourceSpan),
183
184    /// An unknown parsing error occurred.
185    #[error("unknown parsing error")]
186    Unknown,
187}
188
189impl ParseSingleError {
190    pub(crate) fn invalid_regex(input: &str, start: usize, end: usize) -> Self {
191        // Use regex-syntax to parse the input so that we get better error messages.
192        match regex_syntax::Parser::new().parse(input) {
193            Ok(_) => {
194                // It is weird that a regex failed to parse with regex but succeeded with
195                // regex-syntax, but we can't do better.
196                Self::InvalidRegexWithoutMessage((start, end - start).into())
197            }
198            Err(err) => {
199                let (message, span) = match &err {
200                    regex_syntax::Error::Parse(err) => (format!("{}", err.kind()), err.span()),
201                    regex_syntax::Error::Translate(err) => (format!("{}", err.kind()), err.span()),
202                    _ => return Self::InvalidRegexWithoutMessage((start, end - start).into()),
203                };
204
205                // This isn't perfect because it doesn't account for "\/", but it'll do for now.
206                let err_start = start + span.start.offset;
207                let err_end = start + span.end.offset;
208
209                Self::InvalidRegex {
210                    span: (err_start, err_end - err_start).into(),
211                    message,
212                }
213            }
214        }
215    }
216}
217
218#[derive(Clone, Debug, Error, PartialEq, Eq)]
219pub enum GlobConstructError {
220    #[error("{}", .0.kind())]
221    InvalidGlob(globset::Error),
222
223    #[error("{}", .0)]
224    RegexError(String),
225}
226
227#[derive(Debug)]
228pub(crate) struct State<'a> {
229    // A `RefCell` is required here because the state must implement `Clone` to work with nom.
230    errors: &'a mut Vec<ParseSingleError>,
231}
232
233impl<'a> State<'a> {
234    pub fn new(errors: &'a mut Vec<ParseSingleError>) -> Self {
235        Self { errors }
236    }
237
238    pub fn report_error(&mut self, error: ParseSingleError) {
239        self.errors.push(error);
240    }
241}
242
243#[derive(Copy, Clone, Debug, PartialEq, Eq)]
244pub enum BannedPredicateReason {
245    /// `default()` causes infinite recursion in a default filter.
246    DefaultInfiniteRecursion,
247
248    /// `group()` creates a circular dependency in override filters, because
249    /// group membership is determined by overrides themselves.
250    GroupCircularDependency,
251
252    /// `group()` is not available in default-filter expressions.
253    GroupNotAvailableInDefaultFilter,
254
255    /// `group()` predicates are not supported while archiving.
256    GroupNotAvailableInArchive,
257
258    /// `test()` predicates are not supported while archiving.
259    TestNotAvailableInArchive,
260}
261
262impl fmt::Display for BannedPredicateReason {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        match self {
265            BannedPredicateReason::DefaultInfiniteRecursion => {
266                write!(f, "default() causes infinite recursion")
267            }
268            BannedPredicateReason::GroupCircularDependency => {
269                write!(f, "group() creates a circular dependency with overrides")
270            }
271            BannedPredicateReason::GroupNotAvailableInDefaultFilter => {
272                write!(f, "group() is not available in default-filter expressions")
273            }
274            BannedPredicateReason::GroupNotAvailableInArchive => {
275                write!(f, "group() predicates are not supported while archiving")
276            }
277            BannedPredicateReason::TestNotAvailableInArchive => {
278                write!(f, "test() predicates are not supported while archiving")
279            }
280        }
281    }
282}