Skip to content

Commit c991ec4

Browse files
authored
Rollup merge of rust-lang#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"` ```
2 parents 3481e05 + 25ab1c7 commit c991ec4

File tree

7 files changed

+222
-48
lines changed

7 files changed

+222
-48
lines changed

compiler/rustc_builtin_macros/src/source_util.rs

+120-30
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ use rustc_ast::ptr::P;
33
use rustc_ast::token;
44
use rustc_ast::tokenstream::TokenStream;
55
use rustc_ast_pretty::pprust;
6+
use rustc_data_structures::sync::Lrc;
67
use rustc_expand::base::{
7-
check_zero_tts, get_single_str_from_tts, parse_expr, resolve_path, DummyResult, ExtCtxt,
8-
MacEager, MacResult,
8+
check_zero_tts, get_single_str_from_tts, get_single_str_spanned_from_tts, parse_expr,
9+
resolve_path, DummyResult, ExtCtxt, MacEager, MacResult,
910
};
1011
use rustc_expand::module::DirOwnership;
1112
use rustc_parse::new_parser_from_file;
1213
use rustc_parse::parser::{ForceCollect, Parser};
1314
use rustc_session::lint::builtin::INCOMPLETE_INCLUDE;
15+
use rustc_span::source_map::SourceMap;
1416
use rustc_span::symbol::Symbol;
1517
use rustc_span::{Pos, Span};
16-
1718
use smallvec::SmallVec;
19+
use std::path::{Path, PathBuf};
1820
use std::rc::Rc;
1921

2022
// These macros all relate to the file system; they either return
@@ -180,32 +182,22 @@ pub fn expand_include_str(
180182
tts: TokenStream,
181183
) -> Box<dyn MacResult + 'static> {
182184
let sp = cx.with_def_site_ctxt(sp);
183-
let file = match get_single_str_from_tts(cx, sp, tts, "include_str!") {
184-
Ok(file) => file,
185+
let (path, path_span) = match get_single_str_spanned_from_tts(cx, sp, tts, "include_str!") {
186+
Ok(res) => res,
185187
Err(guar) => return DummyResult::any(sp, guar),
186188
};
187-
let file = match resolve_path(&cx.sess, file.as_str(), sp) {
188-
Ok(f) => f,
189-
Err(err) => {
190-
let guar = err.emit();
191-
return DummyResult::any(sp, guar);
192-
}
193-
};
194-
match cx.source_map().load_binary_file(&file) {
189+
match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) {
195190
Ok(bytes) => match std::str::from_utf8(&bytes) {
196191
Ok(src) => {
197192
let interned_src = Symbol::intern(src);
198193
MacEager::expr(cx.expr_str(sp, interned_src))
199194
}
200195
Err(_) => {
201-
let guar = cx.dcx().span_err(sp, format!("{} wasn't a utf-8 file", file.display()));
196+
let guar = cx.dcx().span_err(sp, format!("`{path}` wasn't a utf-8 file"));
202197
DummyResult::any(sp, guar)
203198
}
204199
},
205-
Err(e) => {
206-
let guar = cx.dcx().span_err(sp, format!("couldn't read {}: {}", file.display(), e));
207-
DummyResult::any(sp, guar)
208-
}
200+
Err(dummy) => dummy,
209201
}
210202
}
211203

