diff --git a/compiler/rustc_parse/src/lib.rs b/compiler/rustc_parse/src/lib.rs index 7805994b441b8..14debe267d3dc 100644 --- a/compiler/rustc_parse/src/lib.rs +++ b/compiler/rustc_parse/src/lib.rs @@ -20,8 +20,9 @@ use rustc_ast_pretty::pprust; use rustc_errors::{Diag, EmissionGuarantee, FatalError, PResult, pluralize}; pub use rustc_lexer::UNICODE_VERSION; use rustc_session::parse::ParseSess; +use rustc_span::edit_distance::find_best_match_for_name; use rustc_span::source_map::SourceMap; -use rustc_span::{FileName, SourceFile, Span}; +use rustc_span::{FileName, SourceFile, Span, Symbol}; pub const MACRO_ARGUMENTS: Option<&str> = Some("macro arguments"); @@ -105,6 +106,8 @@ pub fn new_parser_from_source_str( /// dropped. /// /// If a span is given, that is used on an error as the source of the problem. +/// +/// Error messages are tailored to the specific error kind. pub fn new_parser_from_file<'a>( psess: &'a ParseSess, path: &Path, @@ -113,8 +116,42 @@ pub fn new_parser_from_file<'a>( ) -> Result, Vec>> { let sm = psess.source_map(); let source_file = sm.load_file(path).unwrap_or_else(|e| { - let msg = format!("couldn't read `{}`: {}", path.display(), e); + use std::io::ErrorKind; + + let msg = match e.kind() { + ErrorKind::NotFound => format!("couldn't find file `{}`", path.display()), + ErrorKind::PermissionDenied => { + format!("permission denied when opening file `{}`", path.display()) + } + ErrorKind::IsADirectory => format!("`{}` is a directory", path.display()), + _ => format!("couldn't read `{}`: {}", path.display(), e), + }; + let mut err = psess.dcx().struct_fatal(msg); + + if e.kind() == ErrorKind::NotFound { + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + let parent = match path.parent() { + Some(p) if !p.as_os_str().is_empty() => p, + _ => Path::new("."), + }; + if let Ok(entries) = std::fs::read_dir(parent) { + let candidates: Vec = entries + .flatten() + .filter_map(|entry| entry.file_name().to_str().map(Symbol::intern)) + .collect(); + let lookup = Symbol::intern(file_name); + if let Some(suggestion) = find_best_match_for_name(&candidates, lookup, None) { + let suggested_path = if parent == Path::new(".") { + suggestion.as_str().to_string() + } else { + parent.join(suggestion.as_str()).display().to_string() + }; + err.help(format!("you might have meant to open `{}`", suggested_path)); + } + } + } + } if let Ok(contents) = std::fs::read(path) && let Err(utf8err) = std::str::from_utf8(&contents) { diff --git a/tests/run-make/input-file-errors/rmake.rs b/tests/run-make/input-file-errors/rmake.rs new file mode 100644 index 0000000000000..a3d8516af662c --- /dev/null +++ b/tests/run-make/input-file-errors/rmake.rs @@ -0,0 +1,63 @@ +// Tests that rustc produces helpful error messages when the input file +// cannot be opened, including specific messages for different error kinds +// and typo suggestions for NotFound errors. +// +// The permission-denied test requires Unix file mode bits and is skipped +// on Windows. It is also skipped on riscv64/arm because those CI runners +// run as root, which bypasses permission restrictions. + +//@ ignore-riscv64 +//@ ignore-arm +//@ ignore-windows +//@ needs-target-std + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +use run_make_support::{rfs, run_in_tmpdir, rustc}; + +fn main() { + // 1. NotFound — basic case: no "os error 2" in the output + run_in_tmpdir(|| { + rustc() + .input("fo.rs") + .run_fail() + .assert_stderr_contains("couldn't find file `fo.rs`") + .assert_stderr_not_contains("os error 2"); + }); + + // 2. NotFound with typo suggestion: foo.rs exists, compiling fo.rs should suggest foo.rs + run_in_tmpdir(|| { + rfs::write("foo.rs", b"fn main() {}"); + rustc() + .input("fo.rs") + .run_fail() + .assert_stderr_contains("couldn't find file `fo.rs`") + .assert_stderr_contains("you might have meant to open `foo.rs`"); + }); + + // 3. PermissionDenied — file exists but is unreadable + run_in_tmpdir(|| { + rfs::write("secret.rs", b"fn main() {}"); + + let mut perms = rfs::metadata("secret.rs").permissions(); + perms.set_mode(0o000); // no read, write, or execute + rfs::set_permissions("secret.rs", perms); + + // Run rustc before restoring permissions, store the result + let output = rustc().input("secret.rs").run_fail(); + + // Restore permissions so the tmpdir cleanup can delete the file + let mut perms = rfs::metadata("secret.rs").permissions(); + perms.set_mode(0o644); + rfs::set_permissions("secret.rs", perms); + + output.assert_stderr_contains("permission denied when opening file"); + }); + + // 4. IsADirectory — path points to a directory, not a file + run_in_tmpdir(|| { + rfs::create_dir("mydir.rs"); + rustc().input("mydir.rs").run_fail().assert_stderr_contains("is a directory"); + }); +} diff --git a/tests/ui/modules/path-no-file-name.stderr b/tests/ui/modules/path-no-file-name.stderr index 6274ecfed1365..4a45b22194dd4 100644 --- a/tests/ui/modules/path-no-file-name.stderr +++ b/tests/ui/modules/path-no-file-name.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/.`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE) +error: permission denied when opening file `$DIR/.` --> $DIR/path-no-file-name.rs:5:1 | LL | mod m; diff --git a/tests/ui/parser/issues/issue-5806.rs b/tests/ui/parser/issues/issue-5806.rs index 1a819e22197fe..e6eaead5d886b 100644 --- a/tests/ui/parser/issues/issue-5806.rs +++ b/tests/ui/parser/issues/issue-5806.rs @@ -2,6 +2,6 @@ //@ normalize-stderr: "os error \d+" -> "os error $$ACCESS_DENIED_CODE" #[path = "../parser"] -mod foo; //~ ERROR couldn't read +mod foo; //~ ERROR couldn't find file `$DIR/../parser` fn main() {} diff --git a/tests/ui/parser/issues/issue-5806.stderr b/tests/ui/parser/issues/issue-5806.stderr index 88cc982baf259..2de6c0ab08670 100644 --- a/tests/ui/parser/issues/issue-5806.stderr +++ b/tests/ui/parser/issues/issue-5806.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/../parser`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE) +error: couldn't find file `$DIR/../parser` --> $DIR/issue-5806.rs:5:1 | LL | mod foo; diff --git a/tests/ui/parser/mod_file_with_path_attr.stderr b/tests/ui/parser/mod_file_with_path_attr.stderr index ef8a715712bbd..986b0914b6c4f 100644 --- a/tests/ui/parser/mod_file_with_path_attr.stderr +++ b/tests/ui/parser/mod_file_with_path_attr.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/not_a_real_file.rs`: $FILE_NOT_FOUND_MSG (os error 2) +error: couldn't find file `$DIR/not_a_real_file.rs` --> $DIR/mod_file_with_path_attr.rs:4:1 | LL | mod m; diff --git a/tests/ui/unpretty/staged-api-invalid-path-108697.rs b/tests/ui/unpretty/staged-api-invalid-path-108697.rs index 8a806b10d9da1..b368bcbc074f9 100644 --- a/tests/ui/unpretty/staged-api-invalid-path-108697.rs +++ b/tests/ui/unpretty/staged-api-invalid-path-108697.rs @@ -6,4 +6,4 @@ #![feature(staged_api)] #[path = "lol"] mod foo; -//~^ ERROR couldn't read `$DIR/lol` +//~^ ERROR couldn't find file `$DIR/lol` diff --git a/tests/ui/unpretty/staged-api-invalid-path-108697.stderr b/tests/ui/unpretty/staged-api-invalid-path-108697.stderr index 188f4985ded56..a475dc2a60f47 100644 --- a/tests/ui/unpretty/staged-api-invalid-path-108697.stderr +++ b/tests/ui/unpretty/staged-api-invalid-path-108697.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/lol`: $FILE_NOT_FOUND_MSG (os error 2) +error: couldn't find file `$DIR/lol` --> $DIR/staged-api-invalid-path-108697.rs:8:1 | LL | mod foo;