-
Notifications
You must be signed in to change notification settings - Fork 4
fix: normalize paths in native resolver for .js → .ts remap #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f3d83e
6e12745
5e40792
b4aad58
5b58273
ca6908d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,10 +10,33 @@ fn file_exists(path: &str, known: Option<&HashSet<String>>) -> bool { | |||||||||||||||||||||||||||||||||||||||||||||||
| known.map_or_else(|| Path::new(path).exists(), |set| set.contains(path)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Resolve `.` and `..` components in a path without touching the filesystem. | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// Unlike `PathBuf::components().collect()`, this properly collapses `..` by | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// popping the previous component from the result. | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// NOTE: if the path begins with more `..` components than there are preceding | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// components to pop (e.g. a purely relative `../../foo`), the excess `..` | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// components are silently dropped. This function is therefore only correct | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// when called on paths that have already been joined to a base directory with | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// sufficient depth. | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path(p: &Path) -> PathBuf { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let mut result = PathBuf::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| for c in p.components() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| match c { | ||||||||||||||||||||||||||||||||||||||||||||||||
| std::path::Component::ParentDir => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| result.pop(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| std::path::Component::CurDir => {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| _ => result.push(c), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| result | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When This diverges from
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed. Added the documented limitation to the |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Normalize a path to use forward slashes and clean `.` / `..` segments | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// (cross-platform consistency). | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn normalize_path(p: &str) -> String { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let cleaned: PathBuf = Path::new(p).components().collect(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| let cleaned = clean_path(Path::new(p)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| cleaned.display().to_string().replace('\\', "/") | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -111,7 +134,7 @@ fn resolve_import_path_inner( | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Relative import — normalize immediately to remove `.` / `..` segments | ||||||||||||||||||||||||||||||||||||||||||||||||
| let dir = Path::new(from_file).parent().unwrap_or(Path::new("")); | ||||||||||||||||||||||||||||||||||||||||||||||||
| let resolved: PathBuf = dir.join(import_source).components().collect(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| let resolved = clean_path(&dir.join(import_source)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| let resolved_str = resolved.display().to_string().replace('\\', "/"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // .js → .ts remap | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -246,3 +269,49 @@ pub fn resolve_imports_batch( | |||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .collect() | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||||||||||||||||||||||||||||||
| mod tests { | ||||||||||||||||||||||||||||||||||||||||||||||||
| use super::*; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path_collapses_parent_dirs() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_path(Path::new("src/cli/commands/../../domain/graph/builder.js")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| PathBuf::from("src/domain/graph/builder.js") | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path_skips_cur_dir() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_path(Path::new("src/./foo.ts")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| PathBuf::from("src/foo.ts") | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path_handles_absolute_root() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_path(Path::new("/src/../foo.ts")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| PathBuf::from("/foo.ts") | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path_mixed_segments() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_path(Path::new("a/b/../c/./d/../e.js")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| PathBuf::from("a/c/e.js") | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| fn clean_path_excess_parent_dirs_silently_dropped() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Documents the known limitation: excess leading `..` are dropped | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| clean_path(Path::new("../../foo")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| PathBuf::from("foo") | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clean_pathThe project has
#[cfg(test)]modules incomplexity.rs,cycles.rs, and several extractors, butimport_resolution.rshas none. Theclean_pathfunction contains path-normalization logic that has known edge-case subtleties worth pinning with tests — especially as a regression guard for the bug this PR fixes.At minimum, a
#[cfg(test)]block with cases like these would give confidence:Without a regression test for the
.js → .tsremap across../segments, the specific fix from issue #592 can silently break again in future refactors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed. Added a
#[cfg(test)]module with five unit tests covering: parent dir collapse, cur dir skip, absolute root, mixed segments, and the excess-parent-dir silent-drop edge case. Also un-skipped theresolves parent directory traversalparity test intests/resolution/parity.test.tsso the specific .js to .ts remap regression from #592 is covered in CI.