Skip to content

Commit 72beb68

Browse files
committed
Normalize .. and . in diagnostic file paths
1 parent 14196db commit 72beb68

11 files changed

Lines changed: 162 additions & 23 deletions

File tree

compiler/rustc_span/src/lib.rs

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ use std::fmt::Display;
7474
use std::hash::Hash;
7575
use std::io::{self, Read};
7676
use std::ops::{Add, Range, Sub};
77-
use std::path::{Path, PathBuf};
77+
use std::path::{Component, Path, PathBuf};
7878
use std::str::FromStr;
7979
use std::sync::Arc;
8080
use std::{fmt, iter};
@@ -495,11 +495,35 @@ impl RealFileName {
495495
.name
496496
.file_name()
497497
.map_or_else(|| "".into(), |f| f.to_string_lossy()),
498-
FileNameDisplayPreference::Scope(scope) => self.path(scope).to_string_lossy(),
498+
FileNameDisplayPreference::Scope(scope)
499+
| FileNameDisplayPreference::Diagnostics(scope) => self.path(scope).to_string_lossy(),
499500
}
500501
}
501502
}
502503

504+
/// Lexically normalizes a path by resolving `.` and `..` components without
505+
/// touching the filesystem. Preserves whether the path is relative or absolute.
506+
///
507+
/// Used for diagnostic display only. `Path::canonicalize` is intentionally avoided
508+
/// because it turns relative paths into absolute ones (see #51349, #83345).
509+
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
510+
let mut out = PathBuf::new();
511+
for component in path.components() {
512+
match component {
513+
Component::CurDir => {}
514+
Component::ParentDir => match out.components().next_back() {
515+
Some(Component::Normal(_)) => {
516+
out.pop();
517+
}
518+
Some(Component::RootDir) => {}
519+
_ => out.push(component),
520+
},
521+
_ => out.push(component),
522+
}
523+
}
524+
if out.as_os_str().is_empty() { PathBuf::from(".") } else { out }
525+
}
526+
503527
/// Differentiates between real files and common virtual files.
504528
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash, Decodable, Encodable)]
505529
pub enum FileName {
@@ -533,14 +557,36 @@ enum FileNameDisplayPreference {
533557
Local,
534558
Short,
535559
Scope(RemapPathScopeComponents),
560+
Diagnostics(RemapPathScopeComponents),
561+
}
562+
563+
impl<'a> FileNameDisplay<'a> {
564+
fn maybe_normalize<'s>(&self, s: Cow<'s, str>) -> Cow<'s, str> {
565+
if !matches!(self.display_pref, FileNameDisplayPreference::Diagnostics(_)) {
566+
return s;
567+
}
568+
let path = Path::new(s.as_ref());
569+
if !path.components().any(|c| matches!(c, Component::ParentDir | Component::CurDir)) {
570+
return s;
571+
}
572+
Cow::Owned(normalize_path(path).into_os_string().to_string_lossy().into_owned())
573+
}
574+
575+
pub fn to_string_lossy(&self) -> Cow<'a, str> {
576+
match self.inner {
577+
FileName::Real(inner) => self.maybe_normalize(inner.to_string_lossy(self.display_pref)),
578+
_ => Cow::from(self.to_string()),
579+
}
580+
}
536581
}
537582

538583
impl fmt::Display for FileNameDisplay<'_> {
539584
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540585
use FileName::*;
541586
match *self.inner {
542587
Real(ref name) => {
543-
write!(fmt, "{}", name.to_string_lossy(self.display_pref))
588+
let s = self.maybe_normalize(name.to_string_lossy(self.display_pref));
589+
write!(fmt, "{s}")
544590
}
545591
CfgSpec(_) => write!(fmt, "<cfgspec>"),
546592
MacroExpansion(_) => write!(fmt, "<macro expansion>"),
@@ -554,15 +600,6 @@ impl fmt::Display for FileNameDisplay<'_> {
554600
}
555601
}
556602

