Skip to content

Commit 453e44e

Browse files
branchseerclaude
andcommitted
refactor: extract AnchoredGlob into vite_glob crate
Replace the ResolvedNegativeGlob tuple type alias with a proper AnchoredGlob struct that encapsulates glob partitioning, path cleaning, and prefix-based matching behind a clean API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a285687 commit 453e44e

8 files changed

Lines changed: 91 additions & 54 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_glob/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ publish = false
88
rust-version.workspace = true
99

1010
[dependencies]
11+
path-clean = { workspace = true }
1112
thiserror = { workspace = true }
13+
vite_path = { workspace = true }
1214
wax = { workspace = true }
1315

1416
[dev-dependencies]

crates/vite_glob/src/anchored.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::sync::Arc;
2+
3+
use vite_path::AbsolutePath;
4+
use wax::Glob;
5+
6+
use crate::Error;
7+
8+
/// A glob pattern anchored to an absolute directory.
9+
///
10+
/// Created by partitioning a glob into an invariant prefix and a variant (dynamic)
11+
/// part, then resolving the prefix against a base directory. The prefix is cleaned
12+
/// to normalize `..` components.
13+
///
14+
/// For example, `../shared/dist/**` relative to `/ws/packages/app` produces:
15+
/// - `prefix`: `/ws/packages/shared/dist`
16+
/// - `variant`: `Some(Glob("**"))`
17+
#[derive(Debug)]
18+
pub struct AnchoredGlob {
19+
prefix: Arc<AbsolutePath>,
20+
variant: Option<Glob<'static>>,
21+
}
22+
23+
impl AnchoredGlob {
24+
/// Create an `AnchoredGlob` by resolving `pattern` relative to `base_dir`.
25+
///
26+
/// The pattern is partitioned into an invariant prefix and a variant glob.
27+
/// The prefix is joined with `base_dir` and cleaned (normalizing `..`).
28+
///
29+
/// # Errors
30+
///
31+
/// Returns an error if the glob pattern is invalid.
32+
///
33+
/// # Panics
34+
///
35+
/// Panics if cleaning an absolute path somehow produces a non-absolute path.
36+
pub fn new(pattern: &str, base_dir: &AbsolutePath) -> Result<Self, Error> {
37+
use path_clean::PathClean as _;
38+
39+
let glob = Glob::new(pattern)?;
40+
let (prefix_path, variant) = glob.partition();
41+
let cleaned = base_dir.as_path().join(&prefix_path).clean();
42+
// Cleaning an absolute path always produces an absolute path
43+
let prefix = Arc::<AbsolutePath>::from(
44+
vite_path::AbsolutePathBuf::new(cleaned)
45+
.expect("cleaning an absolute path produces an absolute path"),
46+
);
47+
Ok(Self { prefix, variant: variant.map(Glob::into_owned) })
48+
}
49+
50+
/// Check if an absolute path matches this anchored glob.
51+
#[must_use]
52+
pub fn is_match(&self, path: &AbsolutePath) -> bool {
53+
use wax::Program as _;
54+
let Ok(remainder) = path.as_path().strip_prefix(self.prefix.as_path()) else {
55+
return false;
56+
};
57+
let Some(v) = &self.variant else {
58+
return remainder.as_os_str().is_empty();
59+
};
60+
v.is_match(remainder)
61+
}
62+
}

crates/vite_glob/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
mod anchored;
12
mod error;
23

34
#[expect(clippy::disallowed_types, reason = "wax::Glob::is_match requires std::path::Path")]
45
use std::path::Path;
56

7+
pub use anchored::AnchoredGlob;
68
pub use error::Error;
79
use wax::{Glob, Program};
810

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ thiserror = { workspace = true }
3434
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "io-util", "macros", "sync"] }
3535
tracing = { workspace = true }
3636
twox-hash = { workspace = true }
37+
vite_glob = { workspace = true }
3738
vite_path = { workspace = true }
3839
vite_select = { workspace = true }
3940
vite_str = { workspace = true }

crates/vite_task/src/session/execute/glob_inputs.rs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ use std::{
1010
io::{self, Read},
1111
};
1212

