about summary refs log tree commit diff
path: root/compiler/rustc_builtin_macros/src
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-03-28 02:47:46 +0000
committerbors <bors@rust-lang.org>2024-03-28 02:47:46 +0000
commit463a11bef4d6378439afebf2a9543aef36ccf2c1 (patch)
tree067185805e8b595771bec3529730b98d5a676a49 /compiler/rustc_builtin_macros/src
parentd779a7a25f67fced5f8fea232ef407c5b228a22f (diff)
parent826ddb30180373a1972cdf5787044d12f8ad868e (diff)
downloadrust-463a11bef4d6378439afebf2a9543aef36ccf2c1.tar.gz
rust-463a11bef4d6378439afebf2a9543aef36ccf2c1.zip
Auto merge of #121833 - kornelski:parent_include, r=estebank
Suggest correct path in include_bytes!

`include_bytes!` paths are relative, and I'm often not sure how nested is the `.rs` file that I'm editing, so I have to guess the number of `"../.."`. This change searches `..` and `../..` for the given file and offers corrected path as a suggestion.

I wasn't sure how to get the right span, and how to properly escape it.

```text
error: couldn't read src/file.txt: No such file or directory (os error 2)
 --> src/main.rs:2:13
  |
2 |     let x = include_bytes!("file.txt");
  |             ^^^^^^^^^^^^^^^----------^
  |                            |
  |                            help: it's in a parent directory: `"../../file.txt"`
```
Diffstat (limited to 'compiler/rustc_builtin_macros/src')
-rw-r--r--compiler/rustc_builtin_macros/src/source_util.rs157
1 files changed, 126 insertions, 31 deletions
diff --git a/compiler/rustc_builtin_macros/src/source_util.rs b/compiler/rustc_builtin_macros/src/source_util.rs
index dbb86df6811..abcdfabcaed 100644
--- a/compiler/rustc_builtin_macros/src/source_util.rs
+++ b/compiler/rustc_builtin_macros/src/source_util.rs
@@ -3,17 +3,22 @@ use rustc_ast::ptr::P;
 use rustc_ast::token;
 use rustc_ast::tokenstream::TokenStream;
 use rustc_ast_pretty::pprust;
-use rustc_expand::base::{check_zero_tts, get_single_str_from_tts, parse_expr, resolve_path};
+use rustc_data_structures::sync::Lrc;
+use rustc_expand::base::{
+    check_zero_tts, get_single_str_from_tts, get_single_str_spanned_from_tts, parse_expr,
+    resolve_path,
+};
 use rustc_expand::base::{DummyResult, ExpandResult, ExtCtxt};
 use rustc_expand::base::{MacEager, MacResult, MacroExpanderResult};
 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, PathBuf};
 use std::rc::Rc;
 
 // These macros all relate to the file system; they either return
@@ -182,35 +187,26 @@ pub fn expand_include_str(
     tts: TokenStream,
 ) -> MacroExpanderResult<'static> {
     let sp = cx.with_def_site_ctxt(sp);
-    let ExpandResult::Ready(mac) = get_single_str_from_tts(cx, sp, tts, "include_str!") else {
+    let ExpandResult::Ready(mac) = get_single_str_spanned_from_tts(cx, sp, tts, "include_str!")
+    else {
         return ExpandResult::Retry(());
     };
-    let file = match mac {
-        Ok(file) => file,
+    let (path, path_span) = match mac {
+        Ok(res) => res,
         Err(guar) => return ExpandResult::Ready(DummyResult::any(sp, guar)),
     };
-    let file = match resolve_path(&cx.sess, file.as_str(), sp) {
-        Ok(f) => f,
-        Err(err) => {
-            let guar = err.emit();
-            return ExpandResult::Ready(DummyResult::any(sp, guar));
-        }
-    };
-    ExpandResult::Ready(match cx.source_map().load_binary_file(&file) {
+    ExpandResult::Ready(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);
                 MacEager::expr(cx.expr_str(sp, interned_src))
             }
             Err(_) => {
-                let guar = cx.dcx().span_err(sp, format!("{} wasn't a utf-8 file", file.display()));
+                let guar = cx.dcx().span_err(sp, format!("`{path}` wasn't a utf-8 file"));
                 DummyResult::any(sp, guar)
             }
         },
-        Err(e) => {
-            let guar = cx.dcx().span_err(sp, format!("couldn't read {}: {}", file.display(), e));
-            DummyResult::any(sp, guar)
-        }
+        Err(dummy) => dummy,
     })
 }
 
@@ -220,28 +216,127 @@ pub fn expand_include_bytes(
     tts: TokenStream,
 ) -> MacroExpanderResult<'static> {
     let sp = cx.with_def_site_ctxt(sp);
-    let ExpandResult::Ready(mac) = get_single_str_from_tts(cx, sp, tts, "include_bytes!") else {
+    let ExpandResult::Ready(mac) = get_single_str_spanned_from_tts(cx, sp, tts, "include_bytes!")
+    else {
         return ExpandResult::Retry(());
     };
-    let file = match mac {
-        Ok(file) => file,
+    let (path, path_span) = match mac {
+        Ok(res) => res,
         Err(guar) => return ExpandResult::Ready(DummyResult::any(sp, guar)),
     };
-    let file = match resolve_path(&cx.sess, file.as_str(), sp) {
-        Ok(f) => f,
+    ExpandResult::Ready(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)
+        }
+        Err(dummy) => dummy,
+    })
+}
+
+fn load_binary_file(
+    cx: &mut ExtCtxt<'_>,
+    original_path: &Path,
+    macro_span: Span,
+    path_span: Span,
+) -> Result<Lrc<[u8]>, Box<dyn MacResult>> {
+    let resolved_path = match resolve_path(&cx.sess, original_path, macro_span) {
+        Ok(path) => path,
         Err(err) => {
             let guar = err.emit();
-            return ExpandResult::Ready(DummyResult::any(sp, guar));
+            return Err(DummyResult::any(macro_span, guar));
         }
     };
-    ExpandResult::Ready(match cx.source_map().load_binary_file(&file) {
-        Ok(bytes) => {
-            let expr = cx.expr(sp, ast::ExprKind::IncludedBytes(bytes));
-            MacEager::expr(expr)
+    match cx.source_map().load_binary_file(&resolved_path) {
+        Ok(data) => Ok(data),
+        Err(io_err) => {
+            let mut err = cx.dcx().struct_span_err(
+                macro_span,
+                format!("couldn't read `{}`: {io_err}", resolved_path.display()),
+            );
+
+            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 with the same name in a different directory",
+                        format!("\"{}\"", new_path.replace('\\', "/").escape_debug()),
+                        rustc_lint_defs::Applicability::MachineApplicable,
+                    );
+                }
+            }
+            let guar = err.emit();
+            Err(DummyResult::any(macro_span, guar))
         }
-        Err(e) => {
-            let guar = cx.dcx().span_err(sp, format!("couldn't read {}: {}", file.display(), e));
-            DummyResult::any(sp, guar)
+    }
+}
+
+fn find_path_suggestion(
+    source_map: &SourceMap,
+    base_dir: &Path,
+    wanted_path: &Path,
+) -> Option<PathBuf> {
+    // 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
 }