Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
646 changes: 405 additions & 241 deletions crates/interface/src/diagnostics/emitter/human.rs

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions crates/interface/src/diagnostics/emitter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,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
})
}
4 changes: 4 additions & 0 deletions crates/interface/src/diagnostics/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
123 changes: 122 additions & 1 deletion crates/interface/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -329,6 +334,122 @@ 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 {
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
}
}

/// Indicates the confidence in the correctness of a suggestion.
///
/// All suggestions are marked with an `Applicability`. Tools use the applicability of a suggestion
Expand Down
1 change: 1 addition & 0 deletions crates/interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/solar/main/assets/logo.png",
Expand Down
16 changes: 15 additions & 1 deletion crates/interface/src/source_map/file.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{BytePos, CharPos, pos::RelativeBytePos};
use std::{
fmt, io,
ops::RangeInclusive,
ops::{Range, RangeInclusive},
path::{Path, PathBuf},
sync::Arc,
};
Expand Down Expand Up @@ -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<BytePos> {
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<usize> {
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/typeck/duplicate_selectors.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand All @@ -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