From d46eda83ccfb645244e55f04ff5e7993265fd8d3 Mon Sep 17 00:00:00 2001 From: xizheyin Date: Sun, 24 May 2026 22:02:56 +0800 Subject: [PATCH] Suggest Span without source changes when source code is unavailable --- .../src/annotate_snippet_emitter_writer.rs | 80 ++++++++++++++++++- compiler/rustc_errors/src/json.rs | 3 + compiler/rustc_session/src/options.rs | 2 + compiler/rustc_session/src/session.rs | 8 +- src/tools/compiletest/src/runtest.rs | 2 + .../help-show-std-source.rs | 15 ++++ .../help-show-std-source.stderr | 16 ++++ 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 tests/ui/compiletest-self-test/help-show-std-source.rs create mode 100644 tests/ui/compiletest-self-test/help-show-std-source.stderr diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs index c3c9f26c31571..b5b7771ac5e25 100644 --- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -47,6 +47,7 @@ pub struct AnnotateSnippetEmitter { track_diagnostics: bool, terminal_url: TerminalUrl, theme: OutputTheme, + show_suggestions_with_unavailable_source: bool, } impl Debug for AnnotateSnippetEmitter { @@ -63,6 +64,10 @@ impl Debug for AnnotateSnippetEmitter { .field("track_diagnostics", &self.track_diagnostics) .field("terminal_url", &self.terminal_url) .field("theme", &self.theme) + .field( + "show_suggestions_with_unavailable_source", + &self.show_suggestions_with_unavailable_source, + ) .finish() } } @@ -136,6 +141,7 @@ impl AnnotateSnippetEmitter { track_diagnostics: false, terminal_url: TerminalUrl::No, theme: OutputTheme::Ascii, + show_suggestions_with_unavailable_source: false, } } @@ -307,6 +313,11 @@ impl AnnotateSnippetEmitter { SuggestionStyle::HideCodeInline | SuggestionStyle::ShowCode | SuggestionStyle::ShowAlways => { + let unavailable_source_span = if self.show_suggestions_with_unavailable_source { + self.suggestion_span_with_unavailable_source(sm, &suggestion) + } else { + None + }; let substitutions = suggestion .substitutions .into_iter() @@ -341,8 +352,19 @@ impl AnnotateSnippetEmitter { return None; } - // We can't splice anything if the source is unavailable. - if !sm.ensure_source_file_source_present(&lo_file) { + // We can't splice anything if the source is unavailable. When the + // span-only fallback is enabled, also avoid rendering source that was + // intentionally hidden from diagnostics. + let can_show_source = if self.show_suggestions_with_unavailable_source { + should_show_source_code( + &self.ignored_directories_in_source_blocks, + sm, + &lo_file, + ) + } else { + sm.ensure_source_file_source_present(&lo_file) + }; + if !can_show_source { return None; } @@ -356,6 +378,30 @@ impl AnnotateSnippetEmitter { .collect::>(); if substitutions.is_empty() { + if let Some(span) = unavailable_source_span { + let msg = format_diag_message(&suggestion.msg, args).to_string(); + report.push(std::mem::replace( + &mut group, + Group::with_title( + annotate_snippets::Level::HELP.secondary_title(msg), + ), + )); + + let file_ann = collect_annotations(args, &span, sm); + let level = annotate_snippets::Level::HELP; + for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() + { + group = self.unannotated_messages( + annotations, + &file.name, + sm, + file_idx, + &mut report, + group, + &level, + ); + } + } continue; } let mut msg = format_diag_message(&suggestion.msg, args).to_string(); @@ -645,6 +691,36 @@ impl AnnotateSnippetEmitter { } group } + + fn suggestion_span_with_unavailable_source( + &self, + sm: &Arc, + suggestion: &CodeSuggestion, + ) -> Option { + let spans = suggestion + .substitutions + .iter() + .flat_map(|subst| &subst.parts) + .filter_map(|part| { + if sm.is_valid_span(part.span).is_err() { + debug!("suggestion contains an invalid span: {:?}", part); + return None; + } + let lines = sm.span_to_lines(part.span).ok()?; + if should_show_source_code( + &self.ignored_directories_in_source_blocks, + sm, + &lines.file, + ) { + None + } else { + Some(part.span) + } + }) + .collect::>(); + + if spans.is_empty() { None } else { Some(MultiSpan::from_spans(spans)) } + } } fn emit_to_destination( diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs index 04ac140f33261..f367c309cbac4 100644 --- a/compiler/rustc_errors/src/json.rs +++ b/compiler/rustc_errors/src/json.rs @@ -54,6 +54,7 @@ pub struct JsonEmitter { macro_backtrace: bool, track_diagnostics: bool, terminal_url: TerminalUrl, + show_suggestions_with_unavailable_source: bool, } impl JsonEmitter { @@ -76,6 +77,7 @@ impl JsonEmitter { macro_backtrace: false, track_diagnostics: false, terminal_url: TerminalUrl::No, + show_suggestions_with_unavailable_source: false, } } @@ -373,6 +375,7 @@ impl Diagnostic { .terminal_url(je.terminal_url) .ui_testing(je.ui_testing) .ignored_directories_in_source_blocks(je.ignored_directories_in_source_blocks.clone()) + .show_suggestions_with_unavailable_source(je.show_suggestions_with_unavailable_source) .theme(if je.json_rendered.unicode { OutputTheme::Unicode } else { OutputTheme::Ascii }) .emit_diagnostic(diag); diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index aa9331ee8f659..00b0d701a58be 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2629,6 +2629,8 @@ written to standard error output)"), "make the current crate share its generic instantiations"), shell_argfiles: bool = (false, parse_bool, [UNTRACKED], "allow argument files to be specified with POSIX \"shell-style\" argument quoting"), + show_suggestions_with_unavailable_source: bool = (false, parse_bool, [UNTRACKED], + "show span-only suggestions when the source code is unavailable"), simulate_remapped_rust_src_base: Option = (None, parse_opt_pathbuf, [TRACKED], "simulate the effect of remap-debuginfo = true at bootstrapping by remapping path \ to rust's source base directory. only meant for testing purposes"), diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 003164e8f9054..fb85fca0f806a 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -947,6 +947,8 @@ impl Session { fn default_emitter(sopts: &config::Options, source_map: Arc) -> Box { let macro_backtrace = sopts.unstable_opts.macro_backtrace; let track_diagnostics = sopts.unstable_opts.track_diagnostics; + let show_suggestions_with_unavailable_source = + sopts.unstable_opts.show_suggestions_with_unavailable_source; let terminal_url = match sopts.unstable_opts.terminal_urls { TerminalUrl::Auto => { match (std::env::var("COLORTERM").as_deref(), std::env::var("TERM").as_deref()) { @@ -974,6 +976,9 @@ fn default_emitter(sopts: &config::Options, source_map: Arc) -> Box) -> Box TestCx<'test> { compiler.args(&["--json", "future-incompat"]); } compiler.arg("-Zui-testing"); + compiler.arg("-Zshow-suggestions-with-unavailable-source"); compiler.arg("-Zdeduplicate-diagnostics=no"); } TestMode::Ui => { @@ -1704,6 +1705,7 @@ impl<'test> TestCx<'test> { compiler.arg("-Ccodegen-units=1"); // Hide line numbers to reduce churn compiler.arg("-Zui-testing"); + compiler.arg("-Zshow-suggestions-with-unavailable-source"); compiler.arg("-Zdeduplicate-diagnostics=no"); compiler.arg("-Zwrite-long-types-to-disk=no"); // FIXME: use this for other modes too, for perf? diff --git a/tests/ui/compiletest-self-test/help-show-std-source.rs b/tests/ui/compiletest-self-test/help-show-std-source.rs new file mode 100644 index 0000000000000..0da9c25566b9c --- /dev/null +++ b/tests/ui/compiletest-self-test/help-show-std-source.rs @@ -0,0 +1,15 @@ +//@ compile-flags: -Zignore-directory-in-diagnostics-source-blocks={{src-base}}/compiletest-self-test + +// The flag above hides this test's source block from diagnostics. The suggestion should +// still be emitted, but only as a span location instead of a rendered source patch. + +macro_rules! my_assert { + ($left:expr, $right:expr) => { + if !($left == $right) {} //~ ERROR can't compare `&isize` with `{integer}` [E0277] + }; +} + +fn main() { + let y: &isize = &1; + my_assert!(y, 2); +} diff --git a/tests/ui/compiletest-self-test/help-show-std-source.stderr b/tests/ui/compiletest-self-test/help-show-std-source.stderr new file mode 100644 index 0000000000000..cc44c5180bb44 --- /dev/null +++ b/tests/ui/compiletest-self-test/help-show-std-source.stderr @@ -0,0 +1,16 @@ +error[E0277]: can't compare `&isize` with `{integer}` + --> $DIR/help-show-std-source.rs:8:19 + | + = note: no implementation for `&isize == {integer}` + ::: $DIR/help-show-std-source.rs:14:4 + | + = note: in this macro invocation + | + = help: the trait `PartialEq<{integer}>` is not implemented for `&isize` + = note: this error originates in the macro `my_assert` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider dereferencing here + --> $DIR/help-show-std-source.rs:14:15 + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0277`.