| //! The current rustc diagnostics emitter. | |
| //! | |
| //! An `Emitter` takes care of generating the output from a `Diag` struct. | |
| //! | |
| //! There are various `Emitter` implementations that generate different output formats such as | |
| //! JSON and human readable output. | |
| //! | |
| //! The output types are defined in `rustc_session::config::ErrorOutputType`. | |
| use std::borrow::Cow; | |
| use std::io::prelude::*; | |
| use std::io::{self, IsTerminal}; | |
| use std::iter; | |
| use std::path::Path; | |
| use anstream::{AutoStream, ColorChoice}; | |
| use anstyle::{AnsiColor, Effects}; | |
| use rustc_data_structures::fx::FxIndexSet; | |
| use rustc_data_structures::sync::DynSend; | |
| use rustc_error_messages::DiagArgMap; | |
| use rustc_span::hygiene::{ExpnKind, MacroKind}; | |
| use rustc_span::source_map::SourceMap; | |
| use rustc_span::{FileName, SourceFile, Span}; | |
| use tracing::{debug, warn}; | |
| use crate::formatting::format_diag_message; | |
| use crate::timings::TimingRecord; | |
| use crate::{ | |
| CodeSuggestion, DiagInner, DiagMessage, Level, MultiSpan, Style, Subdiag, SuggestionStyle, | |
| }; | |
| /// Describes the way the content of the `rendered` field of the json output is generated | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| pub struct HumanReadableErrorType { | |
| pub short: bool, | |
| pub unicode: bool, | |
| } | |
| impl HumanReadableErrorType { | |
| pub fn short(&self) -> bool { | |
| self.short | |
| } | |
| } | |
| pub enum TimingEvent { | |
| Start, | |
| End, | |
| } | |
| pub type DynEmitter = dyn Emitter + DynSend; | |
| /// Emitter trait for emitting errors and other structured information. | |
| pub trait Emitter { | |
| /// Emit a structured diagnostic. | |
| fn emit_diagnostic(&mut self, diag: DiagInner); | |
| /// Emit a notification that an artifact has been output. | |
| /// Currently only supported for the JSON format. | |
| fn emit_artifact_notification(&mut self, _path: &Path, _artifact_type: &str) {} | |
| /// Emit a timestamp with start/end of a timing section. | |
| /// Currently only supported for the JSON format. | |
| fn emit_timing_section(&mut self, _record: TimingRecord, _event: TimingEvent) {} | |
| /// Emit a report about future breakage. | |
| /// Currently only supported for the JSON format. | |
| fn emit_future_breakage_report(&mut self, _diags: Vec<DiagInner>) {} | |
| /// Emit list of unused externs. | |
| /// Currently only supported for the JSON format. | |
| fn emit_unused_externs( | |
| &mut self, | |
| _lint_level: rustc_lint_defs::Level, | |
| _unused_externs: &[&str], | |
| ) { | |
| } | |
| /// Checks if should show explanations about "rustc --explain" | |
| fn should_show_explain(&self) -> bool { | |
| true | |
| } | |
| /// Checks if we can use colors in the current output stream. | |
| fn supports_color(&self) -> bool { | |
| false | |
| } | |
| fn source_map(&self) -> Option<&SourceMap>; | |
| /// Formats the substitutions of the primary_span | |
| /// | |
| /// There are a lot of conditions to this method, but in short: | |
| /// | |
| /// * If the current `DiagInner` has only one visible `CodeSuggestion`, | |
| /// we format the `help` suggestion depending on the content of the | |
| /// substitutions. In that case, we modify the span and clear the | |
| /// suggestions. | |
| /// | |
| /// * If the current `DiagInner` has multiple suggestions, | |
| /// we leave `primary_span` and the suggestions untouched. | |
| fn primary_span_formatted( | |
| &self, | |
| primary_span: &mut MultiSpan, | |
| suggestions: &mut Vec<CodeSuggestion>, | |
| fluent_args: &DiagArgMap, | |
| ) { | |
| if let Some((sugg, rest)) = suggestions.split_first() { | |
| let msg = format_diag_message(&sugg.msg, fluent_args); | |
| if rest.is_empty() | |
| // ^ if there is only one suggestion | |
| // don't display multi-suggestions as labels | |
| && let [substitution] = sugg.substitutions.as_slice() | |
| // don't display multipart suggestions as labels | |
| && let [part] = substitution.parts.as_slice() | |
| // don't display long messages as labels | |
| && msg.split_whitespace().count() < 10 | |
| // don't display multiline suggestions as labels | |
| && !part.snippet.contains('\n') | |
| && ![ | |
| // when this style is set we want the suggestion to be a message, not inline | |
| SuggestionStyle::HideCodeAlways, | |
| // trivial suggestion for tooling's sake, never shown | |
| SuggestionStyle::CompletelyHidden, | |
| // subtle suggestion, never shown inline | |
| SuggestionStyle::ShowAlways, | |
| ].contains(&sugg.style) | |
| { | |
| let snippet = part.snippet.trim(); | |
| let msg = if snippet.is_empty() || sugg.style.hide_inline() { | |
| // This substitution is only removal OR we explicitly don't want to show the | |
| // code inline (`hide_inline`). Therefore, we don't show the substitution. | |
| format!("help: {msg}") | |
| } else { | |
| // Show the default suggestion text with the substitution | |
| let confusion_type = self | |
| .source_map() | |
| .map(|sm| detect_confusion_type(sm, snippet, part.span)) | |
| .unwrap_or(ConfusionType::None); | |
| format!("help: {}{}: `{}`", msg, confusion_type.label_text(), snippet,) | |
| }; | |
| primary_span.push_span_label(part.span, msg); | |
| // We return only the modified primary_span | |
| suggestions.clear(); | |
| } else { | |
| // if there are multiple suggestions, print them all in full | |
| // to be consistent. We could try to figure out if we can | |
| // make one (or the first one) inline, but that would give | |
| // undue importance to a semi-random suggestion | |
| } | |
| } else { | |
| // do nothing | |
| } | |
| } | |
| fn fix_multispans_in_extern_macros_and_render_macro_backtrace( | |
| &self, | |
| span: &mut MultiSpan, | |
| children: &mut Vec<Subdiag>, | |
| level: &Level, | |
| backtrace: bool, | |
| ) { | |
| // Check for spans in macros, before `fix_multispans_in_extern_macros` | |
| // has a chance to replace them. | |
| let has_macro_spans: Vec<_> = iter::once(&*span) | |
| .chain(children.iter().map(|child| &child.span)) | |
| .flat_map(|span| span.primary_spans()) | |
| .flat_map(|sp| sp.macro_backtrace()) | |
| .filter_map(|expn_data| { | |
| match expn_data.kind { | |
| ExpnKind::Root => None, | |
| // Skip past non-macro entries, just in case there | |
| // are some which do actually involve macros. | |
| ExpnKind::Desugaring(..) | ExpnKind::AstPass(..) => None, | |
| ExpnKind::Macro(macro_kind, name) => { | |
| Some((macro_kind, name, expn_data.hide_backtrace)) | |
| } | |
| } | |
| }) | |
| .collect(); | |
| if !backtrace { | |
| self.fix_multispans_in_extern_macros(span, children); | |
| } | |
| self.render_multispans_macro_backtrace(span, children, backtrace); | |
| if !backtrace { | |
| // Skip builtin macros, as their expansion isn't relevant to the end user. This includes | |
| // actual intrinsics, like `asm!`. | |
| if let Some((macro_kind, name, _)) = has_macro_spans.first() | |
| && let Some((_, _, false)) = has_macro_spans.last() | |
| { | |
| // Mark the actual macro this originates from | |
| let and_then = if let Some((macro_kind, last_name, _)) = has_macro_spans.last() | |
| && last_name != name | |
| { | |
| let descr = macro_kind.descr(); | |
| format!(" which comes from the expansion of the {descr} `{last_name}`") | |
| } else { | |
| "".to_string() | |
| }; | |
| let descr = macro_kind.descr(); | |
| let msg = format!( | |
| "this {level} originates in the {descr} `{name}`{and_then} \ | |
| (in Nightly builds, run with -Z macro-backtrace for more info)", | |
| ); | |
| children.push(Subdiag { | |
| level: Level::Note, | |
| messages: vec![(DiagMessage::from(msg), Style::NoStyle)], | |
| span: MultiSpan::new(), | |
| }); | |
| } | |
| } | |
| } | |
| fn render_multispans_macro_backtrace( | |
| &self, | |
| span: &mut MultiSpan, | |
| children: &mut Vec<Subdiag>, | |
| backtrace: bool, | |
| ) { | |
| for span in iter::once(span).chain(children.iter_mut().map(|child| &mut child.span)) { | |
| self.render_multispan_macro_backtrace(span, backtrace); | |
| } | |
| } | |
| fn render_multispan_macro_backtrace(&self, span: &mut MultiSpan, always_backtrace: bool) { | |
| let mut new_labels = FxIndexSet::default(); | |
| for &sp in span.primary_spans() { | |
| if sp.is_dummy() { | |
| continue; | |
| } | |
| // FIXME(eddyb) use `retain` on `macro_backtrace` to remove all the | |
| // entries we don't want to print, to make sure the indices being | |
| // printed are contiguous (or omitted if there's only one entry). | |
| let macro_backtrace: Vec<_> = sp.macro_backtrace().collect(); | |
| for (i, trace) in macro_backtrace.iter().rev().enumerate() { | |
| if trace.def_site.is_dummy() { | |
| continue; | |
| } | |
| if always_backtrace { | |
| new_labels.insert(( | |
| trace.def_site, | |
| format!( | |
| "in this expansion of `{}`{}", | |
| trace.kind.descr(), | |
| if macro_backtrace.len() > 1 { | |
| // if macro_backtrace.len() == 1 it'll be | |
| // pointed at by "in this macro invocation" | |
| format!(" (#{})", i + 1) | |
| } else { | |
| String::new() | |
| }, | |
| ), | |
| )); | |
| } | |
| // Don't add a label on the call site if the diagnostic itself | |
| // already points to (a part of) that call site, as the label | |
| // is meant for showing the relevant invocation when the actual | |
| // diagnostic is pointing to some part of macro definition. | |
| // | |
| // This also handles the case where an external span got replaced | |
| // with the call site span by `fix_multispans_in_extern_macros`. | |
| // | |
| // NB: `-Zmacro-backtrace` overrides this, for uniformity, as the | |
| // "in this expansion of" label above is always added in that mode, | |
| // and it needs an "in this macro invocation" label to match that. | |
| let redundant_span = trace.call_site.contains(sp); | |
| if !redundant_span || always_backtrace { | |
| let msg: Cow<'static, _> = match trace.kind { | |
| ExpnKind::Macro(MacroKind::Attr, _) => { | |
| "this attribute macro expansion".into() | |
| } | |
| ExpnKind::Macro(MacroKind::Derive, _) => { | |
| "this derive macro expansion".into() | |
| } | |
| ExpnKind::Macro(MacroKind::Bang, _) => "this macro invocation".into(), | |
| ExpnKind::Root => "the crate root".into(), | |
| ExpnKind::AstPass(kind) => kind.descr().into(), | |
| ExpnKind::Desugaring(kind) => { | |
| format!("this {} desugaring", kind.descr()).into() | |
| } | |
| }; | |
| new_labels.insert(( | |
| trace.call_site, | |
| format!( | |
| "in {}{}", | |
| msg, | |
| if macro_backtrace.len() > 1 && always_backtrace { | |
| // only specify order when the macro | |
| // backtrace is multiple levels deep | |
| format!(" (#{})", i + 1) | |
| } else { | |
| String::new() | |
| }, | |
| ), | |
| )); | |
| } | |
| if !always_backtrace { | |
| break; | |
| } | |
| } | |
| } | |
| for (label_span, label_text) in new_labels { | |
| span.push_span_label(label_span, label_text); | |
| } | |
| } | |
| // This does a small "fix" for multispans by looking to see if it can find any that | |
| // point directly at external macros. Since these are often difficult to read, | |
| // this will change the span to point at the use site. | |
| fn fix_multispans_in_extern_macros(&self, span: &mut MultiSpan, children: &mut Vec<Subdiag>) { | |
| debug!("fix_multispans_in_extern_macros: before: span={:?} children={:?}", span, children); | |
| self.fix_multispan_in_extern_macros(span); | |
| for child in children.iter_mut() { | |
| self.fix_multispan_in_extern_macros(&mut child.span); | |
| } | |
| debug!("fix_multispans_in_extern_macros: after: span={:?} children={:?}", span, children); | |
| } | |
| // This "fixes" MultiSpans that contain `Span`s pointing to locations inside of external macros. | |
| // Since these locations are often difficult to read, | |
| // we move these spans from the external macros to their corresponding use site. | |
| fn fix_multispan_in_extern_macros(&self, span: &mut MultiSpan) { | |
| let Some(source_map) = self.source_map() else { return }; | |
| // First, find all the spans in external macros and point instead at their use site. | |
| let replacements: Vec<(Span, Span)> = span | |
| .primary_spans() | |
| .iter() | |
| .copied() | |
| .chain(span.span_labels().iter().map(|sp_label| sp_label.span)) | |
| .filter_map(|sp| { | |
| if !sp.is_dummy() && source_map.is_imported(sp) { | |
| let mut span = sp; | |
| while let Some(callsite) = span.parent_callsite() { | |
| span = callsite; | |
| if !source_map.is_imported(span) { | |
| return Some((sp, span)); | |
| } | |
| } | |
| } | |
| None | |
| }) | |
| .collect(); | |
| // After we have them, make sure we replace these 'bad' def sites with their use sites. | |
| for (from, to) in replacements { | |
| span.replace(from, to); | |
| } | |
| } | |
| } | |
| /// An emitter that adds a note to each diagnostic. | |
| pub struct EmitterWithNote { | |
| pub emitter: Box<dyn Emitter + DynSend>, | |
| pub note: String, | |
| } | |
| impl Emitter for EmitterWithNote { | |
| fn source_map(&self) -> Option<&SourceMap> { | |
| None | |
| } | |
| fn emit_diagnostic(&mut self, mut diag: DiagInner) { | |
| diag.sub(Level::Note, self.note.clone(), MultiSpan::new()); | |
| self.emitter.emit_diagnostic(diag); | |
| } | |
| } | |
| pub struct SilentEmitter; | |
| impl Emitter for SilentEmitter { | |
| fn source_map(&self) -> Option<&SourceMap> { | |
| None | |
| } | |
| fn emit_diagnostic(&mut self, _diag: DiagInner) {} | |
| } | |
| /// Maximum number of suggestions to be shown | |
| /// | |
| /// Arbitrary, but taken from trait import suggestion limit | |
| pub const MAX_SUGGESTIONS: usize = 4; | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| pub enum ColorConfig { | |
| Auto, | |
| Always, | |
| Never, | |
| } | |
| impl ColorConfig { | |
| pub fn to_color_choice(self) -> ColorChoice { | |
| match self { | |
| ColorConfig::Always => { | |
| if io::stderr().is_terminal() { | |
| ColorChoice::Always | |
| } else { | |
| ColorChoice::AlwaysAnsi | |
| } | |
| } | |
| ColorConfig::Never => ColorChoice::Never, | |
| ColorConfig::Auto if io::stderr().is_terminal() => ColorChoice::Auto, | |
| ColorConfig::Auto => ColorChoice::Never, | |
| } | |
| } | |
| } | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| pub enum OutputTheme { | |
| Ascii, | |
| Unicode, | |
| } | |
| // We replace some characters so the CLI output is always consistent and underlines aligned. | |
| // Keep the following list in sync with `rustc_span::char_width`. | |
| const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ | |
| // In terminals without Unicode support the following will be garbled, but in *all* terminals | |
| // the underlying codepoint will be as well. We could gate this replacement behind a "unicode | |
| // support" gate. | |
| ('\0', "␀"), | |
| ('\u{0001}', "␁"), | |
| ('\u{0002}', "␂"), | |
| ('\u{0003}', "␃"), | |
| ('\u{0004}', "␄"), | |
| ('\u{0005}', "␅"), | |
| ('\u{0006}', "␆"), | |
| ('\u{0007}', "␇"), | |
| ('\u{0008}', "␈"), | |
| ('\t', " "), // We do our own tab replacement | |
| ('\u{000b}', "␋"), | |
| ('\u{000c}', "␌"), | |
| ('\u{000d}', "␍"), | |
| ('\u{000e}', "␎"), | |
| ('\u{000f}', "␏"), | |
| ('\u{0010}', "␐"), | |
| ('\u{0011}', "␑"), | |
| ('\u{0012}', "␒"), | |
| ('\u{0013}', "␓"), | |
| ('\u{0014}', "␔"), | |
| ('\u{0015}', "␕"), | |
| ('\u{0016}', "␖"), | |
| ('\u{0017}', "␗"), | |
| ('\u{0018}', "␘"), | |
| ('\u{0019}', "␙"), | |
| ('\u{001a}', "␚"), | |
| ('\u{001b}', "␛"), | |
| ('\u{001c}', "␜"), | |
| ('\u{001d}', "␝"), | |
| ('\u{001e}', "␞"), | |
| ('\u{001f}', "␟"), | |
| ('\u{007f}', "␡"), | |
| ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. | |
| ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently | |
| ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk | |
| ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. | |
| ('\u{202d}', "�"), | |
| ('\u{202e}', "�"), | |
| ('\u{2066}', "�"), | |
| ('\u{2067}', "�"), | |
| ('\u{2068}', "�"), | |
| ('\u{2069}', "�"), | |
| ]; | |
| pub(crate) fn normalize_whitespace(s: &str) -> String { | |
| const { | |
| let mut i = 1; | |
| while i < OUTPUT_REPLACEMENTS.len() { | |
| assert!( | |
| OUTPUT_REPLACEMENTS[i - 1].0 < OUTPUT_REPLACEMENTS[i].0, | |
| "The OUTPUT_REPLACEMENTS array must be sorted (for binary search to work) \ | |
| and must contain no duplicate entries" | |
| ); | |
| i += 1; | |
| } | |
| } | |
| // Scan the input string for a character in the ordered table above. | |
| // If it's present, replace it with its alternative string (it can be more than 1 char!). | |
| // Otherwise, retain the input char. | |
| s.chars().fold(String::with_capacity(s.len()), |mut s, c| { | |
| match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { | |
| Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), | |
| _ => s.push(c), | |
| } | |
| s | |
| }) | |
| } | |
| pub type Destination = AutoStream<Box<dyn Write + Send>>; | |
| struct Buffy { | |
| buffer_writer: std::io::Stderr, | |
| buffer: Vec<u8>, | |
| } | |
| impl Write for Buffy { | |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { | |
| self.buffer.write(buf) | |
| } | |
| fn flush(&mut self) -> io::Result<()> { | |
| self.buffer_writer.write_all(&self.buffer)?; | |
| self.buffer.clear(); | |
| Ok(()) | |
| } | |
| } | |
| impl Drop for Buffy { | |
| fn drop(&mut self) { | |
| if !self.buffer.is_empty() { | |
| self.flush().unwrap(); | |
| panic!("buffers need to be flushed in order to print their contents"); | |
| } | |
| } | |
| } | |
| pub fn stderr_destination(color: ColorConfig) -> Destination { | |
| let buffer_writer = std::io::stderr(); | |
| // We need to resolve `ColorChoice::Auto` before `Box`ing since | |
| // `ColorChoice::Auto` on `dyn Write` will always resolve to `Never` | |
| let choice = get_stderr_color_choice(color, &buffer_writer); | |
| // On Windows we'll be performing global synchronization on the entire | |
| // system for emitting rustc errors, so there's no need to buffer | |
| // anything. | |
| // | |
| // On non-Windows we rely on the atomicity of `write` to ensure errors | |
| // don't get all jumbled up. | |
| if cfg!(windows) { | |
| AutoStream::new(Box::new(buffer_writer), choice) | |
| } else { | |
| let buffer = Vec::new(); | |
| AutoStream::new(Box::new(Buffy { buffer_writer, buffer }), choice) | |
| } | |
| } | |
| pub fn get_stderr_color_choice(color: ColorConfig, stderr: &std::io::Stderr) -> ColorChoice { | |
| let choice = color.to_color_choice(); | |
| if matches!(choice, ColorChoice::Auto) { AutoStream::choice(stderr) } else { choice } | |
| } | |
| /// On Windows, BRIGHT_BLUE is hard to read on black. Use cyan instead. | |
| /// | |
| /// See #36178. | |
| const BRIGHT_BLUE: anstyle::Style = if cfg!(windows) { | |
| AnsiColor::BrightCyan.on_default() | |
| } else { | |
| AnsiColor::BrightBlue.on_default() | |
| }; | |
| impl Style { | |
| pub(crate) fn anstyle(&self, lvl: Level) -> anstyle::Style { | |
| match self { | |
| Style::Addition => AnsiColor::BrightGreen.on_default(), | |
| Style::Removal => AnsiColor::BrightRed.on_default(), | |
| Style::LineAndColumn => anstyle::Style::new(), | |
| Style::LineNumber => BRIGHT_BLUE.effects(Effects::BOLD), | |
| Style::Quotation => anstyle::Style::new(), | |
| Style::MainHeaderMsg => if cfg!(windows) { | |
| AnsiColor::BrightWhite.on_default() | |
| } else { | |
| anstyle::Style::new() | |
| } | |
| .effects(Effects::BOLD), | |
| Style::UnderlinePrimary | Style::LabelPrimary => lvl.color().effects(Effects::BOLD), | |
| Style::UnderlineSecondary | Style::LabelSecondary => BRIGHT_BLUE.effects(Effects::BOLD), | |
| Style::HeaderMsg | Style::NoStyle => anstyle::Style::new(), | |
| Style::Level(lvl) => lvl.color().effects(Effects::BOLD), | |
| Style::Highlight => AnsiColor::Magenta.on_default().effects(Effects::BOLD), | |
| } | |
| } | |
| } | |
| /// Whether the original and suggested code are the same. | |
| pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool { | |
| let found = match sm.span_to_snippet(sp) { | |
| Ok(snippet) => snippet, | |
| Err(e) => { | |
| warn!(error = ?e, "Invalid span {:?}", sp); | |
| return true; | |
| } | |
| }; | |
| found != suggested | |
| } | |
| /// Whether the original and suggested code are visually similar enough to warrant extra wording. | |
| pub fn detect_confusion_type(sm: &SourceMap, suggested: &str, sp: Span) -> ConfusionType { | |
| let found = match sm.span_to_snippet(sp) { | |
| Ok(snippet) => snippet, | |
| Err(e) => { | |
| warn!(error = ?e, "Invalid span {:?}", sp); | |
| return ConfusionType::None; | |
| } | |
| }; | |
| let mut has_case_confusion = false; | |
| let mut has_digit_letter_confusion = false; | |
| if found.len() == suggested.len() { | |
| let mut has_case_diff = false; | |
| let mut has_digit_letter_confusable = false; | |
| let mut has_other_diff = false; | |
| // Letters whose lowercase version is very similar to the uppercase | |
| // version. | |
| let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z']; | |
| let digit_letter_confusables = [('0', 'O'), ('1', 'l'), ('5', 'S'), ('8', 'B'), ('9', 'g')]; | |
| for (f, s) in iter::zip(found.chars(), suggested.chars()) { | |
| if f != s { | |
| if f.eq_ignore_ascii_case(&s) { | |
| // Check for case differences (any character that differs only in case) | |
| if ascii_confusables.contains(&f) || ascii_confusables.contains(&s) { | |
| has_case_diff = true; | |
| } else { | |
| has_other_diff = true; | |
| } | |
| } else if digit_letter_confusables.contains(&(f, s)) | |
| || digit_letter_confusables.contains(&(s, f)) | |
| { | |
| // Check for digit-letter confusables (like 0 vs O, 1 vs l, etc.) | |
| has_digit_letter_confusable = true; | |
| } else { | |
| has_other_diff = true; | |
| } | |
| } | |
| } | |
| // If we have case differences and no other differences | |
| if has_case_diff && !has_other_diff && found != suggested { | |
| has_case_confusion = true; | |
| } | |
| if has_digit_letter_confusable && !has_other_diff && found != suggested { | |
| has_digit_letter_confusion = true; | |
| } | |
| } | |
| match (has_case_confusion, has_digit_letter_confusion) { | |
| (true, true) => ConfusionType::Both, | |
| (true, false) => ConfusionType::Case, | |
| (false, true) => ConfusionType::DigitLetter, | |
| (false, false) => ConfusionType::None, | |
| } | |
| } | |
| /// Represents the type of confusion detected between original and suggested code. | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| pub enum ConfusionType { | |
| /// No confusion detected | |
| None, | |
| /// Only case differences (e.g., "hello" vs "Hello") | |
| Case, | |
| /// Only digit-letter confusion (e.g., "0" vs "O", "1" vs "l") | |
| DigitLetter, | |
| /// Both case and digit-letter confusion | |
| Both, | |
| } | |
| impl ConfusionType { | |
| /// Returns the appropriate label text for this confusion type. | |
| pub fn label_text(&self) -> &'static str { | |
| match self { | |
| ConfusionType::None => "", | |
| ConfusionType::Case => " (notice the capitalization)", | |
| ConfusionType::DigitLetter => " (notice the digit/letter confusion)", | |
| ConfusionType::Both => " (notice the capitalization and digit/letter confusion)", | |
| } | |
| } | |
| /// Combines two confusion types. If either is `Both`, the result is `Both`. | |
| /// If one is `Case` and the other is `DigitLetter`, the result is `Both`. | |
| /// Otherwise, returns the non-`None` type, or `None` if both are `None`. | |
| pub fn combine(self, other: ConfusionType) -> ConfusionType { | |
| match (self, other) { | |
| (ConfusionType::None, other) => other, | |
| (this, ConfusionType::None) => this, | |
| (ConfusionType::Both, _) | (_, ConfusionType::Both) => ConfusionType::Both, | |
| (ConfusionType::Case, ConfusionType::DigitLetter) | |
| | (ConfusionType::DigitLetter, ConfusionType::Case) => ConfusionType::Both, | |
| (ConfusionType::Case, ConfusionType::Case) => ConfusionType::Case, | |
| (ConfusionType::DigitLetter, ConfusionType::DigitLetter) => ConfusionType::DigitLetter, | |
| } | |
| } | |
| /// Returns true if this confusion type represents any kind of confusion. | |
| pub fn has_confusion(&self) -> bool { | |
| *self != ConfusionType::None | |
| } | |
| } | |
| pub(crate) fn should_show_source_code( | |
| ignored_directories: &[String], | |
| sm: &SourceMap, | |
| file: &SourceFile, | |
| ) -> bool { | |
| if !sm.ensure_source_file_source_present(file) { | |
| return false; | |
| } | |
| let FileName::Real(name) = &file.name else { return true }; | |
| name.local_path() | |
| .map(|path| ignored_directories.iter().all(|dir| !path.starts_with(dir))) | |
| .unwrap_or(true) | |
| } |