1use crate::{
5 errors::{FiltersetParseErrors, ParseSingleError},
6 parsing::{
7 DisplayParsedRegex, DisplayParsedString, ExprResult, GenericGlob, ParsedExpr, SetDef,
8 new_span, parse,
9 },
10};
11use guppy::{
12 PackageId,
13 graph::{BuildTargetId, PackageGraph, PackageMetadata, cargo::BuildPlatform},
14};
15use miette::SourceSpan;
16use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
17use recursion::{Collapsible, CollapsibleExt, MappableFrame, PartiallyApplied};
18use smol_str::SmolStr;
19use std::{collections::HashSet, fmt, sync::OnceLock};
20
21#[derive(Debug, Clone)]
25pub enum NameMatcher {
26 Equal { value: String, implicit: bool },
28 Contains { value: String, implicit: bool },
30 Glob { glob: GenericGlob, implicit: bool },
32 Regex(regex::Regex),
34}
35
36impl NameMatcher {
37 pub(crate) fn implicit_equal(value: String) -> Self {
38 Self::Equal {
39 value,
40 implicit: true,
41 }
42 }
43
44 pub(crate) fn implicit_contains(value: String) -> Self {
45 Self::Contains {
46 value,
47 implicit: true,
48 }
49 }
50}
51
52impl PartialEq for NameMatcher {
53 fn eq(&self, other: &Self) -> bool {
54 match (self, other) {
55 (
56 Self::Contains {
57 value: s1,
58 implicit: default1,
59 },
60 Self::Contains {
61 value: s2,
62 implicit: default2,
63 },
64 ) => s1 == s2 && default1 == default2,
65 (
66 Self::Equal {
67 value: s1,
68 implicit: default1,
69 },
70 Self::Equal {
71 value: s2,
72 implicit: default2,
73 },
74 ) => s1 == s2 && default1 == default2,
75 (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(),
76 (Self::Glob { glob: g1, .. }, Self::Glob { glob: g2, .. }) => {
77 g1.regex().as_str() == g2.regex().as_str()
78 }
79 _ => false,
80 }
81 }
82}
83
84impl Eq for NameMatcher {}
85
86impl fmt::Display for NameMatcher {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::Equal { value, implicit } => write!(
90 f,
91 "{}{}",
92 if *implicit { "" } else { "=" },
93 DisplayParsedString(value)
94 ),
95 Self::Contains { value, implicit } => write!(
96 f,
97 "{}{}",
98 if *implicit { "" } else { "~" },
99 DisplayParsedString(value)
100 ),
101 Self::Glob { glob, implicit } => write!(
102 f,
103 "{}{}",
104 if *implicit { "" } else { "#" },
105 DisplayParsedString(glob.as_str())
106 ),
107 Self::Regex(r) => write!(f, "/{}/", DisplayParsedRegex(r)),
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum FiltersetLeaf {
115 Packages(HashSet<PackageId>),
117 Kind(NameMatcher, SourceSpan),
119 Platform(BuildPlatform, SourceSpan),
121 Binary(NameMatcher, SourceSpan),
123 BinaryId(NameMatcher, SourceSpan),
125 Test(NameMatcher, SourceSpan),
127 Default,
129 All,
131 None,
133}
134
135#[derive(Copy, Clone, Debug, Eq, PartialEq)]
137pub struct BinaryQuery<'a> {
138 pub package_id: &'a PackageId,
140
141 pub binary_id: &'a RustBinaryId,
143
144 pub binary_name: &'a str,
146
147 pub kind: &'a RustTestBinaryKind,
149
150 pub platform: BuildPlatform,
152}
153
154#[derive(Copy, Clone, Debug, Eq, PartialEq)]
156pub struct TestQuery<'a> {
157 pub binary_query: BinaryQuery<'a>,
159
160 pub test_name: &'a str,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct Filterset {
169 pub input: String,
171
172 pub parsed: ParsedExpr,
174
175 pub compiled: CompiledExpr,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum CompiledExpr {
181 Not(Box<CompiledExpr>),
183 Union(Box<CompiledExpr>, Box<CompiledExpr>),
185 Intersection(Box<CompiledExpr>, Box<CompiledExpr>),
187 Set(FiltersetLeaf),
189}
190
191impl CompiledExpr {
192 pub const ALL: Self = CompiledExpr::Set(FiltersetLeaf::All);
194
195 pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
202 use ExprFrame::*;
203 Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, Option<bool>>| {
204 match layer {
205 Set(set) => set.matches_binary(query, cx),
206 Not(a) => a.logic_not(),
207 Union(a, b) => a.logic_or(b),
209 Intersection(a, b) => a.logic_and(b),
210 Difference(a, b) => a.logic_and(b.logic_not()),
211 Parens(a) => a,
212 }
213 })
214 }
215
216 pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
218 use ExprFrame::*;
219 Wrapped(self).collapse_frames(|layer: ExprFrame<&FiltersetLeaf, bool>| match layer {
220 Set(set) => set.matches_test(query, cx),
221 Not(a) => !a,
222 Union(a, b) => a || b,
223 Intersection(a, b) => a && b,
224 Difference(a, b) => a && !b,
225 Parens(a) => a,
226 })
227 }
228}
229
230impl NameMatcher {
231 pub(crate) fn is_match(&self, input: &str) -> bool {
232 match self {
233 Self::Equal { value, .. } => value == input,
234 Self::Contains { value, .. } => input.contains(value),
235 Self::Glob { glob, .. } => glob.is_match(input),
236 Self::Regex(reg) => reg.is_match(input),
237 }
238 }
239}
240
241impl FiltersetLeaf {
242 fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext) -> bool {
243 match self {
244 Self::All => true,
245 Self::None => false,
246 Self::Default => cx.default_filter.matches_test(query, cx),
247 Self::Test(matcher, _) => matcher.is_match(query.test_name),
248 Self::Binary(matcher, _) => matcher.is_match(query.binary_query.binary_name),
249 Self::BinaryId(matcher, _) => matcher.is_match(query.binary_query.binary_id.as_str()),
250 Self::Platform(platform, _) => query.binary_query.platform == *platform,
251 Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind.as_str()),
252 Self::Packages(packages) => packages.contains(query.binary_query.package_id),
253 }
254 }
255
256 fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext) -> Option<bool> {
257 match self {
258 Self::All => Logic::top(),
259 Self::None => Logic::bottom(),
260 Self::Default => cx.default_filter.matches_binary(query, cx),
261 Self::Test(_, _) => None,
262 Self::Binary(matcher, _) => Some(matcher.is_match(query.binary_name)),
263 Self::BinaryId(matcher, _) => Some(matcher.is_match(query.binary_id.as_str())),
264 Self::Platform(platform, _) => Some(query.platform == *platform),
265 Self::Kind(matcher, _) => Some(matcher.is_match(query.kind.as_str())),
266 Self::Packages(packages) => Some(packages.contains(query.package_id)),
267 }
268 }
269}
270
271#[derive(Debug)]
273pub struct ParseContext<'g> {
274 graph: &'g PackageGraph,
276
277 cache: OnceLock<ParseContextCache<'g>>,
279}
280
281impl<'g> ParseContext<'g> {
282 #[inline]
284 pub fn new(graph: &'g PackageGraph) -> Self {
285 Self {
286 graph,
287 cache: OnceLock::new(),
288 }
289 }
290
291 #[inline]
293 pub fn graph(&self) -> &'g PackageGraph {
294 self.graph
295 }
296
297 pub(crate) fn make_cache(&self) -> &ParseContextCache<'g> {
298 self.cache
299 .get_or_init(|| ParseContextCache::new(self.graph))
300 }
301}
302
303#[derive(Debug)]
304pub(crate) struct ParseContextCache<'g> {
305 pub(crate) workspace_packages: Vec<PackageMetadata<'g>>,
306 pub(crate) binary_ids: HashSet<SmolStr>,
309 pub(crate) binary_names: HashSet<&'g str>,
310}
311
312impl<'g> ParseContextCache<'g> {
313 fn new(graph: &'g PackageGraph) -> Self {
314 let workspace_packages: Vec<_> = graph
315 .resolve_workspace()
316 .packages(guppy::graph::DependencyDirection::Forward)
317 .collect();
318 let (binary_ids, binary_names) = workspace_packages
319 .iter()
320 .flat_map(|pkg| {
321 pkg.build_targets().filter_map(|bt| {
322 let kind = compute_kind(&bt.id())?;
323 let binary_id = RustBinaryId::from_parts(pkg.name(), &kind, bt.name());
324 Some((SmolStr::new(binary_id.as_str()), bt.name()))
325 })
326 })
327 .unzip();
328
329 Self {
330 workspace_packages,
331 binary_ids,
332 binary_names,
333 }
334 }
335}
336
337fn compute_kind(id: &BuildTargetId<'_>) -> Option<RustTestBinaryKind> {
338 match id {
339 BuildTargetId::Library => Some(RustTestBinaryKind::LIB),
343 BuildTargetId::Benchmark(_) => Some(RustTestBinaryKind::BENCH),
344 BuildTargetId::Example(_) => Some(RustTestBinaryKind::EXAMPLE),
345 BuildTargetId::BuildScript => {
346 None
348 }
349 BuildTargetId::Binary(_) => Some(RustTestBinaryKind::BIN),
350 BuildTargetId::Test(_) => Some(RustTestBinaryKind::TEST),
351 _ => panic!("unknown build target id: {id:?}"),
352 }
353}
354
355#[derive(Copy, Clone, Debug, PartialEq, Eq)]
357pub enum FiltersetKind {
358 Test,
360
361 DefaultFilter,
366}
367
368impl fmt::Display for FiltersetKind {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 match self {
371 Self::Test => write!(f, "test"),
372 Self::DefaultFilter => write!(f, "default-filter"),
373 }
374 }
375}
376
377#[derive(Copy, Clone, Debug)]
379pub struct EvalContext<'a> {
380 pub default_filter: &'a CompiledExpr,
382}
383
384impl Filterset {
385 pub fn parse(
387 input: String,
388 cx: &ParseContext<'_>,
389 kind: FiltersetKind,
390 ) -> Result<Self, FiltersetParseErrors> {
391 let mut errors = Vec::new();
392 match parse(new_span(&input, &mut errors)) {
393 Ok(parsed_expr) => {
394 if !errors.is_empty() {
395 return Err(FiltersetParseErrors::new(input.clone(), errors));
396 }
397
398 match parsed_expr {
399 ExprResult::Valid(parsed) => {
400 let compiled = crate::compile::compile(&parsed, cx, kind)
401 .map_err(|errors| FiltersetParseErrors::new(input.clone(), errors))?;
402 Ok(Self {
403 input,
404 parsed,
405 compiled,
406 })
407 }
408 _ => {
409 Err(FiltersetParseErrors::new(
414 input,
415 vec![ParseSingleError::Unknown],
416 ))
417 }
418 }
419 }
420 Err(_) => {
421 Err(FiltersetParseErrors::new(
425 input,
426 vec![ParseSingleError::Unknown],
427 ))
428 }
429 }
430 }
431
432 pub fn matches_binary(&self, query: &BinaryQuery<'_>, cx: &EvalContext<'_>) -> Option<bool> {
439 self.compiled.matches_binary(query, cx)
440 }
441
442 pub fn matches_test(&self, query: &TestQuery<'_>, cx: &EvalContext<'_>) -> bool {
444 self.compiled.matches_test(query, cx)
445 }
446
447 pub fn needs_deps(raw_expr: &str) -> bool {
449 raw_expr.contains("deps")
451 }
452}
453
454trait Logic {
459 fn top() -> Self;
461
462 fn bottom() -> Self;
464
465 fn logic_and(self, other: Self) -> Self;
467
468 fn logic_or(self, other: Self) -> Self;
470
471 fn logic_not(self) -> Self;
473}
474
475impl Logic for bool {
477 #[inline]
478 fn top() -> Self {
479 true
480 }
481
482 #[inline]
483 fn bottom() -> Self {
484 false
485 }
486
487 #[inline]
488 fn logic_and(self, other: Self) -> Self {
489 self && other
490 }
491
492 #[inline]
493 fn logic_or(self, other: Self) -> Self {
494 self || other
495 }
496
497 #[inline]
498 fn logic_not(self) -> Self {
499 !self
500 }
501}
502
503impl Logic for Option<bool> {
508 #[inline]
509 fn top() -> Self {
510 Some(true)
511 }
512
513 #[inline]
514 fn bottom() -> Self {
515 Some(false)
516 }
517
518 #[inline]
519 fn logic_and(self, other: Self) -> Self {
520 match (self, other) {
521 (Some(false), _) | (_, Some(false)) => Some(false),
523 (Some(true), Some(true)) => Some(true),
525 _ => None,
527 }
528 }
529
530 #[inline]
531 fn logic_or(self, other: Self) -> Self {
532 match (self, other) {
533 (Some(true), _) | (_, Some(true)) => Some(true),
535 (Some(false), Some(false)) => Some(false),
537 _ => None,
539 }
540 }
541
542 #[inline]
543 fn logic_not(self) -> Self {
544 self.map(|v| !v)
545 }
546}
547
548pub(crate) enum ExprFrame<Set, A> {
549 Not(A),
550 Union(A, A),
551 Intersection(A, A),
552 Difference(A, A),
553 Parens(A),
554 Set(Set),
555}
556
557impl<Set> MappableFrame for ExprFrame<Set, PartiallyApplied> {
558 type Frame<Next> = ExprFrame<Set, Next>;
559
560 fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
561 use ExprFrame::*;
562 match input {
563 Not(a) => Not(f(a)),
564 Union(a, b) => {
568 let b = f(b);
569 let a = f(a);
570 Union(a, b)
571 }
572 Intersection(a, b) => {
573 let b = f(b);
574 let a = f(a);
575 Intersection(a, b)
576 }
577 Difference(a, b) => {
578 let b = f(b);
579 let a = f(a);
580 Difference(a, b)
581 }
582 Parens(a) => Parens(f(a)),
583 Set(f) => Set(f),
584 }
585 }
586}
587
588pub(crate) struct Wrapped<T>(pub(crate) T);
590
591impl<'a> Collapsible for Wrapped<&'a CompiledExpr> {
592 type FrameToken = ExprFrame<&'a FiltersetLeaf, PartiallyApplied>;
593
594 fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
595 match self.0 {
596 CompiledExpr::Not(a) => ExprFrame::Not(Wrapped(a.as_ref())),
597 CompiledExpr::Union(a, b) => ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref())),
598 CompiledExpr::Intersection(a, b) => {
599 ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
600 }
601 CompiledExpr::Set(f) => ExprFrame::Set(f),
602 }
603 }
604}
605
606impl<'a> Collapsible for Wrapped<&'a ParsedExpr> {
607 type FrameToken = ExprFrame<&'a SetDef, PartiallyApplied>;
608
609 fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
610 match self.0 {
611 ParsedExpr::Not(_, a) => ExprFrame::Not(Wrapped(a.as_ref())),
612 ParsedExpr::Union(_, a, b) => {
613 ExprFrame::Union(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
614 }
615 ParsedExpr::Intersection(_, a, b) => {
616 ExprFrame::Intersection(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
617 }
618 ParsedExpr::Difference(_, a, b) => {
619 ExprFrame::Difference(Wrapped(a.as_ref()), Wrapped(b.as_ref()))
620 }
621 ParsedExpr::Parens(a) => ExprFrame::Parens(Wrapped(a.as_ref())),
622 ParsedExpr::Set(f) => ExprFrame::Set(f),
623 }
624 }
625}