diff --git a/crates/data-structures/src/lib.rs b/crates/data-structures/src/lib.rs index 34f094a39..1619dca82 100644 --- a/crates/data-structures/src/lib.rs +++ b/crates/data-structures/src/lib.rs @@ -40,6 +40,28 @@ pub use thin_slice::{RawThinSlice, ThinSlice}; pub use smallvec; +/// Pluralize a word based on a count. +#[macro_export] +#[rustfmt::skip] +macro_rules! pluralize { + // Pluralize based on count (e.g., apples) + ($x:expr) => { + if $x == 1 { "" } else { "s" } + }; + ("has", $x:expr) => { + if $x == 1 { "has" } else { "have" } + }; + ("is", $x:expr) => { + if $x == 1 { "is" } else { "are" } + }; + ("was", $x:expr) => { + if $x == 1 { "was" } else { "were" } + }; + ("this", $x:expr) => { + if $x == 1 { "this" } else { "these" } + }; +} + /// This calls the passed function while ensuring it won't be inlined into the caller. #[inline(never)] #[cold] diff --git a/crates/interface/src/diagnostics/emitter/human.rs b/crates/interface/src/diagnostics/emitter/human.rs index 8fba1ee53..db609b810 100644 --- a/crates/interface/src/diagnostics/emitter/human.rs +++ b/crates/interface/src/diagnostics/emitter/human.rs @@ -1,27 +1,34 @@ -use super::{Diag, Emitter, io_panic, rustc::FileWithAnnotatedLines}; +use super::{Diag, Emitter}; use crate::{ - SourceMap, - diagnostics::{Level, MultiSpan, Style, SubDiagnostic, SuggestionStyle}, - source_map::SourceFile, + SourceMap, Span, + diagnostics::{ + ConfusionType, DiagId, DiagMsg, Level, MultiSpan, SpanLabel, Style, SubDiagnostic, + SuggestionStyle, Suggestions, detect_confusion_type, emitter::normalize_whitespace, + is_different, + }, + source_map::{FileName, SourceFile}, }; use annotate_snippets::{ - Annotation, AnnotationKind, Group, Level as ASLevel, Message, Patch, Renderer, Report, Snippet, - Title, renderer::DecorStyle, + Annotation as ASAnnotation, AnnotationKind, Group, Level as ASLevel, Padding, Patch, Renderer, + Snippet, renderer::DecorStyle, }; use anstream::{AutoStream, ColorChoice}; use solar_config::HumanEmitterKind; +use solar_data_structures::pluralize; use std::{ any::Any, borrow::Cow, - collections::BTreeMap, io::{self, Write}, sync::{Arc, OnceLock}, }; -// TODO: Tabs are not formatted correctly: https://github.com/rust-lang/annotate-snippets-rs/issues/25 - type Writer = dyn Write + Send + 'static; +/// Maximum number of suggestions to be shown +/// +/// Arbitrary, but taken from trait import suggestion limit +pub(super) const MAX_SUGGESTIONS: usize = 4; + const DEFAULT_RENDERER: Renderer = Renderer::styled() .error(Level::Error.style()) .warning(Level::Warning.style()) @@ -45,12 +52,18 @@ pub struct HumanEmitter { unsafe impl Send for HumanEmitter {} impl Emitter for HumanEmitter { - fn emit_diagnostic(&mut self, diagnostic: &mut Diag) { - self.snippet(diagnostic, |this, snippet| { - writeln!(this.writer, "{}\n", this.renderer.render(snippet))?; - this.writer.flush() - }) - .unwrap_or_else(|e| io_panic(e)); + fn emit_diagnostic(&mut self, diag: &mut Diag) { + let mut primary_span = Cow::Borrowed(&diag.span); + self.primary_span_formatted(&mut primary_span, &mut diag.suggestions); + + self.emit_messages_default( + &diag.level, + &diag.messages, + &diag.code, + &primary_span, + &diag.children, + &diag.suggestions, + ); } fn source_map(&self) -> Option<&Arc> { @@ -179,112 +192,326 @@ impl HumanEmitter { } } - /// Formats the given `diagnostic` into a [`Message`] suitable for use with the renderer. - fn snippet( + fn emit_messages_default( &mut self, - diagnostic: &mut Diag, - f: impl FnOnce(&mut Self, Report<'_>) -> R, - ) -> R { - // Current format (annotate-snippets 0.12.0) (comments in <...>): - /* - title.level[title.id]: title.label - --> snippets[0].path:ll:cc - | - LL | snippets[0].source[ann[0].range] - | ^^^^^^^^^^^^^^^^ ann[0].label - LL | snippets[0].source[ann[1].range] - | ---------------- ann[1].label - | - ::: snippets[1].path:ll:cc - | - etc... - | - = footer[0].level: footer[0].label - = footer[1].level: footer[1].label - = ... - - */ - - // Process suggestions. Inline primary span if necessary. - let mut primary_span = Cow::Borrowed(&diagnostic.span); - self.primary_span_formatted(&mut primary_span, &mut diagnostic.suggestions); - - // Render suggestions unless style is `HideCodeAlways`. - // Note that if the span was previously inlined, suggestions will be empty. - let children = diagnostic - .suggestions - .iter() - .filter(|sugg| sugg.style != SuggestionStyle::HideCodeAlways) - .collect::>(); - - let sm = self.source_map.as_deref(); - let title = title_from_diagnostic(diagnostic); - let snippets = sm.map(|sm| iter_snippets(sm, &primary_span)).into_iter().flatten(); - - // Dummy subdiagnostics go in the main group's footer, non-dummy ones go as separate groups. - let subs = |d| diagnostic.children.iter().filter(move |sub| sub.span.is_dummy() == d); - let sub_groups = subs(false).map(|sub| { - let mut g = Group::with_title(title_from_subdiagnostic(sub, self.supports_color())); - if let Some(sm) = sm { - g = g.elements(iter_snippets(sm, &sub.span)); + level: &Level, + msgs: &[(DiagMsg, Style)], + code: &Option, + msp: &MultiSpan, + children: &[SubDiagnostic], + suggestions: &Suggestions, + ) { + 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 != &Style::NoStyle) { + annotation_level.clone().secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level))) + } else { + annotation_level.clone().primary_title(self.no_style_msgs(msgs)) + }; + + if let Some(c) = code { + title = title.id(c.as_string()); + // Unlike rustc, there is no URL associated with DiagId yet. + // TODO: Add URL mapping + // if let TerminalUrl::Yes = self.terminal_url { + // title = title.id_url(format!("/error_codes/{c}.html")); + // } + } + + let mut report = vec![]; + let mut main_group = Group::with_title(title); + let mut footer_group = Group::with_level(ASLevel::NOTE); + + // If we don't have span information, emit and exit + let Some(sm) = self.source_map.as_ref() else { + main_group = main_group.elements(children.iter().map(|c| { + let msg = self.no_style_msgs(&c.messages); + let level = annotation_level_for_level(c.level); + level.message(msg) + })); + + report.push(main_group); + if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) { + panic!("failed to emit error: {e}"); } - g - }); - - let mut footers = - subs(true).map(|sub| message_from_subdiagnostic(sub, self.supports_color())).peekable(); - let footer_group = - footers.peek().is_some().then(|| Group::with_level(ASLevel::NOTE).elements(footers)); - - // Create suggestion groups for non-inline suggestions - let suggestion_groups = children.iter().flat_map(|suggestion| { - let sm = self.source_map.as_deref()?; - - // For each substitution, create a separate group - // Currently we typically only have one substitution per suggestion - for substitution in &suggestion.substitutions { - // Group parts by file - let mut parts_by_file: BTreeMap<_, Vec<_>> = BTreeMap::new(); - for part in &substitution.parts { - let file = sm.lookup_source_file(part.span.lo()); - parts_by_file.entry(file.name.clone()).or_default().push(part); + return; + }; + + let mut file_ann = collect_annotations(msp, sm); + + // 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, annotations) in file_ann.into_iter() { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + main_group = main_group.element(snippet); } + } + } - if parts_by_file.is_empty() { - continue; + 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 != &Style::NoStyle) { + Cow::Owned(self.pre_style_msgs(&c.messages, c.level)) + } else { + Cow::Owned(self.no_style_msgs(&c.messages)) + }; + + // This is a secondary message with no span info + if !c.span.has_primary_spans() && !c.span.has_span_labels() { + footer_group = footer_group.element(level.clone().message(msg)); + continue; + } + + report.push(std::mem::replace( + &mut main_group, + Group::with_title(level.clone().secondary_title(msg)), + )); + + let mut file_ann = collect_annotations(&c.span, sm); + 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); } + } - let mut snippets = vec![]; - for (filename, parts) in parts_by_file { - let file = sm.get_file_ref(&filename)?; - let mut snippet = Snippet::source(file.src.to_string()) - .path(sm.filename_for_diagnostics(&file.name).to_string()) - .fold(true); + for (file, annotations) in file_ann.into_iter() { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + main_group = main_group.element(snippet); + } + } + } - for part in parts { - if let Ok(range) = sm.span_to_range(part.span) { - snippet = snippet.patch(Patch::new(range, part.snippet.as_str())); + let suggestions_expected = suggestions + .iter() + .filter(|s| { + matches!( + s.style, + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways + ) + }) + .count(); + for suggestion in suggestions.unwrap_tag() { + match suggestion.style { + SuggestionStyle::CompletelyHidden => { + // do not display this suggestion, it is meant only for tools + } + SuggestionStyle::HideCodeAlways => { + let msg = self.no_style_msgs(&[(suggestion.msg.to_owned(), Style::HeaderMsg)]); + main_group = main_group.element(annotate_snippets::Level::HELP.message(msg)); + } + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways => { + let substitutions = suggestion + .substitutions + .iter() + .cloned() // clone is required to sort and filter duplicated spans + .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.windows(2).find(|s| s[0].span.overlaps(s[1].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)); + + if !invalid { Some(subst) } else { None } + }) + .collect::>(); + + if substitutions.is_empty() { + continue; + } + let mut msg = suggestion.msg.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(); + + 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 parts = sub + .parts + .into_iter() + .filter_map(|p| { + if is_different(sm, &p.snippet, p.span) { + Some((p.span, p.snippet)) + } else { + None + } + }) + .collect::>(); + + if parts.is_empty() { + None + } else { + let spans = parts.iter().map(|(span, _)| *span).collect::>(); + + // Unlike rustc, there is no attribute suggestion in Solidity (yet). + // When similar feature to attribute arrives, refer to rustc's + // implementation https://github.com/rust-lang/rust/blob/4146079cee94242771864147e32fb5d9adbd34f8/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs#L424 + let fold = 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.into_inner()) + }, + )), + ) + } else { + None + } + } + }) + .collect::>(); + if !subs.is_empty() { + report.push(std::mem::replace( + &mut main_group, + Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)), + )); + + main_group = main_group.elements(subs); + if other_suggestions > 0 { + main_group = main_group.element( + annotate_snippets::Level::NOTE.no_name().message(format!( + "and {} other candidate{}", + other_suggestions, + pluralize!(other_suggestions) + )), + ); } } - snippets.push(snippet); - } - - if !snippets.is_empty() { - let title = ASLevel::HELP.secondary_title(suggestion.msg.as_str()); - return Some(Group::with_title(title).elements(snippets)); } } + } - None - }); + // FIXME: This hack should be removed once annotate_snippets is the + // default emitter. + if suggestions_expected > 0 && report.is_empty() { + main_group = main_group.element(Padding); + } + + if !main_group.is_empty() { + report.push(main_group); + } + + if !footer_group.is_empty() { + report.push(footer_group); + } - let main_group = Group::with_title(title).elements(snippets); - let report = std::iter::once(main_group) - .chain(suggestion_groups) - .chain(footer_group) - .chain(sub_groups) - .collect::>(); - f(self, &report) + if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) { + panic!("failed to emit error: {e}"); + } + } + + fn pre_style_msgs(&self, msgs: &[(DiagMsg, Style)], level: Level) -> String { + msgs.iter() + .filter_map(|(m, style)| { + let text = m.as_str(); + let style = style.to_color_spec(level); + if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) } + }) + .collect() + } + + // Unlike rustc, there is no translation. + // Since the behavior of `translator.translate_messages` does not contains styling, + // this function to concatenate messages instead can be used. + fn no_style_msgs(&self, msgs: &[(DiagMsg, Style)]) -> String { + msgs.iter().map(|(m, _)| m.as_str()).collect() + } + + fn annotated_snippet<'a>( + &self, + annotations: Vec, + file_name: &FileName, + sm: &Arc, + ) -> Option>> { + let spans = annotations.iter().map(|a| a.span).collect::>(); + 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(); + + 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 + } } } @@ -367,156 +594,93 @@ impl HumanBufferEmitter { } } -fn title_from_diagnostic(diag: &Diag) -> Title<'_> { - let mut title = to_as_level(diag.level).primary_title(diag.label()); - if let Some(id) = diag.id() { - title = title.id(id); +fn annotation_level_for_level<'a>(level: Level) -> ASLevel<'a> { + match level { + Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR, + Level::Warning => ASLevel::WARNING, + Level::Note | Level::OnceNote => ASLevel::NOTE, + Level::Help | Level::OnceHelp => ASLevel::HELP, + Level::Allow => ASLevel::INFO, } - title + .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) }) } -fn title_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Title<'_> { - to_as_level(sub.level).secondary_title(sub.label_with_style(supports_color)) +fn stderr_choice(color_choice: ColorChoice) -> ColorChoice { + static AUTO: OnceLock = OnceLock::new(); + if color_choice == ColorChoice::Auto { + *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr())) + } else { + color_choice + } } -fn message_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Message<'_> { - to_as_level(sub.level).message(sub.label_with_style(supports_color)) -} +fn emit_to_destination(rendered: String, lvl: &Level, dst: &mut Writer) -> io::Result<()> { + // Unlike rustc, there is no lock + // use crate::lock; + // let _buffer_lock = lock::acquire_global_lock("rustc_errors"); -fn iter_snippets<'a>( - sm: &SourceMap, - msp: &MultiSpan, -) -> impl Iterator>> { - collect_files(sm, msp).into_iter().map(|file| file_to_snippet(sm, &file.file, &file.lines)) + writeln!(dst, "{rendered}")?; + if !lvl.is_failure_note() { + writeln!(dst)?; + } + dst.flush()?; + Ok(()) } -fn collect_files(sm: &SourceMap, msp: &MultiSpan) -> Vec { - let mut annotated_files = FileWithAnnotatedLines::collect_annotations(sm, msp); - // Make sure our primary file comes first - if let Some(primary_span) = msp.primary_span() - && !primary_span.is_dummy() - && annotated_files.len() > 1 - { - let primary_lo = sm.lookup_char_pos(primary_span.lo()); - if let Ok(pos) = - annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name)) - { - annotated_files.swap(0, pos); - } - } - annotated_files +#[derive(Debug)] +struct Annotation { + kind: AnnotationKind, + span: Span, + label: Option, } -/// Merges back multi-line annotations that were split across multiple lines into a single -/// annotation that's suitable for `annotate-snippets`. -/// -/// Expects that lines are sorted. -fn file_to_snippet<'a>( - sm: &SourceMap, - file: &SourceFile, - lines: &[super::rustc::Line], -) -> Snippet<'a, Annotation<'a>> { - /// `label, start_idx` - type MultiLine<'a> = (Option<&'a String>, usize); - fn multi_line_at<'a, 'b>( - mls: &'a mut Vec>, - depth: usize, - ) -> &'a mut MultiLine<'b> { - assert!(depth > 0); - if mls.len() < depth { - mls.resize_with(depth, || (None, 0)); - } - &mut mls[depth - 1] - } - - debug_assert!(!lines.is_empty()); - - let first_line = lines.first().unwrap().line_index; - debug_assert!(first_line > 0, "line index is 1-based"); - let last_line = lines.last().unwrap().line_index; - debug_assert!(last_line >= first_line); - debug_assert!(lines.is_sorted()); - let snippet_base = file.line_position(first_line - 1).unwrap(); - - let source = file.get_lines(first_line - 1..=last_line - 1).unwrap_or_default(); - let mut annotations = Vec::new(); - let mut push_annotation = |kind: AnnotationKind, span, label| { - annotations.push(kind.span(span).label(label)); - }; - let annotation_kind = |is_primary: bool| { - if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context } - }; - - let mut mls = Vec::new(); - for line in lines { - let line_abs_pos = file.line_position(line.line_index - 1).unwrap(); - let line_rel_pos = line_abs_pos - snippet_base; - // Returns the position of the given column in the local snippet. - // We have to convert the column char position to byte position. - let rel_pos = |c: &super::rustc::AnnotationColumn| { - line_rel_pos + char_to_byte_pos(&source[line_rel_pos..], c.file) +fn collect_annotations( + msp: &MultiSpan, + sm: &Arc, +) -> Vec<(Arc, Vec)> { + let mut output: Vec<(Arc, Vec)> = 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()); - for ann in &line.annotations { - match ann.annotation_type { - super::rustc::AnnotationType::Singleline => { - push_annotation( - annotation_kind(ann.is_primary), - rel_pos(&ann.start_col)..rel_pos(&ann.end_col), - ann.label.clone().unwrap_or_default(), - ); - } - super::rustc::AnnotationType::MultilineStart(depth) => { - *multi_line_at(&mut mls, depth) = (ann.label.as_ref(), rel_pos(&ann.start_col)); - } - super::rustc::AnnotationType::MultilineLine(_depth) => { - // TODO: unvalidated - push_annotation( - AnnotationKind::Visible, - line_rel_pos..line_rel_pos, - String::new(), - ); - } - super::rustc::AnnotationType::MultilineEnd(depth) => { - let (label, multiline_start_idx) = *multi_line_at(&mut mls, depth); - let end_idx = rel_pos(&ann.end_col); - debug_assert!(end_idx >= multiline_start_idx); - push_annotation( - annotation_kind(ann.is_primary), - multiline_start_idx..end_idx, - label.or(ann.label.as_ref()).cloned().unwrap_or_default(), - ); - } + let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context }; + + let label = label.as_ref().map(|m| normalize_whitespace(m)); + + let ann = Annotation { kind, span, label }; + if sm.is_valid_span(ann.span).is_ok() { + if let Some((_, annotations)) = output.iter_mut().find(|(f, _)| f.name == file.name) { + annotations.push(ann); + } else { + output.push((file, vec![ann])); } } } - Snippet::source(source.to_string()) - .path(sm.filename_for_diagnostics(&file.name).to_string()) - .line_start(first_line) - .fold(true) - .annotations(annotations) + output } -fn to_as_level<'a>(level: Level) -> ASLevel<'a> { - match level { - Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR, - Level::Warning => ASLevel::WARNING, - Level::Note | Level::OnceNote => ASLevel::NOTE, - Level::Help | Level::OnceHelp => ASLevel::HELP, - Level::Allow => ASLevel::INFO, - } - .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) }) -} +fn shrink_file( + spans: &[Span], + _file_name: &FileName, + sm: &Arc, +) -> 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; -fn char_to_byte_pos(s: &str, char_pos: usize) -> usize { - s.chars().take(char_pos).map(char::len_utf8).sum() -} + 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; -fn stderr_choice(color_choice: ColorChoice) -> ColorChoice { - static AUTO: OnceLock = OnceLock::new(); - if color_choice == ColorChoice::Auto { - *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr())) - } else { - color_choice - } + let bounding_span = Span::new(lo, hi); + let source = sm.span_to_snippet(bounding_span).unwrap_or_default(); + let offset_line = lo_loc.line; + + Some((bounding_span, source, offset_line)) } diff --git a/crates/interface/src/diagnostics/emitter/mod.rs b/crates/interface/src/diagnostics/emitter/mod.rs index 08aee398a..2857a5711 100644 --- a/crates/interface/src/diagnostics/emitter/mod.rs +++ b/crates/interface/src/diagnostics/emitter/mod.rs @@ -13,8 +13,6 @@ pub use json::JsonEmitter; mod mem; pub use mem::InMemoryEmitter; -mod rustc; - /// Dynamic diagnostic emitter. See [`Emitter`]. pub type DynEmitter = dyn Emitter + Send; @@ -185,3 +183,77 @@ impl Emitter for LocalEmitter { fn io_panic(error: std::io::Error) -> ! { panic!("failed to emit diagnostic: {error}"); } + +// 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 + }) +} diff --git a/crates/interface/src/diagnostics/emitter/rustc.rs b/crates/interface/src/diagnostics/emitter/rustc.rs deleted file mode 100644 index 134c46a93..000000000 --- a/crates/interface/src/diagnostics/emitter/rustc.rs +++ /dev/null @@ -1,358 +0,0 @@ -//! Annotation collector for displaying diagnostics vendored from Rustc. - -use crate::{ - SourceMap, - diagnostics::{MultiSpan, SpanLabel}, - source_map::{Loc, SourceFile}, -}; -use std::{ - cmp::{max, min}, - sync::Arc, -}; - -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) struct Line { - pub(crate) line_index: usize, - pub(crate) annotations: Vec, -} - -#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Default)] -pub(crate) struct AnnotationColumn { - /// the (0-indexed) column for *display* purposes, counted in characters, not utf-8 bytes - pub(crate) display: usize, - /// the (0-indexed) column in the file, counted in characters, not utf-8 bytes. - /// - /// this may be different from `self.display`, - /// e.g. if the file contains hard tabs, because we convert tabs to spaces for error messages. - /// - /// for example: - /// ```text - /// (hard tab)hello - /// ^ this is display column 4, but file column 1 - /// ``` - /// - /// we want to keep around the correct file offset so that column numbers in error messages - /// are correct. (motivated by ) - pub(crate) file: usize, -} - -impl AnnotationColumn { - pub(crate) fn from_loc(loc: &Loc) -> Self { - Self { display: loc.col_display, file: loc.col.0 } - } -} - -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) struct MultilineAnnotation { - pub(crate) depth: usize, - pub(crate) line_start: usize, - pub(crate) line_end: usize, - pub(crate) start_col: AnnotationColumn, - pub(crate) end_col: AnnotationColumn, - pub(crate) is_primary: bool, - pub(crate) label: Option, - pub(crate) overlaps_exactly: bool, -} - -impl MultilineAnnotation { - pub(crate) fn increase_depth(&mut self) { - self.depth += 1; - } - - /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. - pub(crate) fn same_span(&self, other: &Self) -> bool { - self.line_start == other.line_start - && self.line_end == other.line_end - && self.start_col == other.start_col - && self.end_col == other.end_col - } - - pub(crate) fn as_start(&self) -> Annotation { - Annotation { - start_col: self.start_col, - end_col: AnnotationColumn { - // these might not correspond to the same place anymore, - // but that's okay for our purposes - display: self.start_col.display + 1, - file: self.start_col.file + 1, - }, - is_primary: self.is_primary, - label: None, - annotation_type: AnnotationType::MultilineStart(self.depth), - } - } - - pub(crate) fn as_end(&self) -> Annotation { - Annotation { - start_col: AnnotationColumn { - // these might not correspond to the same place anymore, - // but that's okay for our purposes - display: self.end_col.display.saturating_sub(1), - file: self.end_col.file.saturating_sub(1), - }, - end_col: self.end_col, - is_primary: self.is_primary, - label: self.label.clone(), - annotation_type: AnnotationType::MultilineEnd(self.depth), - } - } - - pub(crate) fn as_line(&self) -> Annotation { - Annotation { - start_col: Default::default(), - end_col: Default::default(), - is_primary: self.is_primary, - label: None, - annotation_type: AnnotationType::MultilineLine(self.depth), - } - } -} - -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) enum AnnotationType { - /// Annotation under a single line of code - Singleline, - - // The Multiline type above is replaced with the following three in order - // to reuse the current label drawing code. - // - // Each of these corresponds to one part of the following diagram: - // - // x | foo(1 + bar(x, - // | _________^ < MultilineStart - // x | | y), < MultilineLine - // | |______________^ label < MultilineEnd - // x | z); - /// Annotation marking the first character of a fully shown multiline span - MultilineStart(usize), - /// Annotation marking the last character of a fully shown multiline span - MultilineEnd(usize), - /// Line at the left enclosing the lines of a fully shown multiline span - // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 - // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in - // `draw_multiline_line`. - MultilineLine(usize), -} - -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) struct Annotation { - /// Start column. - /// Note that it is important that this field goes - /// first, so that when we sort, we sort orderings by start - /// column. - pub(crate) start_col: AnnotationColumn, - - /// End column within the line (exclusive) - pub(crate) end_col: AnnotationColumn, - - /// Is this annotation derived from primary span - pub(crate) is_primary: bool, - - /// Optional label to display adjacent to the annotation. - pub(crate) label: Option, - - /// Is this a single line, multiline or multiline span minimized down to a - /// smaller span. - pub(crate) annotation_type: AnnotationType, -} - -#[derive(Debug)] -pub(crate) struct FileWithAnnotatedLines { - pub(crate) file: Arc, - pub(crate) lines: Vec, - multiline_depth: usize, -} - -impl FileWithAnnotatedLines { - /// Preprocess all the annotations so that they are grouped by file and by line number - /// This helps us quickly iterate over the whole message (including secondary file spans) - pub(crate) fn collect_annotations(sm: &SourceMap, msp: &MultiSpan) -> Vec { - fn add_annotation_to_file( - file_vec: &mut Vec, - file: Arc, - line_index: usize, - ann: Annotation, - ) { - for slot in file_vec.iter_mut() { - // Look through each of our files for the one we're adding to - if slot.file.name == file.name { - // See if we already have a line for it - for line_slot in &mut slot.lines { - if line_slot.line_index == line_index { - line_slot.annotations.push(ann); - return; - } - } - // We don't have a line yet, create one - slot.lines.push(Line { line_index, annotations: vec![ann] }); - slot.lines.sort(); - return; - } - } - // This is the first time we're seeing the file - file_vec.push(FileWithAnnotatedLines { - file, - lines: vec![Line { line_index, annotations: vec![ann] }], - multiline_depth: 0, - }); - } - - let mut output = vec![]; - let mut multiline_annotations = 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 lo = sm.lookup_char_pos(span.lo()); - let mut hi = sm.lookup_char_pos(span.hi()); - - // Watch out for "empty spans". If we get a span like 6..6, we - // want to just display a `^` at 6, so convert that to - // 6..7. This is degenerate input, but it's best to degrade - // gracefully -- and the parser likes to supply a span like - // that for EOF, in particular. - - if lo.col_display == hi.col_display && lo.line == hi.line { - hi.col_display += 1; - } - - let label = label.as_ref().map(|m| m.as_str().to_string()); - - if lo.line != hi.line { - let ml = MultilineAnnotation { - depth: 1, - line_start: lo.line, - line_end: hi.line, - start_col: AnnotationColumn::from_loc(&lo), - end_col: AnnotationColumn::from_loc(&hi), - is_primary, - label, - overlaps_exactly: false, - }; - multiline_annotations.push((lo.file, ml)); - } else { - let ann = Annotation { - start_col: AnnotationColumn::from_loc(&lo), - end_col: AnnotationColumn::from_loc(&hi), - is_primary, - label, - annotation_type: AnnotationType::Singleline, - }; - add_annotation_to_file(&mut output, lo.file, lo.data.line, ann); - }; - } - - // Find overlapping multiline annotations, put them at different depths - multiline_annotations.sort_by_key(|(_, ml)| (ml.line_start, usize::MAX - ml.line_end)); - for (_, ann) in multiline_annotations.clone() { - for (_, a) in multiline_annotations.iter_mut() { - // Move all other multiline annotations overlapping with this one - // one level to the right. - if !(ann.same_span(a)) - && num_overlap(ann.line_start, ann.line_end, a.line_start, a.line_end, true) - { - a.increase_depth(); - } else if ann.same_span(a) && &ann != a { - a.overlaps_exactly = true; - } else { - break; - } - } - } - - let mut max_depth = 0; // max overlapping multiline spans - for (_, ann) in &multiline_annotations { - max_depth = max(max_depth, ann.depth); - } - // Change order of multispan depth to minimize the number of overlaps in the ASCII art. - for (_, a) in multiline_annotations.iter_mut() { - a.depth = max_depth - a.depth + 1; - } - for (file, ann) in multiline_annotations { - let mut end_ann = ann.as_end(); - if !ann.overlaps_exactly { - // avoid output like - // - // | foo( - // | _____^ - // | |_____| - // | || bar, - // | || ); - // | || ^ - // | ||______| - // | |______foo - // | baz - // - // and instead get - // - // | foo( - // | _____^ - // | | bar, - // | | ); - // | | ^ - // | | | - // | |______foo - // | baz - add_annotation_to_file( - &mut output, - Arc::clone(&file), - ann.line_start, - ann.as_start(), - ); - // 4 is the minimum vertical length of a multiline span when presented: two lines - // of code and two lines of underline. This is not true for the special case where - // the beginning doesn't have an underline, but the current logic seems to be - // working correctly. - let middle = min(ann.line_start + 4, ann.line_end); - // We'll show up to 4 lines past the beginning of the multispan start. - // We will *not* include the tail of lines that are only whitespace, a comment or - // a bare delimiter. - let filter = |s: &str| { - let s = s.trim(); - // Consider comments as empty, but don't consider docstrings to be empty. - !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) - // Consider lines with nothing but whitespace, a single delimiter as empty. - && !["", "{", "}", "(", ")", "[", "]"].contains(&s) - }; - let until = (ann.line_start..middle) - .rev() - .filter_map(|line| file.get_line(line - 1).map(|s| (line + 1, s))) - .find(|(_, s)| filter(s)) - .map(|(line, _)| line) - .unwrap_or(ann.line_start); - for line in ann.line_start + 1..until { - // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). - add_annotation_to_file(&mut output, Arc::clone(&file), line, ann.as_line()); - } - let line_end = ann.line_end - 1; - let end_is_empty = file.get_line(line_end - 1).is_some_and(|s| !filter(s)); - if middle < line_end && !end_is_empty { - add_annotation_to_file(&mut output, Arc::clone(&file), line_end, ann.as_line()); - } - } else { - end_ann.annotation_type = AnnotationType::Singleline; - } - add_annotation_to_file(&mut output, file, ann.line_end, end_ann); - } - for file_vec in output.iter_mut() { - file_vec.multiline_depth = max_depth; - } - output - } -} - -fn num_overlap( - a_start: usize, - a_end: usize, - b_start: usize, - b_end: usize, - inclusive: bool, -) -> bool { - let extra = if inclusive { 1 } else { 0 }; - (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) -} diff --git a/crates/interface/src/diagnostics/message.rs b/crates/interface/src/diagnostics/message.rs index 18ce2d47d..7c362deb9 100644 --- a/crates/interface/src/diagnostics/message.rs +++ b/crates/interface/src/diagnostics/message.rs @@ -41,6 +41,10 @@ impl DiagMsg { pub fn as_str(&self) -> &str { &self.inner } + + pub fn into_inner(self) -> Cow<'static, str> { + self.inner + } } /// A span together with some additional data. diff --git a/crates/interface/src/diagnostics/mod.rs b/crates/interface/src/diagnostics/mod.rs index a46d0fab6..4a67ae488 100644 --- a/crates/interface/src/diagnostics/mod.rs +++ b/crates/interface/src/diagnostics/mod.rs @@ -2,12 +2,13 @@ //! //! Modified from [`rustc_errors`](https://github.com/rust-lang/rust/blob/520e30be83b4ed57b609d33166c988d1512bf4f3/compiler/rustc_errors/src/diagnostic.rs). -use crate::Span; +use crate::{SourceMap, Span}; use anstyle::{AnsiColor, Color}; use std::{ borrow::Cow, fmt::{self, Write}, hash::{Hash, Hasher}, + iter, ops::Deref, panic::Location, }; @@ -249,6 +250,10 @@ impl Level { } } + pub fn is_failure_note(&self) -> bool { + matches!(*self, Self::FailureNote) + } + /// Returns the style of this level. #[inline] pub const fn style(self) -> anstyle::Style { @@ -329,6 +334,121 @@ impl Style { } } +/// 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; + + 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 { + Self::None => "", + Self::Case => " (notice the capitalization)", + Self::DigitLetter => " (notice the digit/letter confusion)", + Self::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: Self) -> Self { + match (self, other) { + (Self::None, other) => other, + (this, Self::None) => this, + (Self::Both, _) | (_, Self::Both) => Self::Both, + (Self::Case, Self::DigitLetter) | (Self::DigitLetter, Self::Case) => Self::Both, + (Self::Case, Self::Case) => Self::Case, + (Self::DigitLetter, Self::DigitLetter) => Self::DigitLetter, + } + } + + /// Returns true if this confusion type represents any kind of confusion. + pub fn has_confusion(&self) -> bool { + *self != Self::None + } +} + /// Indicates the confidence in the correctness of a suggestion. /// /// All suggestions are marked with an `Applicability`. Tools use the applicability of a suggestion diff --git a/crates/interface/src/lib.rs b/crates/interface/src/lib.rs index 11748a143..7264d99bd 100644 --- a/crates/interface/src/lib.rs +++ b/crates/interface/src/lib.rs @@ -45,28 +45,6 @@ pub type Result = std::result::Result; /// Solar aims to support Solidity 0.8.* and later versions. pub const MIN_SOLIDITY_VERSION: semver::Version = semver::Version::new(0, 8, 0); -/// Pluralize a word based on a count. -#[macro_export] -#[rustfmt::skip] -macro_rules! pluralize { - // Pluralize based on count (e.g., apples) - ($x:expr) => { - if $x == 1 { "" } else { "s" } - }; - ("has", $x:expr) => { - if $x == 1 { "has" } else { "have" } - }; - ("is", $x:expr) => { - if $x == 1 { "is" } else { "are" } - }; - ("was", $x:expr) => { - if $x == 1 { "was" } else { "were" } - }; - ("this", $x:expr) => { - if $x == 1 { "this" } else { "these" } - }; -} - /// Creates new session globals on the current thread if they doesn't exist already and then /// executes the given closure. /// diff --git a/crates/interface/src/source_map/file.rs b/crates/interface/src/source_map/file.rs index e75fc9455..f26a46171 100644 --- a/crates/interface/src/source_map/file.rs +++ b/crates/interface/src/source_map/file.rs @@ -1,7 +1,7 @@ use crate::{BytePos, CharPos, pos::RelativeBytePos}; use std::{ fmt, io, - ops::RangeInclusive, + ops::{Range, RangeInclusive}, path::{Path, PathBuf}, sync::Arc, }; @@ -276,6 +276,20 @@ impl SourceFile { self.lines().partition_point(|x| x <= &pos).checked_sub(1) } + pub fn line_bounds(&self, line_index: usize) -> Range { + if self.is_empty() { + return self.start_pos..self.start_pos; + } + + let lines = self.lines(); + assert!(line_index < lines.len()); + if line_index == (lines.len() - 1) { + self.absolute_position(lines[line_index])..self.end_position() + } else { + self.absolute_position(lines[line_index])..self.absolute_position(lines[line_index + 1]) + } + } + /// Returns the relative byte position of the start of the line at the given /// 0-based line index. pub fn line_position(&self, line_number: usize) -> Option { diff --git a/crates/sema/src/typeck/checker.rs b/crates/sema/src/typeck/checker.rs index 9ca6ae0b5..9b9eda8c0 100644 --- a/crates/sema/src/typeck/checker.rs +++ b/crates/sema/src/typeck/checker.rs @@ -5,8 +5,8 @@ use crate::{ }; use alloy_primitives::U256; use solar_ast::{DataLocation, ElementaryType, Span}; -use solar_data_structures::{Never, map::FxHashMap, smallvec::SmallVec}; -use solar_interface::{diagnostics::DiagCtxt, pluralize}; +use solar_data_structures::{Never, map::FxHashMap, pluralize, smallvec::SmallVec}; +use solar_interface::diagnostics::DiagCtxt; use std::ops::ControlFlow; pub(super) fn check(gcx: Gcx<'_>, source: hir::SourceId) { diff --git a/tests/ui/typeck/duplicate_selectors.stderr b/tests/ui/typeck/duplicate_selectors.stderr index cb7d45ffe..67bd37e32 100644 --- a/tests/ui/typeck/duplicate_selectors.stderr +++ b/tests/ui/typeck/duplicate_selectors.stderr @@ -4,7 +4,6 @@ error: function signature hash collision LL | contract D is C { | ^ | - = note: the function signatures `mintEfficientN2M_001Z5BWH()` and `BlazingIt4490597615()` produce the same 4-byte selector `0x00000000` note: first function --> ROOT/tests/ui/typeck/duplicate_selectors.sol:LL:CC | @@ -15,6 +14,7 @@ note: second function | LL | function BlazingIt4490597615() public {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: the function signatures `mintEfficientN2M_001Z5BWH()` and `BlazingIt4490597615()` produce the same 4-byte selector `0x00000000` error: aborting due to 1 previous error