Skip to content

Commit 1b833d5

Browse files
branchseerclaude
andcommitted
refactor: add clean() methods to AbsolutePath/RelativePath, replace direct path_clean usage
Move path_clean dependency into vite_path and expose it through typed clean() methods on AbsolutePath and RelativePath, documenting the symlink limitation of purely lexical normalization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f3440e4 commit 1b833d5

11 files changed

Lines changed: 49 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 4 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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ publish = false
88
rust-version.workspace = true
99

1010
[dependencies]
11-
path-clean = { workspace = true }
1211
rustc-hash = { workspace = true }
1312
thiserror = { workspace = true }
1413
vite_path = { workspace = true }

crates/vite_path/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rust-version.workspace = true
99
[dependencies]
1010
bincode = { workspace = true }
1111
diff-struct = { workspace = true }
12+
path-clean = { workspace = true }
1213
ref-cast = { workspace = true }
1314
serde = { workspace = true, features = ["derive", "rc"] }
1415
thiserror = { workspace = true }

crates/vite_path/src/absolute/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,24 @@ impl AbsolutePath {
200200
pub fn ends_with<P: AsRef<Path>>(&self, path: P) -> bool {
201201
self.0.ends_with(path.as_ref())
202202
}
203+
204+
/// Lexically normalizes the path by resolving `.` and `..` components
205+
/// without accessing the filesystem.
206+
///
207+
/// **Symlink limitation**: Because this is purely lexical, it can produce
208+
/// incorrect results when symlinks are involved. For example, if
209+
/// `/a/link` is a symlink to `/x/y`, then cleaning `/a/link/../c`
210+
/// yields `/a/c` instead of the correct `/x/c`. Use
211+
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
212+
#[must_use]
213+
pub fn clean(&self) -> AbsolutePathBuf {
214+
use path_clean::PathClean as _;
215+
216+
let cleaned = self.0.clean();
217+
// SAFETY: Lexical cleaning of an absolute path preserves absoluteness —
218+
// it only removes `.`/`..` components and redundant separators.
219+
unsafe { AbsolutePathBuf::assume_absolute(cleaned) }
220+
}
203221
}
204222

205223
/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`

crates/vite_path/src/relative.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ impl RelativePath {
6262
relative_path_buf
6363
}
6464

65+
/// Lexically normalizes the path by resolving `..` components without
66+
/// accessing the filesystem. (`.` components are already stripped by
67+
/// [`RelativePathBuf::new`].)
68+
///
69+
/// **Symlink limitation**: Because this is purely lexical, it can produce
70+
/// incorrect results when symlinks are involved. For example, if
71+
/// `a/link` is a symlink to `x/y`, then cleaning `a/link/../c`
72+
/// yields `a/c` instead of the correct `x/c`. Use
73+
/// [`std::fs::canonicalize`] when you need symlink-correct resolution.
74+
///
75+
/// # Panics
76+
///
77+
/// Panics if the cleaned path is no longer a valid relative path, which
78+
/// should never happen in practice.
79+
#[must_use]
80+
pub fn clean(&self) -> RelativePathBuf {
81+
use path_clean::PathClean as _;
82+
83+
let cleaned = self.as_path().clean();
84+
RelativePathBuf::new(cleaned).expect("cleaning a relative path preserves relativity")
85+
}
86+
6587
/// Returns a path that, when joined onto `base`, yields `self`.
6688
///
6789
/// If `base` is not a prefix of `self`, returns [`None`].

crates/vite_task/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ fspy = { workspace = true }
2323
futures-util = { workspace = true }
2424
once_cell = { workspace = true }
2525
owo-colors = { workspace = true }
26-
path-clean = { workspace = true }
2726
pty_terminal_test_client = { workspace = true }
2827
rayon = { workspace = true }
2928
rusqlite = { workspace = true, features = ["bundled"] }

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,8 @@ pub async fn spawn_with_tracking(
210210
// like `packages/sub-pkg/../shared/dist/output.js` that won't match
211211
// workspace-root-relative negative globs without normalization.
212212
if !resolved_negatives.is_empty() {
213-
let cleaned = path_clean::PathClean::clean(relative.as_path());
214-
if let Some(cleaned_str) = cleaned.to_str()
215-
&& resolved_negatives.iter().any(|neg| neg.is_match(cleaned_str))
216-
{
213+
let cleaned = relative.clean();
214+
if resolved_negatives.iter().any(|neg| neg.is_match(cleaned.as_str())) {
217215
return None;
218216
}
219217
}

crates/vite_task_graph/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ petgraph = { workspace = true }
1616
rustc-hash = { workspace = true }
1717
serde = { workspace = true, features = ["derive"] }
1818
serde_json = { workspace = true }
19-
path-clean = { workspace = true }
2019
thiserror = { workspace = true }
2120
tracing = { workspace = true }
22-
wax = { workspace = true }
2321
vite_graph_ser = { workspace = true }
2422
vite_path = { workspace = true }
2523
vite_str = { workspace = true }
2624
vite_workspace = { workspace = true }
25+
wax = { workspace = true }
2726

2827
[dev-dependencies]
2928
pretty_assertions = { workspace = true }

crates/vite_task_graph/src/config/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,15 @@ fn resolve_glob_to_workspace_relative(
195195
workspace_root: &AbsolutePath,
196196
) -> Result<Str, ResolveTaskConfigError> {
197197
use cow_utils::CowUtils as _;
198-
use path_clean::PathClean as _;
199198

200199
let glob = wax::Glob::new(pattern).map_err(|source| ResolveTaskConfigError::InvalidGlob {
201200
pattern: Str::from(pattern),
202201
source: Box::new(source),
203202
})?;
204203
let (invariant_prefix, variant) = glob.partition();
205204

206-
let joined = package_dir.as_path().join(&invariant_prefix).clean();
207-
let stripped = joined.strip_prefix(workspace_root.as_path()).map_err(|_| {
205+
let joined = package_dir.join(&invariant_prefix).clean();
206+
let stripped = joined.as_path().strip_prefix(workspace_root.as_path()).map_err(|_| {
208207
ResolveTaskConfigError::GlobOutsideWorkspace { pattern: Str::from(pattern) }
209208
})?;
210209

@@ -383,7 +382,6 @@ mod tests {
383382

384383
use super::*;
385384

386-
#[expect(clippy::disallowed_types, reason = "PathBuf needed for AbsolutePathBuf::new in tests")]
387385
fn test_paths() -> (AbsolutePathBuf, AbsolutePathBuf) {
388386
if cfg!(windows) {
389387
(

crates/vite_workspace/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ rust-version.workspace = true
99

1010
[dependencies]
1111
clap = { workspace = true, features = ["derive"] }
12-
path-clean = { workspace = true }
1312
petgraph = { workspace = true, features = ["serde-1"] }
1413
rustc-hash = { workspace = true }
1514
serde = { workspace = true, features = ["derive"] }

0 commit comments

Comments
 (0)