mirror of
https://github.com/rust-lang/rust.git
synced 2026-05-22 10:05:06 +03:00
639cb694df
And move try_find_description to rustc_errors::codes.
733 lines
27 KiB
Rust
733 lines
27 KiB
Rust
//! 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::error::Report;
|
|
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::FluentArgs;
|
|
use rustc_span::hygiene::{ExpnKind, MacroKind};
|
|
use rustc_span::source_map::SourceMap;
|
|
use rustc_span::{FileName, SourceFile, Span};
|
|
use tracing::{debug, warn};
|
|
|
|
use crate::timings::TimingRecord;
|
|
use crate::translation::Translator;
|
|
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>;
|
|
|
|
fn translator(&self) -> &Translator;
|
|
|
|
/// 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: &FluentArgs<'_>,
|
|
) {
|
|
if let Some((sugg, rest)) = suggestions.split_first() {
|
|
let msg = self
|
|
.translator()
|
|
.translate_message(&sugg.msg, fluent_args)
|
|
.map_err(Report::new)
|
|
.unwrap();
|
|
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);
|
|
}
|
|
|
|
fn translator(&self) -> &Translator {
|
|
self.emitter.translator()
|
|
}
|
|
}
|
|
|
|
pub struct SilentEmitter {
|
|
pub translator: Translator,
|
|
}
|
|
|
|
impl Emitter for SilentEmitter {
|
|
fn source_map(&self) -> Option<&SourceMap> {
|
|
None
|
|
}
|
|
|
|
fn emit_diagnostic(&mut self, _diag: DiagInner) {}
|
|
|
|
fn translator(&self) -> &Translator {
|
|
&self.translator
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|