Skip to content

Commit 2f4edae

Browse files
committed
rustc: improve diagnostics for file-open failures
Emit more targeted diagnostics when an input file cannot be opened, including dedicated messages for common error kinds and typo suggestions for missing files. Add run-make tests covering NotFound, PermissionDenied, IsADirectory, and non-existent directory cases. Signed-off-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
1 parent cddcbec commit 2f4edae

9 files changed

Lines changed: 110 additions & 10 deletions

File tree

compiler/rustc_parse/src/lib.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ use rustc_ast_pretty::pprust;
2020
use rustc_errors::{Diag, EmissionGuarantee, FatalError, PResult, pluralize};
2121
pub use rustc_lexer::UNICODE_VERSION;
2222
use rustc_session::parse::ParseSess;
23+
use rustc_span::edit_distance::find_best_match_for_name;
2324
use rustc_span::source_map::SourceMap;
24-
use rustc_span::{FileName, SourceFile, Span};
25+
use rustc_span::{FileName, SourceFile, Span, Symbol};
2526

2627
pub const MACRO_ARGUMENTS: Option<&str> = Some("macro arguments");
2728

@@ -105,6 +106,8 @@ pub fn new_parser_from_source_str(
105106
/// dropped.
106107
///
107108
/// If a span is given, that is used on an error as the source of the problem.
109+
///
110+
/// Error messages are tailored to the specific error kind.
108111
pub fn new_parser_from_file<'a>(
109112
psess: &'a ParseSess,
110113
path: &Path,
@@ -113,8 +116,43 @@ pub fn new_parser_from_file<'a>(
113116
) -> Result<Parser<'a>, Vec<Diag<'a>>> {
114117
let sm = psess.source_map();
115118
let source_file = sm.load_file(path).unwrap_or_else(|e| {
116-
let msg = format!("couldn't read `{}`: {}", path.display(), e);
119+
use std::io::ErrorKind;
120+
121+
let msg = match e.kind() {
122+
ErrorKind::NotFound => format!("couldn't find file `{}`", path.display()),
123+
ErrorKind::PermissionDenied => {
124+
format!("permission denied when opening file `{}`", path.display())
125+
}
126+
ErrorKind::IsADirectory => format!("`{}` is a directory", path.display()),
127+
_ => format!("couldn't read `{}`: {}", path.display(), e),
128+
};
129+
117130
let mut err = psess.dcx().struct_fatal(msg);
131+
132+
if e.kind() == ErrorKind::NotFound {
133+
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
134+
let parent =
135+
path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
136+
if let Ok(entries) = std::fs::read_dir(parent) {
137+
let candidates: Vec<Symbol> = entries
138+
.flatten()
139+
.filter_map(|entry| entry.file_name().to_str().map(Symbol::intern))
140+
.collect();
141+
let lookup = Symbol::intern(file_name);
142+
if let Some(suggestion) = find_best_match_for_name(&candidates, lookup, None) {
143+
let suggested_path = if parent == Path::new(".") {
144+
suggestion.as_str().to_string()
145+
} else {
146+
parent.join(suggestion.as_str()).display().to_string()
147+
};
148+
err.help(format!(
149+
"you might have meant to open `{}`: `rustc {}`",
150+
suggested_path, suggested_path,
151+
));
152+
}
153+
}
154+
}
155+
}
118156
if let Ok(contents) = std::fs::read(path)
119157
&& let Err(utf8err) = std::str::from_utf8(&contents)
120158
{
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Tests that rustc produces helpful error messages when the input file
2+
// cannot be opened, including specific messages for different error kinds
3+
// and typo suggestions for NotFound errors.
4+
//
5+
// The permission-denied test requires Unix file mode bits and is skipped
6+
// on Windows. It is also skipped on riscv64/arm because those CI runners
7+
// run as root, which bypasses permission restrictions.
8+
9+
//@ ignore-riscv64
10+
//@ ignore-arm
11+
//@ ignore-windows
12+
//@ needs-target-std
13+
14+
#[cfg(unix)]
15+
use std::os::unix::fs::PermissionsExt;
16+
17+
use run_make_support::{rfs, run_in_tmpdir, rustc};
18+
19+
fn main() {
20+
// 1. NotFound — basic case: no "os error 2" in the output
21+
run_in_tmpdir(|| {
22+
rustc()
23+
.input("fo.rs")
24+
.run_fail()
25+
.assert_stderr_contains("couldn't find file `fo.rs`")
26+
.assert_stderr_not_contains("os error 2");
27+
});
28+
29+
// 2. NotFound with typo suggestion: foo.rs exists, compiling fo.rs should suggest foo.rs
30+
run_in_tmpdir(|| {
31+
rfs::write("foo.rs", b"fn main() {}");
32+
rustc()
33+
.input("fo.rs")
34+
.run_fail()
35+
.assert_stderr_contains("couldn't find file `fo.rs`")
36+
.assert_stderr_contains("you might have meant to open `foo.rs`");
37+
});
38+
39+
// 3. PermissionDenied — file exists but is unreadable
40+
run_in_tmpdir(|| {
41+
rfs::write("secret.rs", b"fn main() {}");
42+
43+
let mut perms = rfs::metadata("secret.rs").permissions();
44+
perms.set_mode(0o000); // no read, write, or execute
45+
rfs::set_permissions("secret.rs", perms);
46+
47+
// Run rustc before restoring permissions, store the result
48+
let output = rustc().input("secret.rs").run_fail();
49+
50+
// Restore permissions so the tmpdir cleanup can delete the file
51+
let mut perms = rfs::metadata("secret.rs").permissions();
52+
perms.set_mode(0o644);
53+
rfs::set_permissions("secret.rs", perms);
54+
55+
output.assert_stderr_contains("permission denied when opening file");
56+
});
57+
58+
// 4. IsADirectory — path points to a directory, not a file
59+
run_in_tmpdir(|| {
60+
rfs::create_dir("mydir.rs");
61+
rustc().input("mydir.rs").run_fail().assert_stderr_contains("is a directory");
62+
});
63+
}

tests/ui/modules/path-no-file-name.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
//@ normalize-stderr: "os error \d+" -> "os error $$ACCESS_DENIED_CODE"
33

44
#[path = "."]
5-
mod m; //~ ERROR couldn't read
6-
5+
mod m; //~ ERROR `$DIR/.` is a directory
76
fn main() {}

tests/ui/modules/path-no-file-name.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: couldn't read `$DIR/.`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE)
1+
error: `$DIR/.` is a directory
22
--> $DIR/path-no-file-name.rs:5:1
33
|
44
LL | mod m;

tests/ui/parser/issues/issue-5806.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
//@ normalize-stderr: "os error \d+" -> "os error $$ACCESS_DENIED_CODE"
33

44
#[path = "../parser"]
5-
mod foo; //~ ERROR couldn't read
5+
mod foo; //~ ERROR couldn't find file `$DIR/../parser`
66

77
fn main() {}

tests/ui/parser/issues/issue-5806.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: couldn't read `$DIR/../parser`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE)
1+
error: couldn't find file `$DIR/../parser`
22
--> $DIR/issue-5806.rs:5:1
33
|
44
LL | mod foo;

tests/ui/parser/mod_file_with_path_attr.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: couldn't read `$DIR/not_a_real_file.rs`: $FILE_NOT_FOUND_MSG (os error 2)
1+
error: couldn't find file `$DIR/not_a_real_file.rs`
22
--> $DIR/mod_file_with_path_attr.rs:4:1
33
|
44
LL | mod m;

tests/ui/unpretty/staged-api-invalid-path-108697.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
#![feature(staged_api)]
77
#[path = "lol"]
88
mod foo;
9-
//~^ ERROR couldn't read `$DIR/lol`
9+
//~^ ERROR couldn't find file `$DIR/lol`

tests/ui/unpretty/staged-api-invalid-path-108697.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: couldn't read `$DIR/lol`: $FILE_NOT_FOUND_MSG (os error 2)
1+
error: couldn't find file `$DIR/lol`
22
--> $DIR/staged-api-invalid-path-108697.rs:8:1
33
|
44
LL | mod foo;

0 commit comments

Comments
 (0)