557-
impl<'a> FileNameDisplay<'a> {
558-
pub fn to_string_lossy(&self) -> Cow<'a, str> {
559-
match self.inner {
560-
FileName::Real(inner) => inner.to_string_lossy(self.display_pref),
561-
_ => Cow::from(self.to_string()),
562-
}
563-
}
564-
}
565-
566603
impl FileName {
567604
pub fn is_real(&self) -> bool {
568605
use FileName::*;
@@ -609,6 +646,12 @@ impl FileName {
609646
FileNameDisplay { inner: self, display_pref: FileNameDisplayPreference::Scope(scope) }
610647
}
611648

649+
/// Like `display`, but with `.` and `..` resolved lexically. See #51349.
650+
#[inline]
651+
pub fn display_normalized(&self, scope: RemapPathScopeComponents) -> FileNameDisplay<'_> {
652+
FileNameDisplay { inner: self, display_pref: FileNameDisplayPreference::Diagnostics(scope) }
653+
}
654+
612655
pub fn macro_expansion_source_code(src: &str) -> FileName {
613656
let mut hasher = StableHasher::new();
614657
src.hash(&mut hasher);

compiler/rustc_span/src/source_map.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ impl SourceMap {
487487
}
488488

489489
pub fn filename_for_diagnostics<'a>(&self, filename: &'a FileName) -> FileNameDisplay<'a> {
490-
filename.display(RemapPathScopeComponents::DIAGNOSTICS)
490+
filename.display_normalized(RemapPathScopeComponents::DIAGNOSTICS)
491491
}
492492

