Skip to content

Commit

Permalink
Auto merge of rust-lang#121833 - kornelski:parent_include, r=estebank
Browse files Browse the repository at this point in the history
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"`
```
  • Loading branch information
bors committed Mar 26, 2024
2 parents 8b9e47c + 0e4d790 commit b47a377
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 49 deletions.
157 changes: 126 additions & 31 deletions compiler/rustc_builtin_macros/src/source_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
}

Expand All @@ -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.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
}
42 changes: 27 additions & 15 deletions compiler/rustc_expand/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,21 +1252,18 @@ pub fn resolve_path(sess: &Session, path: impl Into<PathBuf>, span: Span) -> PRe
// after macro expansion (that is, they are unhygienic).
if !path.is_absolute() {
let callsite = span.source_callsite();
let mut result = match sess.source_map().span_to_filename(callsite) {
FileName::Real(name) => name
.into_local_path()
.expect("attempting to resolve a file path in an external file"),
FileName::DocTest(path, _) => path,
other => {
return Err(sess.dcx().create_err(errors::ResolveRelativePath {
span,
path: sess.source_map().filename_for_diagnostics(&other).to_string(),
}));
}
let source_map = sess.source_map();
let Some(mut base_path) = source_map.span_to_filename(callsite).into_local_path() else {
return Err(sess.dcx().create_err(errors::ResolveRelativePath {
span,
path: source_map
.filename_for_diagnostics(&source_map.span_to_filename(callsite))
.to_string(),
}));
};
result.pop();
result.push(path);
Ok(result)
base_path.pop();
base_path.push(path);
Ok(base_path)
} else {
Ok(path)
}
Expand Down Expand Up @@ -1379,6 +1376,15 @@ pub fn get_single_str_from_tts(
tts: TokenStream,
name: &str,
) -> ExpandResult<Result<Symbol, ErrorGuaranteed>, ()> {
get_single_str_spanned_from_tts(cx, span, tts, name).map(|res| res.map(|(s, _)| s))
}

pub fn get_single_str_spanned_from_tts(
cx: &mut ExtCtxt<'_>,
span: Span,
tts: TokenStream,
name: &str,
) -> ExpandResult<Result<(Symbol, Span), ErrorGuaranteed>, ()> {
let mut p = cx.new_parser_from_tts(tts);
if p.token == token::Eof {
let guar = cx.dcx().emit_err(errors::OnlyOneArgument { span, name });
Expand All @@ -1393,7 +1399,13 @@ pub fn get_single_str_from_tts(
if p.token != token::Eof {
cx.dcx().emit_err(errors::OnlyOneArgument { span, name });
}
expr_to_string(cx, ret, "argument must be a string literal").map(|s| s.map(|(s, _)| s))
expr_to_spanned_string(cx, ret, "argument must be a string literal").map(|res| {
res.map_err(|err| match err {
Ok((err, _)) => err.emit(),
Err(guar) => guar,
})
.map(|(symbol, _style, span)| (symbol, span))
})
}

/// Extracts comma-separated expressions from `tts`.
Expand Down
11 changes: 11 additions & 0 deletions compiler/rustc_span/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,17 @@ impl FileName {
src.hash(&mut hasher);
FileName::InlineAsm(hasher.finish())
}

/// Returns the path suitable for reading from the file system on the local host,
/// if this information exists.
/// Avoid embedding this in build artifacts; see `remapped_path_if_available()` for that.
pub fn into_local_path(self) -> Option<PathBuf> {
match self {
FileName::Real(path) => path.into_local_path(),
FileName::DocTest(path, _) => Some(path),
_ => None,
}
}
}

/// Represents a span.
Expand Down
12 changes: 12 additions & 0 deletions tests/ui/include-macros/parent_dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//@ normalize-stderr-test: "`: .*" -> "`: $$FILE_NOT_FOUND_MSG"

fn main() {
let _ = include_str!("include-macros/file.txt"); //~ ERROR couldn't read

Check failure on line 4 in tests/ui/include-macros/parent_dir.rs

View workflow job for this annotation

GitHub Actions / auto - x86_64-msvc

couldn't read `C:\a\rust\rust\tests\ui\include-macros\include-macros/file.txt`: The system cannot find the path specified. (os error 3)
//~^HELP different directory
let _ = include_str!("hello.rs"); //~ ERROR couldn't read

Check failure on line 6 in tests/ui/include-macros/parent_dir.rs

View workflow job for this annotation

GitHub Actions / auto - x86_64-msvc

couldn't read `C:\a\rust\rust\tests\ui\include-macros\hello.rs`: The system cannot find the file specified. (os error 2)
//~^HELP different directory
let _ = include_bytes!("../../data.bin"); //~ ERROR couldn't read

Check failure on line 8 in tests/ui/include-macros/parent_dir.rs

View workflow job for this annotation

GitHub Actions / auto - x86_64-msvc

couldn't read `C:\a\rust\rust\tests\ui\include-macros\../../data.bin`: The system cannot find the file specified. (os error 2)
//~^HELP different directory
let _ = include_str!("tests/ui/include-macros/file.txt"); //~ ERROR couldn't read

Check failure on line 10 in tests/ui/include-macros/parent_dir.rs

View workflow job for this annotation

GitHub Actions / auto - x86_64-msvc

couldn't read `C:\a\rust\rust\tests\ui\include-macros\tests/ui/include-macros/file.txt`: The system cannot find the path specified. (os error 3)
//~^HELP different directory
}
50 changes: 50 additions & 0 deletions tests/ui/include-macros/parent_dir.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
error: couldn't read `$DIR/include-macros/file.txt`: $FILE_NOT_FOUND_MSG
--> $DIR/parent_dir.rs:4:13
|
LL | let _ = include_str!("include-macros/file.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
help: there is a file with the same name in a different directory
|
LL | let _ = include_str!("file.txt");
| ~~~~~~~~~~

error: couldn't read `$DIR/hello.rs`: $FILE_NOT_FOUND_MSG
--> $DIR/parent_dir.rs:6:13
|
LL | let _ = include_str!("hello.rs");
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
help: there is a file with the same name in a different directory
|
LL | let _ = include_str!("../hello.rs");
| ~~~~~~~~~~~~~

error: couldn't read `$DIR/../../data.bin`: $FILE_NOT_FOUND_MSG
--> $DIR/parent_dir.rs:8:13
|
LL | let _ = include_bytes!("../../data.bin");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `include_bytes` (in Nightly builds, run with -Z macro-backtrace for more info)
help: there is a file with the same name in a different directory
|
LL | let _ = include_bytes!("data.bin");
| ~~~~~~~~~~

error: couldn't read `$DIR/tests/ui/include-macros/file.txt`: $FILE_NOT_FOUND_MSG
--> $DIR/parent_dir.rs:10:13
|
LL | let _ = include_str!("tests/ui/include-macros/file.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
help: there is a file with the same name in a different directory
|
LL | let _ = include_str!("file.txt");
| ~~~~~~~~~~

error: aborting due to 4 previous errors

2 changes: 1 addition & 1 deletion tests/ui/macros/macros-nonfatal-errors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//@ normalize-stderr-test: "existed:.*\(" -> "existed: $$FILE_NOT_FOUND_MSG ("
//@ normalize-stderr-test: "`: .*" -> "`: $$FILE_NOT_FOUND_MSG"

// test that errors in a (selection) of macros don't kill compilation
// immediately, so that we get more errors listed at a time.
Expand Down
4 changes: 2 additions & 2 deletions tests/ui/macros/macros-nonfatal-errors.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ error: argument must be a string literal
LL | include_str!(invalid);
| ^^^^^^^

error: couldn't read $DIR/i'd be quite surprised if a file with this name existed: $FILE_NOT_FOUND_MSG (os error 2)
error: couldn't read `$DIR/i'd be quite surprised if a file with this name existed`: $FILE_NOT_FOUND_MSG
--> $DIR/macros-nonfatal-errors.rs:113:5
|
LL | include_str!("i'd be quite surprised if a file with this name existed");
Expand All @@ -214,7 +214,7 @@ error: argument must be a string literal
LL | include_bytes!(invalid);
| ^^^^^^^

error: couldn't read $DIR/i'd be quite surprised if a file with this name existed: $FILE_NOT_FOUND_MSG (os error 2)
error: couldn't read `$DIR/i'd be quite surprised if a file with this name existed`: $FILE_NOT_FOUND_MSG
--> $DIR/macros-nonfatal-errors.rs:115:5
|
LL | include_bytes!("i'd be quite surprised if a file with this name existed");
Expand Down

0 comments on commit b47a377

Please sign in to comment.