@@ -215,25 +207,123 @@ pub fn expand_include_bytes(
215207
tts: TokenStream,
216208
) -> Box<dyn MacResult + 'static> {
217209
let sp = cx.with_def_site_ctxt(sp);
218-
let file = match get_single_str_from_tts(cx, sp, tts, "include_bytes!") {
219-
Ok(file) => file,
210+
let (path, path_span) = match get_single_str_spanned_from_tts(cx, sp, tts, "include_bytes!") {
211+
Ok(res) => res,
220212
Err(guar) => return DummyResult::any(sp, guar),
221213
};
222-
let file = match resolve_path(&cx.sess, file.as_str(), sp) {
223-
Ok(f) => f,
214+
match load_binary_file(cx, path.as_str().as_ref(), sp, path_span) {
215+
Ok(bytes) => {
216+
let expr = cx.expr(sp, ast::ExprKind::IncludedBytes(bytes));
217+
MacEager::expr(expr)
218+
}
219+
Err(dummy) => dummy,
220+
}
221+
}
222+
223+
fn load_binary_file(
224+
cx: &mut ExtCtxt<'_>,
225+
original_path: &Path,
226+
macro_span: Span,
227+
path_span: Span,
228+
) -> Result<Lrc<[u8]>, Box<dyn MacResult>> {
229+
let resolved_path = match resolve_path(&cx.sess, original_path, macro_span) {
230+
Ok(path) => path,
224231
Err(err) => {
225232
let guar = err.emit();
226-
return DummyResult::any(sp, guar);
233+
return Err(DummyResult::any(macro_span, guar));
227234
}
228235
};
229-
match cx.source_map().load_binary_file(&file) {
230-
Ok(bytes) => {
231-
let expr = cx.expr(sp, ast::ExprKind::IncludedBytes(bytes));
232-
MacEager::expr(expr)
236+
match cx.source_map().load_binary_file(&resolved_path) {
237+
Ok(data) => Ok(data),
238+
Err(io_err) => {
239+
let mut err = cx.dcx().struct_span_err(
240+
macro_span,
241+
format!("couldn't read `{}`: {io_err}", resolved_path.display()),
242+
);
243+
244+
if original_path.is_relative() {
245+
let source_map = cx.sess.source_map();
246+
let new_path = source_map
247+
.span_to_filename(macro_span.source_callsite())
248+
.into_local_path()
249+
.and_then(|src| find_path_suggestion(source_map, src.parent()?, original_path))
250+
.and_then(|path| path.into_os_string().into_string().ok());
251+
252+
if let Some(new_path) = new_path {
253+
err.span_suggestion(
254+
path_span,
255+
"there is a file with the same name in a different directory",
256+
format!("\"{}\"", new_path.escape_debug()),
257+
rustc_lint_defs::Applicability::MachineApplicable,
258+
);
259+
}
260+
}
261+
let guar = err.emit();
262+
Err(DummyResult::any(macro_span, guar))
233263
}
234-
Err(e) => {
235-
let guar = cx.dcx().span_err(sp, format!("couldn't read {}: {}", file.display(), e));
236-
DummyResult::any(sp, guar)
264+
}
265+
}
266+
267+
fn find_path_suggestion(
268+
source_map: &SourceMap,
269+
base_dir: &Path,
270+
wanted_path: &Path,
271+
) -> Option<PathBuf> {
272+
// Fix paths that assume they're relative to cargo manifest dir
273+
let mut base_c = base_dir.components();
274+
let mut wanted_c = wanted_path.components();
275+
let mut without_base = None;
276+
while let Some(wanted_next) = wanted_c.next() {
277+
if wanted_c.as_path().file_name().is_none() {
278+
break;
279+
}
280+
// base_dir may be absolute
281+
while let Some(base_next) = base_c.next() {
282+
if base_next == wanted_next {
283+
without_base = Some(wanted_c.as_path());
284+
break;
285+
}
286+
}
287+
}
288+
let root_absolute = without_base.into_iter().map(PathBuf::from);
289+
290+
let base_dir_components = base_dir.components().count();
291+
// Avoid going all the way to the root dir
292+
let max_parent_components = if base_dir.is_relative() {
293+
base_dir_components + 1
294+
} else {
295+
base_dir_components.saturating_sub(1)
296+
};
297+
298+
// Try with additional leading ../
299+
let mut prefix = PathBuf::new();
300+
let add = std::iter::from_fn(|| {
301+
prefix.push("..");
302+
Some(prefix.join(wanted_path))
303+
})
304+
.take(max_parent_components.min(3));
305+
306+
// Try without leading directories
307+
let mut trimmed_path = wanted_path;
308+
let remove = std::iter::from_fn(|| {
309+
let mut components = trimmed_path.components();
310+
let removed = components.next()?;
311+
trimmed_path = components.as_path();
312+
let _ = trimmed_path.file_name()?; // ensure there is a file name left
313+
Some([
314+
Some(trimmed_path.to_path_buf()),
315+
(removed != std::path::Component::ParentDir)
316+
.then(|| Path::new("..").join(trimmed_path)),
317+
])
318+
})
319+
.flatten()
320+
.flatten()
321+
.take(4);
322+
323+
for new_path in root_absolute.chain(add).chain(remove) {
324+
if source_map.file_exists(&base_dir.join(&new_path)) {
325+
return Some(new_path);
237326
}
238327
}
328+
None
239329
}

compiler/rustc_expand/src/base.rs

+26-15
Original file line numberDiff line numberDiff line change
@@ -1233,21 +1233,18 @@ pub fn resolve_path(sess: &Session, path: impl Into<PathBuf>, span: Span) -> PRe
12331233
// after macro expansion (that is, they are unhygienic).
12341234
if !path.is_absolute() {
12351235
let callsite = span.source_callsite();
1236-
let mut result = match sess.source_map().span_to_filename(callsite) {
1237-
FileName::Real(name) => name
1238-
.into_local_path()
1239-
.expect("attempting to resolve a file path in an external file"),
1240-
FileName::DocTest(path, _) => path,
1241-
other => {
1242-
return Err(sess.dcx().create_err(errors::ResolveRelativePath {
1243-
span,
1244-
path: sess.source_map().filename_for_diagnostics(&other).to_string(),
1245-
}));
1246-
}
1236+
let source_map = sess.source_map();
1237+
let Some(mut base_path) = source_map.span_to_filename(callsite).into_local_path() else {
1238+
return Err(sess.dcx().create_err(errors::ResolveRelativePath {
1239+
span,
1240+
path: source_map
1241+
.filename_for_diagnostics(&source_map.span_to_filename(callsite))
1242+
.to_string(),
1243+
}));
12471244
};
1248-
result.pop();
1249-
result.push(path);
1250-
Ok(result)
1245+
base_path.pop();
1246+
base_path.push(path);
1247+
Ok(base_path)
12511248
} else {
12521249
Ok(path)
12531250
}
@@ -1344,6 +1341,15 @@ pub fn get_single_str_from_tts(
13441341
tts: TokenStream,
13451342
name: &str,
13461343
) -> Result<Symbol, ErrorGuaranteed> {
1344+
get_single_str_spanned_from_tts(cx, span, tts, name).map(|(s, _)| s)
1345+
}
1346+
1347+
pub fn get_single_str_spanned_from_tts(
1348+
cx: &mut ExtCtxt<'_>,
1349+
span: Span,
1350+
tts: TokenStream,
1351+
name: &str,
1352+
) -> Result<(Symbol, Span), ErrorGuaranteed> {
13471353
let mut p = cx.new_parser_from_tts(tts);
13481354
if p.token == token::Eof {
13491355
let guar = cx.dcx().emit_err(errors::OnlyOneArgument { span, name });
@@ -1355,7 +1361,12 @@ pub fn get_single_str_from_tts(
13551361
if p.token != token::Eof {
13561362
cx.dcx().emit_err(errors::OnlyOneArgument { span, name });
13571363
}
1358-
expr_to_string(cx, ret, "argument must be a string literal").map(|(s, _)| s)
1364+
expr_to_spanned_string(cx, ret, "argument must be a string literal")
1365+
.map_err(|err| match err {
1366+
Ok((err, _)) => err.emit(),
1367+
Err(guar) => guar,
1368+
})
1369+
.map(|(symbol, _style, span)| (symbol, span))
13591370
}
13601371

13611372
/// Extracts comma-separated expressions from `tts`.

compiler/rustc_span/src/lib.rs

+11
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,17 @@ impl FileName {
427427
src.hash(&mut hasher);
428428
FileName::InlineAsm(hasher.finish())
429429
}
430+
431+
/// Returns the path suitable for reading from the file system on the local host,
432+
/// if this information exists.
433+
/// Avoid embedding this in build artifacts; see `remapped_path_if_available()` for that.
434+
pub fn into_local_path(self) -> Option<PathBuf> {
435+
match self {
436+
FileName::Real(path) => path.into_local_path(),
437+
FileName::DocTest(path, _) => Some(path),
438+
_ => None,
439+
}
440+
}
430441
}
431442

432443
/// Represents a span.

tests/ui/include-macros/parent_dir.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//@ normalize-stderr-test: "`: .*\(os error" -> "`: $$FILE_NOT_FOUND_MSG (os error"
2+
3+
fn main() {
4+
let _ = include_str!("include-macros/file.txt"); //~ ERROR couldn't read
5+
//~^HELP different directory
6+
let _ = include_str!("hello.rs"); //~ ERROR couldn't read
7+
//~^HELP different directory
8+
let _ = include_bytes!("../../data.bin"); //~ ERROR couldn't read
9+
//~^HELP different directory
10+
let _ = include_str!("tests/ui/include-macros/file.txt"); //~ ERROR couldn't read
11+
//~^HELP different directory
12+
}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
error: couldn't read `$DIR/include-macros/file.txt`: $FILE_NOT_FOUND_MSG (os error 2)
2+
--> $DIR/parent_dir.rs:4:13
3+
|
4+
LL | let _ = include_str!("include-macros/file.txt");
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
8+
help: there is a file with the same name in a different directory
9+
|
10+
LL | let _ = include_str!("file.txt");
11+
| ~~~~~~~~~~
12+
13+
error: couldn't read `$DIR/hello.rs`: $FILE_NOT_FOUND_MSG (os error 2)
14+
--> $DIR/parent_dir.rs:6:13
15+
|
16+
LL | let _ = include_str!("hello.rs");
17+
| ^^^^^^^^^^^^^^^^^^^^^^^^
18+
|
19+
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
20+
help: there is a file with the same name in a different directory
21+
|
22+
LL | let _ = include_str!("../hello.rs");
23+
| ~~~~~~~~~~~~~
24+
25+
error: couldn't read `$DIR/../../data.bin`: $FILE_NOT_FOUND_MSG (os error 2)
26+
--> $DIR/parent_dir.rs:8:13
27+
|
28+
LL | let _ = include_bytes!("../../data.bin");
29+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
|
31+
= note: this error originates in the macro `include_bytes` (in Nightly builds, run with -Z macro-backtrace for more info)
32+
help: there is a file with the same name in a different directory
33+
|
34+
LL | let _ = include_bytes!("data.bin");
35+
| ~~~~~~~~~~
36+
37+
error: couldn't read `$DIR/tests/ui/include-macros/file.txt`: $FILE_NOT_FOUND_MSG (os error 2)
38+
--> $DIR/parent_dir.rs:10:13
39+
|
40+
LL | let _ = include_str!("tests/ui/include-macros/file.txt");
41+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42+
|
43+
= note: this error originates in the macro `include_str` (in Nightly builds, run with -Z macro-backtrace for more info)
44+
help: there is a file with the same name in a different directory
45+
|
46+
LL | let _ = include_str!("file.txt");
47+
| ~~~~~~~~~~
48+
49+
error: aborting due to 4 previous errors
50+

tests/ui/macros/macros-nonfatal-errors.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//@ normalize-stderr-test: "existed:.*\(" -> "existed: $$FILE_NOT_FOUND_MSG ("
1+
//@ normalize-stderr-test: "`: .*\(" -> "`: $$FILE_NOT_FOUND_MSG ("
22

33
// test that errors in a (selection) of macros don't kill compilation
44
// immediately, so that we get more errors listed at a time.

tests/ui/macros/macros-nonfatal-errors.stderr

+2-2
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ error: argument must be a string literal
200200
LL | include_str!(invalid);
201201
| ^^^^^^^
202202

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

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

0 commit comments

Comments
 (0)