493493
pub fn is_multiline(&self, sp: Span) -> bool {

compiler/rustc_span/src/source_map/tests.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,64 @@ fn read_binary_file_handles_lying_stat() {
797797
let bin = RealFileLoader.read_binary_file(kernel_max).unwrap();
798798
assert_eq!(&real[..], &bin[..]);
799799
}
800+
801+
#[test]
802+
fn normalize_path_resolves_parent_dir() {
803+
assert_eq!(normalize_path(&path("a/b/../c.rs")), path("a/c.rs"));
804+
}
805+
806+
#[test]
807+
fn normalize_path_removes_cur_dir() {
808+
assert_eq!(normalize_path(&path("a/b/./c.rs")), path("a/b/c.rs"));
809+
}
810+
811+
#[test]
812+
fn normalize_path_mixed_components() {
813+
assert_eq!(normalize_path(&path("a/b/./../c.rs")), path("a/c.rs"));
814+
}
815+
816+
#[test]
817+
fn normalize_path_preserves_leading_parent_dir() {
818+
assert_eq!(normalize_path(&path("../../lib/foo.rs")), path("../../lib/foo.rs"));
819+
}
820+
821+
#[test]
822+
fn normalize_path_absolute() {
823+
assert_eq!(normalize_path(&path("/abs/a/../b.rs")), path("/abs/b.rs"));
824+
}
825+
826+
#[test]
827+
fn normalize_path_no_op() {
828+
assert_eq!(normalize_path(&path("a.rs")), path("a.rs"));
829+
}
830+
831+
#[test]
832+
fn normalize_path_dot_becomes_dot() {
833+
assert_eq!(normalize_path(Path::new(".")), Path::new("."));
834+
}
835+
836+
#[test]
837+
fn normalize_path_reduces_to_dot() {
838+
assert_eq!(normalize_path(&path("a/..")), Path::new("."));
839+
}
840+
841+
#[test]
842+
fn normalize_path_parent_beyond_normal() {
843+
assert_eq!(normalize_path(&path("a/../..")), path(".."));
844+
}
845+
846+
#[test]
847+
fn normalize_path_complex_backtrack() {
848+
assert_eq!(normalize_path(&path("a/../b/../../c")), path("../c"));
849+
}
850+
851+
#[test]
852+
fn normalize_path_parent_at_root_clamped() {
853+
// /.. is semantically / (can't go above root).
854+
assert_eq!(normalize_path(&path("/..")), path("/"));
855+
}
856+
857+
#[test]
858+
fn normalize_path_parent_at_root_with_tail() {
859+
assert_eq!(normalize_path(&path("/../a/b")), path("/a/b"));
860+
}

src/tools/compiletest/src/runtest.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,12 +2485,22 @@ impl<'test> TestCx<'test> {
24852485
let parent_dir = self.testpaths.file.parent().unwrap();
24862486
normalize_path(parent_dir, "$DIR");
24872487

2488+
// After #51349, rustc normalizes `tests/x/y/../aux/foo.rs` to
2489+
// `tests/x/aux/foo.rs`. Replace the grandparent with `$DIR/..` so
2490+
// stderrs keep the pre-normalization form.
2491+
if let Some(grandparent_dir) = parent_dir.parent() {
2492+
normalize_path(grandparent_dir, "$DIR/..");
2493+
}
2494+
24882495
if self.props.remap_src_base {
24892496
let mut remapped_parent_dir = Utf8PathBuf::from(FAKE_SRC_BASE);
24902497
if self.testpaths.relative_dir != Utf8Path::new("") {
24912498
remapped_parent_dir.push(&self.testpaths.relative_dir);
24922499
}
24932500
normalize_path(&remapped_parent_dir, "$DIR");
2501+
if let Some(remapped_grandparent) = remapped_parent_dir.parent() {
2502+
normalize_path(remapped_grandparent, "$DIR/..");
2503+
}
24942504
}
24952505

24962506
let base_dir = Utf8Path::new("/rustc/FAKE_PREFIX");

tests/ui/const-generics/generic_arg_infer/issue-91614.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ LL | let y = Mask::<_, _>::splat(false);
55
| ^ ------------ type must be known at this point
66
|
77
note: required by a const generic parameter in `Mask`
8-
--> $SRC_DIR/core/src/../../portable-simd/crates/core_simd/src/masks.rs:LL:COL
8+
--> $SRC_DIR/portable-simd/crates/core_simd/src/masks.rs:LL:COL
99
help: consider giving `y` an explicit type, where the value of const parameter `N` is specified
1010
|
1111
LL | let y: Mask<_, N> = Mask::<_, _>::splat(false);
@@ -18,7 +18,7 @@ LL | let y = Mask::<_, _>::splat(false);
1818
| ^ -------------------------- type must be known at this point
1919
|
2020
note: required by a const generic parameter in `Mask::<T, N>::splat`
21-
--> $SRC_DIR/core/src/../../portable-simd/crates/core_simd/src/masks.rs:LL:COL
21+
--> $SRC_DIR/portable-simd/crates/core_simd/src/masks.rs:LL:COL
2222
help: consider giving `y` an explicit type, where the value of const parameter `N` is specified
2323
|
2424
LL | let y: Mask<_, N> = Mask::<_, _>::splat(false);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub fn foo() -> u32 {
2+
"not a u32"
3+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#[path = "../helper.rs"]
2+
mod helper;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Verify that diagnostic file paths are lexically normalized.
2+
// Without the fix for #51349, the error location would show
3+
// `auxiliary/sub/../helper.rs` instead of `auxiliary/helper.rs`.
4+
#[path = "auxiliary/sub/mod.rs"]
5+
mod sub;
6+
7+
fn main() {}
8+
9+
//~? ERROR mismatched types
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error[E0308]: mismatched types
2+
--> $DIR/auxiliary/helper.rs:2:5
3+
|
4+
LL | pub fn foo() -> u32 {
5+
| --- expected `u32` because of return type
6+
LL | "not a u32"
7+
| ^^^^^^^^^^^ expected `u32`, found `&str`
8+
9+
error: aborting due to 1 previous error
10+
11+
For more information about this error, try `rustc --explain E0308`.

tests/ui/imports/ambiguous-2.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ LL | ambiguous_1::id();
66
|
77
= note: ambiguous because of multiple glob imports of a name in the same module
88
note: `id` could refer to the function defined here
9-
--> $DIR/auxiliary/../ambiguous-1.rs:13:13
9+
--> $DIR/ambiguous-1.rs:13:13
1010
|
1111
LL | pub use self::evp::*;
1212
| ^^^^^^^^^
1313
= help: consider updating this dependency to resolve this error
1414
= help: if updating the dependency does not resolve the problem report the problem to the author of the relevant crate
1515
note: `id` could also refer to the function defined here
16-
--> $DIR/auxiliary/../ambiguous-1.rs:15:13
16+
--> $DIR/ambiguous-1.rs:15:13
1717
|
1818
LL | pub use self::handwritten::*;
1919
| ^^^^^^^^^^^^^^^^^
@@ -32,14 +32,14 @@ LL | ambiguous_1::id();
3232
|
3333
= note: ambiguous because of multiple glob imports of a name in the same module
3434
note: `id` could refer to the function defined here
35-
--> $DIR/auxiliary/../ambiguous-1.rs:13:13
35+
--> $DIR/ambiguous-1.rs:13:13
3636
|
3737
LL | pub use self::evp::*;
3838
| ^^^^^^^^^
3939
= help: consider updating this dependency to resolve this error
4040
= help: if updating the dependency does not resolve the problem report the problem to the author of the relevant crate
4141
note: `id` could also refer to the function defined here
42-
--> $DIR/auxiliary/../ambiguous-1.rs:15:13
42+
--> $DIR/ambiguous-1.rs:15:13
4343
|
4444
LL | pub use self::handwritten::*;
4545
| ^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)