diff --git a/compiler/rustc_builtin_macros/src/source_util.rs b/compiler/rustc_builtin_macros/src/source_util.rs index f0c0b8ea877e..aa6a1becd1f8 100644 --- a/compiler/rustc_builtin_macros/src/source_util.rs +++ b/compiler/rustc_builtin_macros/src/source_util.rs @@ -12,10 +12,11 @@ use rustc_expand::module::DirOwnership; use rustc_parse::new_parser_from_file; use rustc_parse::parser::{ForceCollect, Parser}; use rustc_session::lint::builtin::INCOMPLETE_INCLUDE; +use rustc_span::source_map::SourceMap; use rustc_span::symbol::Symbol; use rustc_span::{Pos, Span}; use smallvec::SmallVec; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; // These macros all relate to the file system; they either return @@ -185,7 +186,7 @@ pub fn expand_include_str( Ok(res) => res, Err(guar) => return DummyResult::any(sp, guar), }; - match load_binary_file(cx, path.as_str(), sp, path_span) { + match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) { Ok(bytes) => match std::str::from_utf8(&bytes) { Ok(src) => { let interned_src = Symbol::intern(src); @@ -210,7 +211,7 @@ pub fn expand_include_bytes( Ok(res) => res, Err(guar) => return DummyResult::any(sp, guar), }; - match load_binary_file(cx, path.as_str(), sp, path_span) { + match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) { Ok(bytes) => { let expr = cx.expr(sp, ast::ExprKind::IncludedBytes(bytes)); MacEager::expr(expr) @@ -221,7 +222,7 @@ pub fn expand_include_bytes( fn load_binary_file( cx: &mut ExtCtxt<'_>, - original_path: &str, + original_path: &Path, macro_span: Span, path_span: Span, ) -> Result, Box> { @@ -239,20 +240,22 @@ fn load_binary_file( macro_span, format!("couldn't read {}: {io_err}", resolved_path.display()), ); - if Path::new(original_path).is_relative() { - for prefix in ["..", "../.."] { - let parent_path = Path::new(prefix).join(original_path); - if resolve_path(&cx.sess, &parent_path, macro_span) - .map_or(false, |p| p.exists()) - { - err.span_suggestion( - path_span, - "it's in a parent directory", - format!("\"{}\"", parent_path.display().to_string().escape_debug()), - rustc_lint_defs::Applicability::MachineApplicable, - ); - break; - } + + if original_path.is_relative() { + let source_map = cx.sess.source_map(); + let new_path = source_map + .span_to_filename(macro_span.source_callsite()) + .into_local_path() + .and_then(|src| find_path_suggestion(source_map, src.parent()?, original_path)) + .and_then(|path| path.into_os_string().into_string().ok()); + + if let Some(new_path) = new_path { + err.span_suggestion( + path_span, + "there is a file in another directory", + format!("\"{}\"", new_path.escape_debug()), + rustc_lint_defs::Applicability::MachineApplicable, + ); } } let guar = err.emit(); @@ -260,3 +263,67 @@ fn load_binary_file( } } } + +fn find_path_suggestion( + source_map: &SourceMap, + base_dir: &Path, + wanted_path: &Path, +) -> Option { + // Fix paths that assume they're relative to cargo manifest dir + let mut base_c = base_dir.components(); + let mut wanted_c = wanted_path.components(); + let mut without_base = None; + while let Some(wanted_next) = wanted_c.next() { + if wanted_c.as_path().file_name().is_none() { + break; + } + // base_dir may be absolute + while let Some(base_next) = base_c.next() { + if base_next == wanted_next { + without_base = Some(wanted_c.as_path()); + break; + } + } + } + let root_absolute = without_base.into_iter().map(PathBuf::from); + + let base_dir_components = base_dir.components().count(); + // Avoid going all the way to the root dir + let max_parent_components = if base_dir.is_relative() { + base_dir_components + 1 + } else { + base_dir_components.saturating_sub(1) + }; + + // Try with additional leading ../ + let mut prefix = PathBuf::new(); + let add = std::iter::from_fn(|| { + prefix.push(".."); + Some(prefix.join(wanted_path)) + }) + .take(max_parent_components.min(3)); + + // Try without leading directories + let mut trimmed_path = wanted_path; + let remove = std::iter::from_fn(|| { + let mut components = trimmed_path.components(); + let removed = components.next()?; + trimmed_path = components.as_path(); + let _ = trimmed_path.file_name()?; // ensure there is a file name left + Some([ + Some(trimmed_path.to_path_buf()), + (removed != std::path::Component::ParentDir) + .then(|| Path::new("..").join(trimmed_path)), + ]) + }) + .flatten() + .flatten() + .take(4); + + for new_path in root_absolute.chain(add).chain(remove) { + if source_map.file_exists(&base_dir.join(&new_path)) { + return Some(new_path); + } + } + None +} diff --git a/tests/ui/include-macros/parent_dir.rs b/tests/ui/include-macros/parent_dir.rs index df2714e84411..c83972b9ddfc 100644 --- a/tests/ui/include-macros/parent_dir.rs +++ b/tests/ui/include-macros/parent_dir.rs @@ -1,4 +1,10 @@ fn main() { - let _ = include_str!("include-macros/file.txt"); //~ ERROR coudln't read - //~^HELP parent directory + let _ = include_str!("include-macros/file.txt"); //~ ERROR couldn't read + //~^HELP another directory + let _ = include_str!("hello.rs"); //~ ERROR couldn't read + //~^HELP another directory + let _ = include_bytes!("../../data.bin"); //~ ERROR couldn't read + //~^HELP another directory + let _ = include_str!("tests/ui/include-macros/file.txt"); //~ ERROR couldn't read + //~^HELP another directory } diff --git a/tests/ui/include-macros/parent_dir.stderr b/tests/ui/include-macros/parent_dir.stderr index 163a1d4f2419..f6a350d08640 100644 --- a/tests/ui/include-macros/parent_dir.stderr +++ b/tests/ui/include-macros/parent_dir.stderr @@ -4,9 +4,39 @@ error: couldn't read $DIR/include-macros/file.txt: No such file or directory (os LL | let _ = include_str!("include-macros/file.txt"); | ^^^^^^^^^^^^^-------------------------^ | | - | help: it's in a parent directory: `"../include-macros/file.txt"` + | help: there is a file in another directory: `"file.txt"` | = note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info) -error: aborting due to 1 previous error +error: couldn't read $DIR/hello.rs: No such file or directory (os error 2) + --> $DIR/parent_dir.rs:4:13 + | +LL | let _ = include_str!("hello.rs"); + | ^^^^^^^^^^^^^----------^ + | | + | help: there is a file in another directory: `"../hello.rs"` + | + = note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: couldn't read $DIR/../../data.bin: No such file or directory (os error 2) + --> $DIR/parent_dir.rs:6:13 + | +LL | let _ = include_bytes!("../../data.bin"); + | ^^^^^^^^^^^^^^^----------------^ + | | + | help: there is a file in another directory: `"data.bin"` + | + = note: this error originates in the macro `include_bytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: couldn't read $DIR/tests/ui/include-macros/file.txt: No such file or directory (os error 2) + --> $DIR/parent_dir.rs:8:13 + | +LL | let _ = include_str!("tests/ui/include-macros/file.txt"); + | ^^^^^^^^^^^^^----------------------------------^ + | | + | help: there is a file in another directory: `"file.txt"` + | + = note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 4 previous errors