2019-06-05 21:13:56 +02:00
|
|
|
//! Emit diagnostics using the `annotate-snippets` library
|
|
|
|
|
//!
|
|
|
|
|
//! This is the equivalent of `./emitter.rs` but making use of the
|
|
|
|
|
//! [`annotate-snippets`][annotate_snippets] library instead of building the output ourselves.
|
|
|
|
|
//!
|
|
|
|
|
//! [annotate_snippets]: https://docs.rs/crate/annotate-snippets/
|
2019-05-31 22:01:27 +02:00
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
use std::borrow::Cow;
|
|
|
|
|
use std::error::Report;
|
|
|
|
|
use std::fmt::Debug;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::io::Write;
|
2025-02-03 06:44:41 +03:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
use annotate_snippets::renderer::DEFAULT_TERM_WIDTH;
|
|
|
|
|
use annotate_snippets::{AnnotationKind, Group, Origin, Padding, Patch, Renderer, Snippet};
|
|
|
|
|
use anstream::ColorChoice;
|
|
|
|
|
use derive_setters::Setters;
|
|
|
|
|
use rustc_data_structures::sync::IntoDynSyncSend;
|
|
|
|
|
use rustc_error_messages::{FluentArgs, SpanLabel};
|
|
|
|
|
use rustc_lint_defs::pluralize;
|
2019-12-31 20:15:40 +03:00
|
|
|
use rustc_span::source_map::SourceMap;
|
2025-03-29 09:48:39 -06:00
|
|
|
use rustc_span::{BytePos, FileName, Pos, SourceFile, Span};
|
|
|
|
|
use tracing::debug;
|
2024-07-29 08:13:50 +10:00
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
use crate::emitter::{
|
|
|
|
|
ConfusionType, Destination, MAX_SUGGESTIONS, OutputTheme, detect_confusion_type, is_different,
|
|
|
|
|
normalize_whitespace, should_show_source_code,
|
|
|
|
|
};
|
2024-11-09 18:30:13 +00:00
|
|
|
use crate::registry::Registry;
|
2025-06-19 13:02:04 -05:00
|
|
|
use crate::translation::{Translator, to_fluent_args};
|
2022-03-26 07:27:43 +00:00
|
|
|
use crate::{
|
2025-06-19 13:02:04 -05:00
|
|
|
CodeSuggestion, DiagInner, DiagMessage, Emitter, ErrCode, Level, MultiSpan, Style, Subdiag,
|
2025-03-29 09:48:39 -06:00
|
|
|
SuggestionStyle, TerminalUrl,
|
2022-03-26 07:27:43 +00:00
|
|
|
};
|
2019-05-31 22:01:27 +02:00
|
|
|
|
2019-06-05 21:13:56 +02:00
|
|
|
/// Generates diagnostics using annotate-snippet
|
2025-03-29 09:48:39 -06:00
|
|
|
#[derive(Setters)]
|
2024-01-05 10:37:44 +11:00
|
|
|
pub struct AnnotateSnippetEmitter {
|
2025-03-29 09:48:39 -06:00
|
|
|
#[setters(skip)]
|
|
|
|
|
dst: IntoDynSyncSend<Destination>,
|
|
|
|
|
sm: Option<Arc<SourceMap>>,
|
|
|
|
|
#[setters(skip)]
|
2025-06-19 13:02:04 -05:00
|
|
|
translator: Translator,
|
2019-05-31 22:01:27 +02:00
|
|
|
short_message: bool,
|
|
|
|
|
ui_testing: bool,
|
2025-03-29 09:48:39 -06:00
|
|
|
ignored_directories_in_source_blocks: Vec<String>,
|
|
|
|
|
diagnostic_width: Option<usize>,
|
2019-09-07 09:57:11 -04:00
|
|
|
|
2019-12-15 17:12:30 +02:00
|
|
|
macro_backtrace: bool,
|
2025-03-29 09:48:39 -06:00
|
|
|
track_diagnostics: bool,
|
|
|
|
|
terminal_url: TerminalUrl,
|
|
|
|
|
theme: OutputTheme,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Debug for AnnotateSnippetEmitter {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.debug_struct("AnnotateSnippetEmitter")
|
|
|
|
|
.field("short_message", &self.short_message)
|
|
|
|
|
.field("ui_testing", &self.ui_testing)
|
|
|
|
|
.field(
|
|
|
|
|
"ignored_directories_in_source_blocks",
|
|
|
|
|
&self.ignored_directories_in_source_blocks,
|
|
|
|
|
)
|
|
|
|
|
.field("diagnostic_width", &self.diagnostic_width)
|
|
|
|
|
.field("macro_backtrace", &self.macro_backtrace)
|
|
|
|
|
.field("track_diagnostics", &self.track_diagnostics)
|
|
|
|
|
.field("terminal_url", &self.terminal_url)
|
|
|
|
|
.field("theme", &self.theme)
|
|
|
|
|
.finish()
|
|
|
|
|
}
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-05 10:37:44 +11:00
|
|
|
impl Emitter for AnnotateSnippetEmitter {
|
2019-05-31 22:01:27 +02:00
|
|
|
/// The entry point for the diagnostics generation
|
2024-11-09 18:30:13 +00:00
|
|
|
fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) {
|
2024-02-16 06:07:49 +11:00
|
|
|
let fluent_args = to_fluent_args(diag.args.iter());
|
2022-03-26 07:27:43 +00:00
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
if self.track_diagnostics && diag.span.has_primary_spans() && !diag.span.is_dummy() {
|
|
|
|
|
diag.children.insert(0, diag.emitted_at_sub_diag());
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-08 15:59:05 -04:00
|
|
|
let mut suggestions = diag.suggestions.unwrap_tag();
|
2024-02-02 15:44:22 +11:00
|
|
|
self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args);
|
2019-05-31 22:01:27 +02:00
|
|
|
|
2019-12-15 17:47:51 +02:00
|
|
|
self.fix_multispans_in_extern_macros_and_render_macro_backtrace(
|
2024-02-02 15:44:22 +11:00
|
|
|
&mut diag.span,
|
|
|
|
|
&mut diag.children,
|
2019-10-15 08:12:55 +02:00
|
|
|
&diag.level,
|
2019-12-15 17:12:30 +02:00
|
|
|
self.macro_backtrace,
|
2019-09-07 09:57:11 -04:00
|
|
|
);
|
2019-12-22 17:42:04 -05:00
|
|
|
|
2019-10-15 08:12:55 +02:00
|
|
|
self.emit_messages_default(
|
|
|
|
|
&diag.level,
|
2023-12-20 17:12:17 +11:00
|
|
|
&diag.messages,
|
2022-03-26 07:27:43 +00:00
|
|
|
&fluent_args,
|
2019-10-15 08:12:55 +02:00
|
|
|
&diag.code,
|
2024-02-02 15:44:22 +11:00
|
|
|
&diag.span,
|
|
|
|
|
&diag.children,
|
2025-03-29 09:48:39 -06:00
|
|
|
suggestions,
|
2019-05-31 22:01:27 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 11:03:52 +11:00
|
|
|
fn source_map(&self) -> Option<&SourceMap> {
|
2025-03-29 09:48:39 -06:00
|
|
|
self.sm.as_deref()
|
2019-10-13 21:48:39 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-31 22:01:27 +02:00
|
|
|
fn should_show_explain(&self) -> bool {
|
|
|
|
|
!self.short_message
|
|
|
|
|
}
|
2025-06-19 13:02:04 -05:00
|
|
|
|
|
|
|
|
fn translator(&self) -> &Translator {
|
|
|
|
|
&self.translator
|
|
|
|
|
}
|
2019-05-31 22:01:27 +02:00
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
fn supports_color(&self) -> bool {
|
|
|
|
|
false
|
|
|
|
|
}
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-29 09:48:39 -06:00
|
|
|
fn annotation_level_for_level(level: Level) -> annotate_snippets::level::Level<'static> {
|
2020-05-08 22:48:26 +02:00
|
|
|
match level {
|
2025-03-29 09:48:39 -06:00
|
|
|
Level::Bug | Level::DelayedBug => {
|
|
|
|
|
annotate_snippets::Level::ERROR.with_name("error: internal compiler error")
|
2024-08-19 20:20:48 +00:00
|
|
|
}
|
2025-03-29 09:48:39 -06:00
|
|
|
Level::Fatal | Level::Error => annotate_snippets::level::ERROR,
|
|
|
|
|
Level::ForceWarning | Level::Warning => annotate_snippets::Level::WARNING,
|
|
|
|
|
Level::Note | Level::OnceNote => annotate_snippets::Level::NOTE,
|
|
|
|
|
Level::Help | Level::OnceHelp => annotate_snippets::Level::HELP,
|
|
|
|
|
Level::FailureNote => annotate_snippets::Level::NOTE.no_name(),
|
2020-08-13 15:41:52 -04:00
|
|
|
Level::Allow => panic!("Should not call with Allow"),
|
2025-03-18 17:18:54 +00:00
|
|
|
Level::Expect => panic!("Should not call with Expect"),
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-05 10:37:44 +11:00
|
|
|
impl AnnotateSnippetEmitter {
|
2025-03-29 09:48:39 -06:00
|
|
|
pub fn new(dst: Destination, translator: Translator) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
dst: IntoDynSyncSend(dst),
|
|
|
|
|
sm: None,
|
|
|
|
|
translator,
|
|
|
|
|
short_message: false,
|
|
|
|
|
ui_testing: false,
|
|
|
|
|
ignored_directories_in_source_blocks: Vec::new(),
|
|
|
|
|
diagnostic_width: None,
|
|
|
|
|
macro_backtrace: false,
|
|
|
|
|
track_diagnostics: false,
|
|
|
|
|
terminal_url: TerminalUrl::No,
|
|
|
|
|
theme: OutputTheme::Ascii,
|
|
|
|
|
}
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|
|
|
|
|
|
2019-06-03 07:38:19 +02:00
|
|
|
fn emit_messages_default(
|
|
|
|
|
&mut self,
|
|
|
|
|
level: &Level,
|
2025-03-29 09:48:39 -06:00
|
|
|
msgs: &[(DiagMessage, Style)],
|
2022-03-26 07:27:43 +00:00
|
|
|
args: &FluentArgs<'_>,
|
Stop using `String` for error codes.
Error codes are integers, but `String` is used everywhere to represent
them. Gross!
This commit introduces `ErrCode`, an integral newtype for error codes,
replacing `String`. It also introduces a constant for every error code,
e.g. `E0123`, and removes the `error_code!` macro. The constants are
imported wherever used with `use rustc_errors::codes::*`.
With the old code, we have three different ways to specify an error code
at a use point:
```
error_code!(E0123) // macro call
struct_span_code_err!(dcx, span, E0123, "msg"); // bare ident arg to macro call
\#[diag(name, code = "E0123")] // string
struct Diag;
```
With the new code, they all use the `E0123` constant.
```
E0123 // constant
struct_span_code_err!(dcx, span, E0123, "msg"); // constant
\#[diag(name, code = E0123)] // constant
struct Diag;
```
The commit also changes the structure of the error code definitions:
- `rustc_error_codes` now just defines a higher-order macro listing the
used error codes and nothing else.
- Because that's now the only thing in the `rustc_error_codes` crate, I
moved it into the `lib.rs` file and removed the `error_codes.rs` file.
- `rustc_errors` uses that macro to define everything, e.g. the error
code constants and the `DIAGNOSTIC_TABLES`. This is in its new
`codes.rs` file.
2024-01-14 10:57:07 +11:00
|
|
|
code: &Option<ErrCode>,
|
2019-06-03 07:38:19 +02:00
|
|
|
msp: &MultiSpan,
|
2025-03-29 09:48:39 -06:00
|
|
|
children: &[Subdiag],
|
|
|
|
|
suggestions: Vec<CodeSuggestion>,
|
2019-05-31 22:01:27 +02:00
|
|
|
) {
|
2025-03-29 09:48:39 -06:00
|
|
|
let renderer = self.renderer();
|
|
|
|
|
let annotation_level = annotation_level_for_level(*level);
|
|
|
|
|
|
|
|
|
|
// If at least one portion of the message is styled, we need to
|
|
|
|
|
// "pre-style" the message
|
|
|
|
|
let mut title = if msgs.iter().any(|(_, style)| style != &crate::Style::NoStyle) {
|
|
|
|
|
annotation_level
|
|
|
|
|
.clone()
|
|
|
|
|
.secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level, args)))
|
|
|
|
|
} else {
|
|
|
|
|
annotation_level.clone().primary_title(self.translator.translate_messages(msgs, args))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(c) = code {
|
|
|
|
|
title = title.id(c.to_string());
|
|
|
|
|
if let TerminalUrl::Yes = self.terminal_url {
|
|
|
|
|
title = title.id_url(format!("https://doc.rust-lang.org/error_codes/{c}.html"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut report = vec![];
|
|
|
|
|
let mut group = Group::with_title(title);
|
|
|
|
|
|
|
|
|
|
// If we don't have span information, emit and exit
|
|
|
|
|
let Some(sm) = self.sm.as_ref() else {
|
|
|
|
|
group = group.elements(children.iter().map(|c| {
|
|
|
|
|
let msg = self.translator.translate_messages(&c.messages, args).to_string();
|
|
|
|
|
let level = annotation_level_for_level(c.level);
|
|
|
|
|
level.message(msg)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
report.push(group);
|
|
|
|
|
if let Err(e) = emit_to_destination(
|
|
|
|
|
renderer.render(&report),
|
|
|
|
|
level,
|
|
|
|
|
&mut self.dst,
|
|
|
|
|
self.short_message,
|
|
|
|
|
) {
|
|
|
|
|
panic!("failed to emit error: {e}");
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut file_ann = collect_annotations(args, msp, sm, &self.translator);
|
|
|
|
|
|
|
|
|
|
// Make sure our primary file comes first
|
|
|
|
|
let primary_span = msp.primary_span().unwrap_or_default();
|
|
|
|
|
if !primary_span.is_dummy() {
|
|
|
|
|
let primary_lo = sm.lookup_char_pos(primary_span.lo());
|
|
|
|
|
if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) {
|
|
|
|
|
file_ann.swap(0, pos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() {
|
|
|
|
|
if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) {
|
|
|
|
|
if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
|
|
|
|
|
group = group.element(snippet);
|
|
|
|
|
}
|
|
|
|
|
// we can't annotate anything if the source is unavailable.
|
|
|
|
|
} else if !self.short_message {
|
|
|
|
|
// We'll just print unannotated messages
|
|
|
|
|
group = self.unannotated_messages(
|
|
|
|
|
annotations,
|
|
|
|
|
&file.name,
|
|
|
|
|
sm,
|
|
|
|
|
file_idx,
|
|
|
|
|
&mut report,
|
|
|
|
|
group,
|
|
|
|
|
&annotation_level,
|
|
|
|
|
);
|
|
|
|
|
// If this is the last annotation for a file, and
|
|
|
|
|
// this is the last file, and the first child is a
|
|
|
|
|
// "secondary" message, we need to add padding
|
|
|
|
|
// ╭▸ /rustc/FAKE_PREFIX/library/core/src/clone.rs:236:13
|
|
|
|
|
// │
|
|
|
|
|
// ├ note: the late bound lifetime parameter
|
|
|
|
|
// │ (<- It adds *this*)
|
|
|
|
|
// ╰ warning: this was previously accepted
|
|
|
|
|
if let Some(c) = children.first()
|
|
|
|
|
&& (!c.span.has_primary_spans() && !c.span.has_span_labels())
|
|
|
|
|
{
|
|
|
|
|
group = group.element(Padding);
|
|
|
|
|
}
|
2020-05-16 04:57:40 +02:00
|
|
|
}
|
2025-03-29 09:48:39 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for c in children {
|
|
|
|
|
let level = annotation_level_for_level(c.level);
|
|
|
|
|
|
|
|
|
|
// If at least one portion of the message is styled, we need to
|
|
|
|
|
// "pre-style" the message
|
|
|
|
|
let msg = if c.messages.iter().any(|(_, style)| style != &crate::Style::NoStyle) {
|
|
|
|
|
Cow::Owned(self.pre_style_msgs(&c.messages, c.level, args))
|
2020-05-08 22:48:26 +02:00
|
|
|
} else {
|
2025-03-29 09:48:39 -06:00
|
|
|
self.translator.translate_messages(&c.messages, args)
|
2020-05-08 22:48:26 +02:00
|
|
|
};
|
2025-03-29 09:48:39 -06:00
|
|
|
|
|
|
|
|
// This is a secondary message with no span info
|
|
|
|
|
if !c.span.has_primary_spans() && !c.span.has_span_labels() {
|
|
|
|
|
group = group.element(level.clone().message(msg));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
report.push(std::mem::replace(
|
|
|
|
|
&mut group,
|
|
|
|
|
Group::with_title(level.clone().secondary_title(msg)),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let mut file_ann = collect_annotations(args, &c.span, sm, &self.translator);
|
|
|
|
|
let primary_span = c.span.primary_span().unwrap_or_default();
|
|
|
|
|
if !primary_span.is_dummy() {
|
|
|
|
|
let primary_lo = sm.lookup_char_pos(primary_span.lo());
|
|
|
|
|
if let Ok(pos) =
|
|
|
|
|
file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name))
|
|
|
|
|
{
|
|
|
|
|
file_ann.swap(0, pos);
|
|
|
|
|
}
|
2020-05-16 04:57:40 +02:00
|
|
|
}
|
2025-03-29 09:48:39 -06:00
|
|
|
|
|
|
|
|
for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() {
|
|
|
|
|
if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) {
|
|
|
|
|
if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
|
|
|
|
|
group = group.element(snippet);
|
|
|
|
|
}
|
|
|
|
|
// we can't annotate anything if the source is unavailable.
|
|
|
|
|
} else if !self.short_message {
|
|
|
|
|
// We'll just print unannotated messages
|
|
|
|
|
group = self.unannotated_messages(
|
|
|
|
|
annotations,
|
|
|
|
|
&file.name,
|
|
|
|
|
sm,
|
|
|
|
|
file_idx,
|
|
|
|
|
&mut report,
|
|
|
|
|
group,
|
|
|
|
|
&level,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let suggestions_expected = suggestions
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|s| {
|
|
|
|
|
matches!(
|
|
|
|
|
s.style,
|
|
|
|
|
SuggestionStyle::HideCodeInline
|
|
|
|
|
| SuggestionStyle::ShowCode
|
|
|
|
|
| SuggestionStyle::ShowAlways
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.count();
|
|
|
|
|
for suggestion in suggestions {
|
|
|
|
|
match suggestion.style {
|
|
|
|
|
SuggestionStyle::CompletelyHidden => {
|
|
|
|
|
// do not display this suggestion, it is meant only for tools
|
|
|
|
|
}
|
|
|
|
|
SuggestionStyle::HideCodeAlways => {
|
|
|
|
|
let msg = self
|
|
|
|
|
.translator
|
|
|
|
|
.translate_messages(&[(suggestion.msg.to_owned(), Style::HeaderMsg)], args);
|
|
|
|
|
group = group.element(annotate_snippets::Level::HELP.message(msg));
|
|
|
|
|
}
|
|
|
|
|
SuggestionStyle::HideCodeInline
|
|
|
|
|
| SuggestionStyle::ShowCode
|
|
|
|
|
| SuggestionStyle::ShowAlways => {
|
|
|
|
|
let substitutions = suggestion
|
|
|
|
|
.substitutions
|
2020-05-08 22:48:26 +02:00
|
|
|
.into_iter()
|
2025-03-29 09:48:39 -06:00
|
|
|
.filter_map(|mut subst| {
|
|
|
|
|
// Suggestions coming from macros can have malformed spans. This is a heavy
|
|
|
|
|
// handed approach to avoid ICEs by ignoring the suggestion outright.
|
|
|
|
|
let invalid =
|
|
|
|
|
subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err());
|
|
|
|
|
if invalid {
|
|
|
|
|
debug!("suggestion contains an invalid span: {:?}", subst);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assumption: all spans are in the same file, and all spans
|
|
|
|
|
// are disjoint. Sort in ascending order.
|
|
|
|
|
subst.parts.sort_by_key(|part| part.span.lo());
|
|
|
|
|
// Verify the assumption that all spans are disjoint
|
|
|
|
|
assert_eq!(
|
|
|
|
|
subst.parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)),
|
|
|
|
|
None,
|
|
|
|
|
"all spans must be disjoint",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Account for cases where we are suggesting the same code that's already
|
|
|
|
|
// there. This shouldn't happen often, but in some cases for multipart
|
|
|
|
|
// suggestions it's much easier to handle it here than in the origin.
|
|
|
|
|
subst.parts.retain(|p| is_different(sm, &p.snippet, p.span));
|
|
|
|
|
|
|
|
|
|
let item_span = subst.parts.first()?;
|
|
|
|
|
let file = sm.lookup_source_file(item_span.span.lo());
|
|
|
|
|
if !invalid
|
|
|
|
|
&& should_show_source_code(
|
|
|
|
|
&self.ignored_directories_in_source_blocks,
|
|
|
|
|
sm,
|
|
|
|
|
&file,
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
Some(subst)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
if substitutions.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let mut msg = self
|
|
|
|
|
.translator
|
|
|
|
|
.translate_message(&suggestion.msg, args)
|
|
|
|
|
.map_err(Report::new)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
let lo = substitutions
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|sub| sub.parts.first().map(|p| p.span.lo()))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let file = sm.lookup_source_file(lo);
|
|
|
|
|
|
|
|
|
|
let filename =
|
|
|
|
|
sm.filename_for_diagnostics(&file.name).to_string_lossy().to_string();
|
|
|
|
|
|
|
|
|
|
let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS);
|
|
|
|
|
|
|
|
|
|
let subs = substitutions
|
|
|
|
|
.into_iter()
|
|
|
|
|
.take(MAX_SUGGESTIONS)
|
|
|
|
|
.filter_map(|sub| {
|
|
|
|
|
let mut confusion_type = ConfusionType::None;
|
|
|
|
|
for part in &sub.parts {
|
|
|
|
|
let part_confusion =
|
|
|
|
|
detect_confusion_type(sm, &part.snippet, part.span);
|
|
|
|
|
confusion_type = confusion_type.combine(part_confusion);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !matches!(confusion_type, ConfusionType::None) {
|
|
|
|
|
msg.push_str(confusion_type.label_text());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut parts = sub
|
|
|
|
|
.parts
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|p| {
|
|
|
|
|
if is_different(sm, &p.snippet, p.span) {
|
|
|
|
|
Some((p.span, p.snippet))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
if parts.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
let spans = parts.iter().map(|(span, _)| *span).collect::<Vec<_>>();
|
|
|
|
|
// The suggestion adds an entire line of code, ending on a newline, so we'll also
|
|
|
|
|
// print the *following* line, to provide context of what we're advising people to
|
|
|
|
|
// do. Otherwise you would only see contextless code that can be confused for
|
|
|
|
|
// already existing code, despite the colors and UI elements.
|
|
|
|
|
// We special case `#[derive(_)]\n` and other attribute suggestions, because those
|
|
|
|
|
// are the ones where context is most useful.
|
|
|
|
|
let fold = if let [(p, snippet)] = &mut parts[..]
|
|
|
|
|
&& snippet.trim().starts_with("#[")
|
|
|
|
|
// This allows for spaces to come between the attribute and the newline
|
|
|
|
|
&& snippet.trim().ends_with("]")
|
|
|
|
|
&& snippet.ends_with('\n')
|
|
|
|
|
&& p.hi() == p.lo()
|
|
|
|
|
&& let Ok(b) = sm.span_to_prev_source(*p)
|
|
|
|
|
&& let b = b.rsplit_once('\n').unwrap_or_else(|| ("", &b)).1
|
|
|
|
|
&& b.trim().is_empty()
|
|
|
|
|
{
|
|
|
|
|
// FIXME: This is a hack:
|
|
|
|
|
// The span for attribute suggestions often times points to the
|
|
|
|
|
// beginning of an item, disregarding leading whitespace. This
|
|
|
|
|
// causes the attribute to be properly indented, but leaves original
|
|
|
|
|
// item without indentation when rendered.
|
|
|
|
|
// This fixes that problem by adjusting the span to point to the start
|
|
|
|
|
// of the whitespace, and adds the whitespace to the replacement.
|
|
|
|
|
//
|
|
|
|
|
// Source: " extern "custom" fn negate(a: i64) -> i64 {\n"
|
|
|
|
|
// Span: 4..4
|
|
|
|
|
// Replacement: "#[unsafe(naked)]\n"
|
|
|
|
|
//
|
|
|
|
|
// Before:
|
|
|
|
|
// help: convert this to an `#[unsafe(naked)]` function
|
|
|
|
|
// |
|
|
|
|
|
// LL + #[unsafe(naked)]
|
|
|
|
|
// LL | extern "custom" fn negate(a: i64) -> i64 {
|
|
|
|
|
// |
|
|
|
|
|
//
|
|
|
|
|
// After
|
|
|
|
|
// help: convert this to an `#[unsafe(naked)]` function
|
|
|
|
|
// |
|
|
|
|
|
// LL + #[unsafe(naked)]
|
|
|
|
|
// LL | extern "custom" fn negate(a: i64) -> i64 {
|
|
|
|
|
// |
|
|
|
|
|
if !b.is_empty() && !snippet.ends_with(b) {
|
|
|
|
|
snippet.insert_str(0, b);
|
|
|
|
|
let offset = BytePos(b.len() as u32);
|
|
|
|
|
*p = p.with_lo(p.lo() - offset).shrink_to_lo();
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
} else {
|
|
|
|
|
true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some((bounding_span, source, line_offset)) =
|
|
|
|
|
shrink_file(spans.as_slice(), &file.name, sm)
|
|
|
|
|
{
|
|
|
|
|
let adj_lo = bounding_span.lo().to_usize();
|
|
|
|
|
Some(
|
|
|
|
|
Snippet::source(source)
|
|
|
|
|
.line_start(line_offset)
|
|
|
|
|
.path(filename.clone())
|
|
|
|
|
.fold(fold)
|
|
|
|
|
.patches(parts.into_iter().map(
|
|
|
|
|
|(span, replacement)| {
|
|
|
|
|
let lo =
|
|
|
|
|
span.lo().to_usize().saturating_sub(adj_lo);
|
|
|
|
|
let hi =
|
|
|
|
|
span.hi().to_usize().saturating_sub(adj_lo);
|
|
|
|
|
|
|
|
|
|
Patch::new(lo..hi, replacement)
|
|
|
|
|
},
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-08 22:48:26 +02:00
|
|
|
})
|
2025-03-29 09:48:39 -06:00
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
if !subs.is_empty() {
|
|
|
|
|
report.push(std::mem::replace(
|
|
|
|
|
&mut group,
|
|
|
|
|
Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
group = group.elements(subs);
|
|
|
|
|
if other_suggestions > 0 {
|
|
|
|
|
group = group.element(
|
|
|
|
|
annotate_snippets::Level::NOTE.no_name().message(format!(
|
|
|
|
|
"and {} other candidate{}",
|
|
|
|
|
other_suggestions,
|
|
|
|
|
pluralize!(other_suggestions)
|
|
|
|
|
)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-19 20:20:48 +00:00
|
|
|
}
|
2020-05-08 22:48:26 +02:00
|
|
|
}
|
2025-03-29 09:48:39 -06:00
|
|
|
|
|
|
|
|
// FIXME: This hack should be removed once annotate_snippets is the
|
|
|
|
|
// default emitter.
|
|
|
|
|
if suggestions_expected > 0 && report.is_empty() {
|
|
|
|
|
group = group.element(Padding);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !group.is_empty() {
|
|
|
|
|
report.push(group);
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) =
|
|
|
|
|
emit_to_destination(renderer.render(&report), level, &mut self.dst, self.short_message)
|
|
|
|
|
{
|
|
|
|
|
panic!("failed to emit error: {e}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn renderer(&self) -> Renderer {
|
|
|
|
|
let width = if let Some(width) = self.diagnostic_width {
|
|
|
|
|
width
|
|
|
|
|
} else if self.ui_testing || cfg!(miri) {
|
|
|
|
|
DEFAULT_TERM_WIDTH
|
|
|
|
|
} else {
|
|
|
|
|
termize::dimensions().map(|(w, _)| w).unwrap_or(DEFAULT_TERM_WIDTH)
|
|
|
|
|
};
|
|
|
|
|
let decor_style = match self.theme {
|
|
|
|
|
OutputTheme::Ascii => annotate_snippets::renderer::DecorStyle::Ascii,
|
|
|
|
|
OutputTheme::Unicode => annotate_snippets::renderer::DecorStyle::Unicode,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match self.dst.current_choice() {
|
|
|
|
|
ColorChoice::AlwaysAnsi | ColorChoice::Always | ColorChoice::Auto => Renderer::styled(),
|
|
|
|
|
ColorChoice::Never => Renderer::plain(),
|
|
|
|
|
}
|
|
|
|
|
.term_width(width)
|
|
|
|
|
.anonymized_line_numbers(self.ui_testing)
|
|
|
|
|
.decor_style(decor_style)
|
|
|
|
|
.short_message(self.short_message)
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|
2025-03-29 09:48:39 -06:00
|
|
|
|
|
|
|
|
fn pre_style_msgs(
|
|
|
|
|
&self,
|
|
|
|
|
msgs: &[(DiagMessage, Style)],
|
|
|
|
|
level: Level,
|
|
|
|
|
args: &FluentArgs<'_>,
|
|
|
|
|
) -> String {
|
|
|
|
|
msgs.iter()
|
|
|
|
|
.filter_map(|(m, style)| {
|
|
|
|
|
let text = self.translator.translate_message(m, args).map_err(Report::new).unwrap();
|
|
|
|
|
let style = style.anstyle(level);
|
|
|
|
|
if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) }
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn annotated_snippet<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
annotations: Vec<Annotation>,
|
|
|
|
|
file_name: &FileName,
|
|
|
|
|
sm: &Arc<SourceMap>,
|
|
|
|
|
) -> Option<Snippet<'a, annotate_snippets::Annotation<'a>>> {
|
|
|
|
|
let spans = annotations.iter().map(|a| a.span).collect::<Vec<_>>();
|
|
|
|
|
if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) {
|
|
|
|
|
let adj_lo = bounding_span.lo().to_usize();
|
|
|
|
|
let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string();
|
|
|
|
|
Some(Snippet::source(source).line_start(offset_line).path(filename).annotations(
|
|
|
|
|
annotations.into_iter().map(move |a| {
|
|
|
|
|
let lo = a.span.lo().to_usize().saturating_sub(adj_lo);
|
|
|
|
|
let hi = a.span.hi().to_usize().saturating_sub(adj_lo);
|
|
|
|
|
let ann = a.kind.span(lo..hi);
|
|
|
|
|
if let Some(label) = a.label { ann.label(label) } else { ann }
|
|
|
|
|
}),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unannotated_messages<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
annotations: Vec<Annotation>,
|
|
|
|
|
file_name: &FileName,
|
|
|
|
|
sm: &Arc<SourceMap>,
|
|
|
|
|
file_idx: usize,
|
|
|
|
|
report: &mut Vec<Group<'a>>,
|
|
|
|
|
mut group: Group<'a>,
|
|
|
|
|
level: &annotate_snippets::level::Level<'static>,
|
|
|
|
|
) -> Group<'a> {
|
|
|
|
|
let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string();
|
|
|
|
|
let mut line_tracker = vec![];
|
|
|
|
|
for (i, a) in annotations.into_iter().enumerate() {
|
|
|
|
|
let lo = sm.lookup_char_pos(a.span.lo());
|
|
|
|
|
let hi = sm.lookup_char_pos(a.span.hi());
|
|
|
|
|
if i == 0 || (a.label.is_some()) {
|
|
|
|
|
// Render each new file after the first in its own Group
|
|
|
|
|
// ╭▸ $DIR/deriving-meta-unknown-trait.rs:1:10
|
|
|
|
|
// │
|
|
|
|
|
// LL │ #[derive(Eqr)]
|
|
|
|
|
// │ ━━━
|
|
|
|
|
// ╰╴ (<- It makes it so *this* will get printed)
|
|
|
|
|
// ╭▸ $SRC_DIR/core/src/option.rs:594:0
|
|
|
|
|
// ⸬ $SRC_DIR/core/src/option.rs:602:4
|
|
|
|
|
// │
|
|
|
|
|
// ╰ note: not covered
|
|
|
|
|
if i == 0 && file_idx != 0 {
|
|
|
|
|
report.push(std::mem::replace(&mut group, Group::with_level(level.clone())));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !line_tracker.contains(&lo.line) {
|
|
|
|
|
line_tracker.push(lo.line);
|
|
|
|
|
// ╭▸ $SRC_DIR/core/src/option.rs:594:0 (<- It adds *this*)
|
|
|
|
|
// ⸬ $SRC_DIR/core/src/option.rs:602:4
|
|
|
|
|
// │
|
|
|
|
|
// ╰ note: not covered
|
|
|
|
|
group = group.element(
|
|
|
|
|
Origin::path(filename.clone())
|
|
|
|
|
.line(sm.doctest_offset_line(file_name, lo.line))
|
|
|
|
|
.char_column(lo.col_display),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if hi.line > lo.line
|
|
|
|
|
&& a.label.as_ref().is_some_and(|l| !l.is_empty())
|
|
|
|
|
&& !line_tracker.contains(&hi.line)
|
|
|
|
|
{
|
|
|
|
|
line_tracker.push(hi.line);
|
|
|
|
|
// ╭▸ $SRC_DIR/core/src/option.rs:594:0
|
|
|
|
|
// ⸬ $SRC_DIR/core/src/option.rs:602:4 (<- It adds *this*)
|
|
|
|
|
// │
|
|
|
|
|
// ╰ note: not covered
|
|
|
|
|
group = group.element(
|
|
|
|
|
Origin::path(filename.clone())
|
|
|
|
|
.line(sm.doctest_offset_line(file_name, hi.line))
|
|
|
|
|
.char_column(hi.col_display),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(label) = a.label
|
|
|
|
|
&& !label.is_empty()
|
|
|
|
|
{
|
|
|
|
|
// ╭▸ $SRC_DIR/core/src/option.rs:594:0
|
|
|
|
|
// ⸬ $SRC_DIR/core/src/option.rs:602:4
|
|
|
|
|
// │ (<- It adds *this*)
|
|
|
|
|
// ╰ note: not covered (<- and *this*)
|
|
|
|
|
group = group
|
|
|
|
|
.element(Padding)
|
|
|
|
|
.element(annotate_snippets::Level::NOTE.message(label));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
group
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn emit_to_destination(
|
|
|
|
|
rendered: String,
|
|
|
|
|
lvl: &Level,
|
|
|
|
|
dst: &mut Destination,
|
|
|
|
|
short_message: bool,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
use crate::lock;
|
|
|
|
|
let _buffer_lock = lock::acquire_global_lock("rustc_errors");
|
|
|
|
|
writeln!(dst, "{rendered}")?;
|
|
|
|
|
if !short_message && !lvl.is_failure_note() {
|
|
|
|
|
writeln!(dst)?;
|
|
|
|
|
}
|
|
|
|
|
dst.flush()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Annotation {
|
|
|
|
|
kind: AnnotationKind,
|
|
|
|
|
span: Span,
|
|
|
|
|
label: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_annotations(
|
|
|
|
|
args: &FluentArgs<'_>,
|
|
|
|
|
msp: &MultiSpan,
|
|
|
|
|
sm: &Arc<SourceMap>,
|
|
|
|
|
translator: &Translator,
|
|
|
|
|
) -> Vec<(Arc<SourceFile>, Vec<Annotation>)> {
|
|
|
|
|
let mut output: Vec<(Arc<SourceFile>, Vec<Annotation>)> = vec![];
|
|
|
|
|
|
|
|
|
|
for SpanLabel { span, is_primary, label } in msp.span_labels() {
|
|
|
|
|
// If we don't have a useful span, pick the primary span if that exists.
|
|
|
|
|
// Worst case we'll just print an error at the top of the main file.
|
|
|
|
|
let span = match (span.is_dummy(), msp.primary_span()) {
|
|
|
|
|
(_, None) | (false, _) => span,
|
|
|
|
|
(true, Some(span)) => span,
|
|
|
|
|
};
|
|
|
|
|
let file = sm.lookup_source_file(span.lo());
|
|
|
|
|
|
|
|
|
|
let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context };
|
|
|
|
|
|
|
|
|
|
let label = label.as_ref().map(|m| {
|
|
|
|
|
normalize_whitespace(
|
|
|
|
|
&translator.translate_message(m, args).map_err(Report::new).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let ann = Annotation { kind, span, label };
|
|
|
|
|
if sm.is_valid_span(ann.span).is_ok() {
|
|
|
|
|
// Look through each of our files for the one we're adding to. We
|
|
|
|
|
// use each files `stable_id` to avoid issues with file name
|
|
|
|
|
// collisions when multiple versions of the same crate are present
|
|
|
|
|
// in the dependency graph
|
|
|
|
|
if let Some((_, annotations)) =
|
|
|
|
|
output.iter_mut().find(|(f, _)| f.stable_id == file.stable_id)
|
|
|
|
|
{
|
|
|
|
|
annotations.push(ann);
|
|
|
|
|
} else {
|
|
|
|
|
output.push((file, vec![ann]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn shrink_file(
|
|
|
|
|
spans: &[Span],
|
|
|
|
|
file_name: &FileName,
|
|
|
|
|
sm: &Arc<SourceMap>,
|
|
|
|
|
) -> Option<(Span, String, usize)> {
|
|
|
|
|
let lo_byte = spans.iter().map(|s| s.lo()).min()?;
|
|
|
|
|
let lo_loc = sm.lookup_char_pos(lo_byte);
|
|
|
|
|
let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start;
|
|
|
|
|
|
|
|
|
|
let hi_byte = spans.iter().map(|s| s.hi()).max()?;
|
|
|
|
|
let hi_loc = sm.lookup_char_pos(hi_byte);
|
|
|
|
|
let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end;
|
|
|
|
|
|
|
|
|
|
let bounding_span = Span::with_root_ctxt(lo, hi);
|
|
|
|
|
let source = sm.span_to_snippet(bounding_span).unwrap_or_default();
|
|
|
|
|
let offset_line = sm.doctest_offset_line(file_name, lo_loc.line);
|
|
|
|
|
|
|
|
|
|
Some((bounding_span, source, offset_line))
|
2019-05-31 22:01:27 +02:00
|
|
|
}
|