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