13+
use vite_glob::AnchoredGlob;
1314
#[cfg(test)]
1415
use vite_path::AbsolutePathBuf;
1516
use vite_path::{AbsolutePath, RelativePathBuf};
1617
use vite_str::Str;
1718
use wax::{Glob, walk::Entry as _};
1819

19-
use super::spawn::ResolvedNegativeGlob;
20-
2120
/// Collect walk entries into the result map, filtering against resolved negatives.
2221
///
2322
/// Each positive glob is partitioned into an invariant prefix and a variant pattern.
@@ -28,11 +27,10 @@ use super::spawn::ResolvedNegativeGlob;
2827
fn collect_walk_entries(
2928
walk: impl Iterator<Item = Result<wax::walk::GlobEntry, wax::walk::WalkError>>,
3029
workspace_root: &AbsolutePath,
31-
resolved_negatives: &[ResolvedNegativeGlob],
30+
resolved_negatives: &[AnchoredGlob],
3231
result: &mut BTreeMap<RelativePathBuf, u64>,
3332
) -> anyhow::Result<()> {
3433
use path_clean::PathClean as _;
35-
use wax::Program as _;
3634

3735
for entry in walk {
3836
let entry = match entry {
@@ -53,21 +51,18 @@ fn collect_walk_entries(
5351
// Clean the path to normalize `..` components (from globs like `../shared/src/**`)
5452
let cleaned_path = entry.path().clean();
5553

54+
// Convert to AbsolutePath for negative matching and workspace-relative stripping
55+
let Some(cleaned_abs) = AbsolutePath::new(&cleaned_path) else {
56+
continue;
57+
};
58+
5659
// Filter against resolved negatives
57-
if resolved_negatives.iter().any(|(prefix, variant)| {
58-
let Ok(remainder) = cleaned_path.strip_prefix(prefix) else {
59-
return false;
60-
};
61-
variant.as_ref().map_or(remainder.as_os_str().is_empty(), |v| v.is_match(remainder))
62-
}) {
60+
if resolved_negatives.iter().any(|neg| neg.is_match(cleaned_abs)) {
6361
continue;
6462
}
6563

6664
// Compute path relative to workspace_root for the result
67-
let Some(relative_to_workspace) = cleaned_path
68-
.strip_prefix(workspace_root.as_path())
69-
.ok()
70-
.and_then(|p| RelativePathBuf::new(p).ok())
65+
let Some(relative_to_workspace) = cleaned_abs.strip_prefix(workspace_root).ok().flatten()
7166
else {
7267
continue; // Skip if path is outside workspace_root
7368
};
@@ -116,15 +111,9 @@ pub fn compute_globbed_inputs(
116111
return Ok(BTreeMap::new());
117112
}
118113

119-
// Resolve negatives: partition + clean to get (absolute_prefix, variant)
120-
let resolved_negatives: Vec<ResolvedNegativeGlob> = negative_globs
114+
let resolved_negatives: Vec<AnchoredGlob> = negative_globs
121115
.iter()
122-
.map(|p| {
123-
let glob = Glob::new(p.as_str())?.into_owned();
124-
let (prefix, variant) = glob.partition();
125-
let resolved = base_dir.as_path().join(&prefix).clean();
126-
Ok((resolved, variant.map(Glob::into_owned)))
127-
})
116+
.map(|p| Ok(AnchoredGlob::new(p.as_str(), base_dir)?))
128117
.collect::<anyhow::Result<_>>()?;
129118

130119
let mut result = BTreeMap::new();

crates/vite_task/src/session/execute/mod.rs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use vite_task_plan::{
1515
use self::{
1616
fingerprint::PostRunFingerprint,
1717
glob_inputs::compute_globbed_inputs,
18-
spawn::{ResolvedNegativeGlob, SpawnResult, TrackedPathAccesses, spawn_with_tracking},
18+
spawn::{SpawnResult, TrackedPathAccesses, spawn_with_tracking},
1919
};
2020
use super::{
2121
cache::{CacheEntryValue, ExecutionCache},
@@ -422,25 +422,14 @@ pub async fn execute_spawn(
422422
SpawnOutcome::Spawned(result.exit_status)
423423
}
424424

425-
/// Resolve negative glob patterns into absolute prefix + optional variant for fspy path filtering.
426-
///
427-
/// Each negative glob is partitioned into an invariant prefix and a variant (dynamic) part.
428-
/// The prefix is joined with `glob_base` and cleaned to produce an absolute path for efficient
429-
/// prefix-based filtering in `spawn_with_tracking`.
425+
/// Resolve negative glob patterns into [`AnchoredGlob`]s for filtering.
430426
fn resolve_negative_globs(
431427
glob_base: &AbsolutePath,
432428
negative_globs: &std::collections::BTreeSet<vite_str::Str>,
433-
) -> anyhow::Result<Vec<ResolvedNegativeGlob>> {
434-
use path_clean::PathClean as _;
435-
429+
) -> anyhow::Result<Vec<vite_glob::AnchoredGlob>> {
436430
negative_globs
437431
.iter()
438-
.map(|p| {
439-
let glob = wax::Glob::new(p.as_str())?.into_owned();
440-
let (prefix, variant) = glob.partition();
441-
let resolved = glob_base.as_path().join(&prefix).clean();
442-
Ok((resolved, variant.map(wax::Glob::into_owned)))
443-
})
432+
.map(|p| Ok(vite_glob::AnchoredGlob::new(p.as_str(), glob_base)?))
444433
.collect()
445434
}
446435

crates/vite_task/src/session/execute/spawn.rs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use fspy::AccessMode;
1111
use rustc_hash::FxHashSet;
1212
use serde::Serialize;
1313
use tokio::io::{AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
14+
use vite_glob::AnchoredGlob;
1415
use vite_path::{AbsolutePath, RelativePathBuf};
1516
use vite_task_plan::SpawnCommand;
1617

@@ -54,12 +55,6 @@ pub struct TrackedPathAccesses {
5455
pub path_writes: FxHashSet<RelativePathBuf>,
5556
}
5657

57-
/// Resolved negative glob pattern for filtering fspy-tracked paths.
58-
/// The `PathBuf` is the resolved absolute prefix (glob base + invariant prefix, cleaned).
59-
/// The `Option<Glob>` is the variant (dynamic) part of the pattern, if any.
60-
#[expect(clippy::disallowed_types, reason = "wax partition returns std::path::PathBuf")]
61-
pub type ResolvedNegativeGlob = (std::path::PathBuf, Option<wax::Glob<'static>>);
62-
6358
/// Spawn a command with optional file system tracking via fspy, using piped stdio.
6459
///
6560
/// Returns the execution result including exit status and duration.
@@ -82,7 +77,7 @@ pub async fn spawn_with_tracking(
8277
stderr_writer: &mut (dyn AsyncWrite + Unpin),
8378
std_outputs: Option<&mut Vec<StdOutput>>,
8479
path_accesses: Option<&mut TrackedPathAccesses>,
85-
resolved_negatives: &[ResolvedNegativeGlob],
80+
resolved_negatives: &[AnchoredGlob],
8681
) -> anyhow::Result<SpawnResult> {
8782
/// The tracking state of the spawned process.
8883
/// Determined by whether `path_accesses` is `Some` (fspy enabled) or `None` (fspy disabled).
@@ -217,19 +212,13 @@ pub async fn spawn_with_tracking(
217212

218213
// Filter against resolved negative globs.
219214
// Clean the path to normalize `..` only for matching purposes, since
220-
// resolved negatives are cleaned absolute paths.
215+
// AnchoredGlob prefixes are cleaned absolute paths.
221216
if !resolved_negatives.is_empty() {
222217
let cleaned_abs =
223218
path_clean::PathClean::clean(workspace_root.join(&relative_path).as_path());
224-
if resolved_negatives.iter().any(|(resolved_prefix, variant)| {
225-
let Ok(remainder) = cleaned_abs.strip_prefix(resolved_prefix) else {
226-
return false;
227-
};
228-
variant.as_ref().map_or(remainder.as_os_str().is_empty(), |v| {
229-
use wax::Program as _;
230-
v.is_match(remainder)
231-
})
232-
}) {
219+
if let Some(cleaned) = AbsolutePath::new(&cleaned_abs)
220+
&& resolved_negatives.iter().any(|neg| neg.is_match(cleaned))
221+
{
233222
continue;
234223
}
235224
}

0 commit comments

Comments
 (0)