use crate::{SourceMap, Span};
use anstyle::{AnsiColor, Color};
use std::{
borrow::Cow,
fmt::{self, Write},
hash::{Hash, Hasher},
iter,
ops::Deref,
panic::Location,
};
mod builder;
pub use builder::{DiagBuilder, EmissionGuarantee};
mod context;
pub use context::{DiagCtxt, DiagCtxtFlags};
mod emitter;
pub use emitter::{
DynEmitter, Emitter, HumanBufferEmitter, HumanEmitter, InMemoryEmitter, LocalEmitter,
SilentEmitter,
};
#[cfg(feature = "json")]
pub use emitter::{JsonEmitter, Severity, SolcDiagnostic, SourceLocation};
mod message;
pub use message::{DiagMsg, MultiSpan, SpanLabel};
pub struct EmittedDiagnostics(pub(crate) String);
impl fmt::Debug for EmittedDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Display for EmittedDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for EmittedDiagnostics {}
impl EmittedDiagnostics {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ErrorGuaranteed(());
impl fmt::Debug for ErrorGuaranteed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ErrorGuaranteed")
}
}
impl ErrorGuaranteed {
#[inline]
pub const fn new_unchecked() -> Self {
Self(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BugAbort;
pub struct ExplicitBug;
pub struct FatalAbort;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DiagId {
s: Cow<'static, str>,
}
impl DiagId {
pub fn new_str(s: impl Into<Cow<'static, str>>) -> Self {
Self { s: s.into() }
}
#[doc(hidden)]
#[cfg_attr(debug_assertions, track_caller)]
pub fn new_from_macro(id: u32) -> Self {
debug_assert!((1..=9999).contains(&id), "error code must be in range 0001-9999");
Self { s: Cow::Owned(format!("{id:04}")) }
}
pub fn as_str(&self) -> &str {
&self.s
}
}
#[macro_export]
macro_rules! error_code {
($id:literal) => {
$crate::diagnostics::DiagId::new_from_macro($id)
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
Bug,
Fatal,
Error,
Warning,
Note,
OnceNote,
Help,
OnceHelp,
FailureNote,
Allow,
}
impl Level {
pub fn to_str(self) -> &'static str {
match self {
Self::Bug => "error: internal compiler error",
Self::Fatal | Self::Error => "error",
Self::Warning => "warning",
Self::Note | Self::OnceNote => "note",
Self::Help | Self::OnceHelp => "help",
Self::FailureNote => "failure-note",
Self::Allow
=> unreachable!(),
}
}
#[inline]
pub fn is_error(self) -> bool {
match self {
Self::Bug | Self::Fatal | Self::Error | Self::FailureNote => true,
Self::Warning
| Self::Note
| Self::OnceNote
| Self::Help
| Self::OnceHelp
| Self::Allow => false,
}
}
#[inline]
pub fn is_note(self) -> bool {
match self {
Self::Note | Self::OnceNote => true,
Self::Bug
| Self::Fatal
| Self::Error
| Self::FailureNote
| Self::Warning
| Self::Help
| Self::OnceHelp
| Self::Allow => false,
}
}
pub fn is_failure_note(&self) -> bool {
matches!(*self, Self::FailureNote)
}
#[inline]
pub const fn style(self) -> anstyle::Style {
anstyle::Style::new().fg_color(self.color()).bold()
}
#[inline]
pub const fn color(self) -> Option<Color> {
match self.ansi_color() {
Some(c) => Some(Color::Ansi(c)),
None => None,
}
}
#[inline]
pub const fn ansi_color(self) -> Option<AnsiColor> {
match self {
Self::Bug | Self::Fatal | Self::Error => Some(AnsiColor::BrightRed),
Self::Warning => {
Some(if cfg!(windows) { AnsiColor::BrightYellow } else { AnsiColor::Yellow })
}
Self::Note | Self::OnceNote => Some(AnsiColor::BrightGreen),
Self::Help | Self::OnceHelp => Some(AnsiColor::BrightCyan),
Self::FailureNote | Self::Allow => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Style {
MainHeaderMsg,
HeaderMsg,
LineAndColumn,
LineNumber,
Quotation,
UnderlinePrimary,
UnderlineSecondary,
LabelPrimary,
LabelSecondary,
NoStyle,
Level(Level),
Highlight,
Addition,
Removal,
}
impl Style {
pub const fn to_color_spec(self, level: Level) -> anstyle::Style {
use AnsiColor::*;
const BRIGHT_BLUE: Color = Color::Ansi(if cfg!(windows) { BrightCyan } else { BrightBlue });
const GREEN: Color = Color::Ansi(BrightGreen);
const MAGENTA: Color = Color::Ansi(BrightMagenta);
const RED: Color = Color::Ansi(BrightRed);
const WHITE: Color = Color::Ansi(BrightWhite);
let s = anstyle::Style::new();
match self {
Self::Addition => s.fg_color(Some(GREEN)),
Self::Removal => s.fg_color(Some(RED)),
Self::LineAndColumn => s,
Self::LineNumber => s.fg_color(Some(BRIGHT_BLUE)).bold(),
Self::Quotation => s,
Self::MainHeaderMsg => if cfg!(windows) { s.fg_color(Some(WHITE)) } else { s }.bold(),
Self::UnderlinePrimary | Self::LabelPrimary => s.fg_color(level.color()).bold(),
Self::UnderlineSecondary | Self::LabelSecondary => s.fg_color(Some(BRIGHT_BLUE)).bold(),
Self::HeaderMsg | Self::NoStyle => s,
Self::Level(level2) => s.fg_color(level2.color()).bold(),
Self::Highlight => s.fg_color(Some(MAGENTA)).bold(),
}
}
}
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
}
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) {
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))
{
has_digit_letter_confusable = true;
} else {
has_other_diff = true;
}
}
}
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,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfusionType {
None,
Case,
DigitLetter,
Both,
}
impl ConfusionType {
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)",
}
}
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,
}
}
pub fn has_confusion(&self) -> bool {
*self != Self::None
}
}
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "kebab-case"))]
#[derive(Copy, Clone, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Applicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
#[default]
Unspecified,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
pub enum SuggestionStyle {
HideCodeInline,
HideCodeAlways,
CompletelyHidden,
#[default]
ShowCode,
ShowAlways,
}
impl SuggestionStyle {
fn hide_inline(&self) -> bool {
!matches!(*self, Self::ShowCode)
}
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub enum Suggestions {
Enabled(Vec<CodeSuggestion>),
Sealed(Box<[CodeSuggestion]>),
Disabled,
}
impl Suggestions {
pub fn unwrap_tag(&self) -> &[CodeSuggestion] {
match self {
Self::Enabled(suggestions) => suggestions,
Self::Sealed(suggestions) => suggestions,
Self::Disabled => &[],
}
}
}
impl Default for Suggestions {
fn default() -> Self {
Self::Enabled(vec![])
}
}
impl Deref for Suggestions {
type Target = [CodeSuggestion];
fn deref(&self) -> &Self::Target {
self.unwrap_tag()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CodeSuggestion {
pub substitutions: Vec<Substitution>,
pub msg: DiagMsg,
pub style: SuggestionStyle,
pub applicability: Applicability,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SubstitutionPart {
pub span: Span,
pub snippet: DiagMsg,
}
impl SubstitutionPart {
pub fn is_addition(&self) -> bool {
self.span.lo() == self.span.hi() && !self.snippet.is_empty()
}
pub fn is_deletion(&self) -> bool {
self.span.lo() != self.span.hi() && self.snippet.is_empty()
}
pub fn is_replacement(&self) -> bool {
self.span.lo() != self.span.hi() && !self.snippet.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Substitution {
pub parts: Vec<SubstitutionPart>,
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct SubDiagnostic {
pub level: Level,
pub messages: Vec<(DiagMsg, Style)>,
pub span: MultiSpan,
}
impl SubDiagnostic {
pub fn label(&self) -> Cow<'_, str> {
self.label_with_style(false)
}
pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
flatten_messages(&self.messages, supports_color, self.level)
}
}
#[must_use]
#[derive(Clone, Debug)]
pub struct Diag {
pub(crate) level: Level,
pub messages: Vec<(DiagMsg, Style)>,
pub span: MultiSpan,
pub children: Vec<SubDiagnostic>,
pub code: Option<DiagId>,
pub suggestions: Suggestions,
pub created_at: &'static Location<'static>,
}
impl PartialEq for Diag {
fn eq(&self, other: &Self) -> bool {
self.keys() == other.keys()
}
}
impl Hash for Diag {
fn hash<H: Hasher>(&self, state: &mut H) {
self.keys().hash(state);
}
}
impl Diag {
#[track_caller]
pub fn new<M: Into<DiagMsg>>(level: Level, msg: M) -> Self {
Self::new_with_messages(level, vec![(msg.into(), Style::NoStyle)])
}
#[track_caller]
pub fn new_with_messages(level: Level, messages: Vec<(DiagMsg, Style)>) -> Self {
Self {
level,
messages,
code: None,
span: MultiSpan::new(),
children: vec![],
suggestions: Suggestions::default(),
created_at: Location::caller(),
}
}
#[inline]
pub fn is_error(&self) -> bool {
self.level.is_error()
}
#[inline]
pub fn is_note(&self) -> bool {
self.level.is_note()
}
pub fn label(&self) -> Cow<'_, str> {
flatten_messages(&self.messages, false, self.level)
}
pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
flatten_messages(&self.messages, supports_color, self.level)
}
pub fn messages(&self) -> &[(DiagMsg, Style)] {
&self.messages
}
pub fn level(&self) -> Level {
self.level
}
pub fn id(&self) -> Option<&str> {
self.code.as_ref().map(|code| code.as_str())
}
fn keys(&self) -> impl PartialEq + std::hash::Hash {
(
&self.level,
&self.messages,
&self.code,
&self.span,
&self.children,
&self.suggestions,
)
}
}
impl Diag {
pub fn span(&mut self, span: impl Into<MultiSpan>) -> &mut Self {
self.span = span.into();
self
}
pub fn code(&mut self, code: impl Into<DiagId>) -> &mut Self {
self.code = Some(code.into());
self
}
pub fn span_label(&mut self, span: Span, label: impl Into<DiagMsg>) -> &mut Self {
self.span.push_span_label(span, label);
self
}
pub fn span_labels(
&mut self,
spans: impl IntoIterator<Item = Span>,
label: impl Into<DiagMsg>,
) -> &mut Self {
let label = label.into();
for span in spans {
self.span_label(span, label.clone());
}
self
}
pub(crate) fn locations_note(&mut self, emitted_at: &Location<'_>) -> &mut Self {
let msg = format!(
"created at {},\n\
emitted at {}",
self.created_at, emitted_at
);
self.note(msg)
}
}
impl Diag {
pub fn warn(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Warning, msg, MultiSpan::new())
}
pub fn span_warn(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Warning, msg, span)
}
pub fn note(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Note, msg, MultiSpan::new())
}
pub fn span_note(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Note, msg, span)
}
pub fn highlighted_note(&mut self, messages: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
self.sub_with_highlights(Level::Note, messages, MultiSpan::new())
}
pub fn note_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::OnceNote, msg, MultiSpan::new())
}
pub fn span_note_once(
&mut self,
span: impl Into<MultiSpan>,
msg: impl Into<DiagMsg>,
) -> &mut Self {
self.sub(Level::OnceNote, msg, span)
}
pub fn help(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Help, msg, MultiSpan::new())
}
pub fn help_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::OnceHelp, msg, MultiSpan::new())
}
pub fn highlighted_help(&mut self, msgs: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
self.sub_with_highlights(Level::Help, msgs, MultiSpan::new())
}
pub fn span_help(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
self.sub(Level::Help, msg, span)
}
fn sub(
&mut self,
level: Level,
msg: impl Into<DiagMsg>,
span: impl Into<MultiSpan>,
) -> &mut Self {
self.children.push(SubDiagnostic {
level,
messages: vec![(msg.into(), Style::NoStyle)],
span: span.into(),
});
self
}
fn sub_with_highlights(
&mut self,
level: Level,
messages: Vec<(impl Into<DiagMsg>, Style)>,
span: MultiSpan,
) -> &mut Self {
let messages = messages.into_iter().map(|(m, s)| (m.into(), s)).collect();
self.children.push(SubDiagnostic { level, messages, span });
self
}
}
impl Diag {
pub fn disable_suggestions(&mut self) -> &mut Self {
self.suggestions = Suggestions::Disabled;
self
}
pub fn seal_suggestions(&mut self) -> &mut Self {
if let Suggestions::Enabled(suggestions) = &mut self.suggestions {
let suggestions_slice = std::mem::take(suggestions).into_boxed_slice();
self.suggestions = Suggestions::Sealed(suggestions_slice);
}
self
}
fn push_suggestion(&mut self, suggestion: CodeSuggestion) {
if let Suggestions::Enabled(suggestions) = &mut self.suggestions {
suggestions.push(suggestion);
}
}
pub fn span_suggestion(
&mut self,
span: Span,
msg: impl Into<DiagMsg>,
suggestion: impl Into<DiagMsg>,
applicability: Applicability,
) -> &mut Self {
self.span_suggestion_with_style(
span,
msg,
suggestion,
applicability,
SuggestionStyle::ShowCode,
);
self
}
pub fn span_suggestion_with_style(
&mut self,
span: Span,
msg: impl Into<DiagMsg>,
suggestion: impl Into<DiagMsg>,
applicability: Applicability,
style: SuggestionStyle,
) -> &mut Self {
self.push_suggestion(CodeSuggestion {
substitutions: vec![Substitution {
parts: vec![SubstitutionPart { snippet: suggestion.into(), span }],
}],
msg: msg.into(),
style,
applicability,
});
self
}
pub fn multipart_suggestion(
&mut self,
msg: impl Into<DiagMsg>,
substitutions: Vec<(Span, DiagMsg)>,
applicability: Applicability,
) -> &mut Self {
self.multipart_suggestion_with_style(
msg,
substitutions,
applicability,
SuggestionStyle::ShowCode,
);
self
}
pub fn multipart_suggestion_with_style(
&mut self,
msg: impl Into<DiagMsg>,
substitutions: Vec<(Span, DiagMsg)>,
applicability: Applicability,
style: SuggestionStyle,
) -> &mut Self {
self.push_suggestion(CodeSuggestion {
substitutions: vec![Substitution {
parts: substitutions
.into_iter()
.map(|(span, snippet)| SubstitutionPart { span, snippet })
.collect(),
}],
msg: msg.into(),
style,
applicability,
});
self
}
}
fn flatten_messages(messages: &[(DiagMsg, Style)], with_style: bool, level: Level) -> Cow<'_, str> {
if with_style {
match messages {
[] => Cow::Borrowed(""),
[(msg, Style::NoStyle)] => Cow::Borrowed(msg.as_str()),
[(msg, style)] => {
let mut res = String::new();
write_fmt(&mut res, msg, style, level);
Cow::Owned(res)
}
messages => {
let mut res = String::new();
for (msg, style) in messages {
match style {
Style::NoStyle => res.push_str(msg.as_str()),
_ => write_fmt(&mut res, msg, style, level),
}
}
Cow::Owned(res)
}
}
} else {
match messages {
[] => Cow::Borrowed(""),
[(message, _)] => Cow::Borrowed(message.as_str()),
messages => messages.iter().map(|(msg, _)| msg.as_str()).collect(),
}
}
}
fn write_fmt(output: &mut String, msg: &DiagMsg, style: &Style, level: Level) {
let style = style.to_color_spec(level);
let _ = write!(output, "{style}{}{style:#}", msg.as_str());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BytePos, ColorChoice, Span, source_map};
#[cfg(feature = "json")]
use snapbox::IntoData as _;
use snapbox::{assert_data_eq, str};
#[test]
fn test_styled_messages() {
let mut diag = Diag::new(Level::Note, "test");
diag.highlighted_note(vec![
("plain text ", Style::NoStyle),
("removed", Style::Removal),
(" middle ", Style::NoStyle),
("added", Style::Addition),
]);
let sub = &diag.children[0];
assert_data_eq!(&*sub.label(), str!["plain text removed middle added"]);
assert_data_eq!(
&*sub.label_with_style(true),
str!["plain text [91mremoved[0m middle [92madded[0m"]
);
}
#[test]
fn test_inline_suggestion() {
let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
diag.span(var_span).span_suggestion(
var_span,
"mutable variables should use mixedCase",
var_sugg,
Applicability::MachineApplicable,
);
assert_eq!(diag.suggestions.len(), 1);
assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
assert_data_eq!(
emit_human_diagnostics(diag),
str![[r#"
note: mutable variables should use mixedCase
╭▸ <test.sol>:4:17
│
4 │ uint256 my_var = 0;
╰╴ ━━━━━━ help: mutable variables should use mixedCase: `myVar`
"#]]
);
}
#[test]
fn test_emit_diagnostic_ref_preserves_inline_suggestion() {
let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
diag.span(var_span).span_suggestion(
var_span,
"mutable variables should use mixedCase",
var_sugg,
Applicability::MachineApplicable,
);
let sm = std::sync::Arc::new(source_map::SourceMap::empty());
sm.new_source_file(source_map::FileName::custom("test.sol"), CONTRACT.to_string()).unwrap();
let mut emitter = HumanBufferEmitter::new(ColorChoice::Never).source_map(Some(sm));
emitter.emit_diagnostic_ref(&diag);
assert_eq!(diag.suggestions.len(), 1);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
}
#[test]
fn test_allowed_diagnostic_code_suppresses_warning() {
let dcx = DiagCtxt::with_buffer_emitter(None, ColorChoice::Never)
.with_allowed_diagnostic_codes(["1234".to_string()]);
dcx.warn("suppressed warning").code(crate::error_code!(1234)).emit();
dcx.warn("emitted warning").code(crate::error_code!(5678)).emit();
let diagnostics = dcx.emitted_diagnostics().unwrap().to_string();
assert_eq!(dcx.warn_count(), 1);
assert!(diagnostics.contains("emitted warning"));
assert!(!diagnostics.contains("suppressed warning"));
}
#[test]
fn test_allowed_diagnostic_code_does_not_suppress_error() {
let dcx = DiagCtxt::with_buffer_emitter(None, ColorChoice::Never)
.with_allowed_diagnostic_codes(["1234".to_string()]);
let _ = dcx.err("emitted error").code(crate::error_code!(1234)).emit();
assert!(dcx.has_errors().is_err());
assert!(dcx.emitted_diagnostics().unwrap().to_string().contains("emitted error"));
}
#[test]
fn test_suggestion() {
let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
diag.span(var_span).span_suggestion_with_style(
var_span,
"mutable variables should use mixedCase",
var_sugg,
Applicability::MachineApplicable,
SuggestionStyle::ShowAlways,
);
assert_eq!(diag.suggestions.len(), 1);
assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowAlways);
assert_data_eq!(
emit_human_diagnostics(diag),
str![[r#"
note: mutable variables should use mixedCase
╭▸ <test.sol>:4:17
│
4 │ uint256 my_var = 0;
│ ━━━━━━
╰╴
help: mutable variables should use mixedCase
╭╴
4 - uint256 my_var = 0;
4 + uint256 myVar = 0;
╰╴
"#]]
);
}
#[test]
fn test_suggestion_with_footer() {
let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
diag.span(var_span)
.span_suggestion_with_style(
var_span,
"mutable variables should use mixedCase",
var_sugg,
Applicability::MachineApplicable,
SuggestionStyle::ShowAlways,
)
.help("some footer help msg that should be displayed at the very bottom");
assert_eq!(diag.suggestions.len(), 1);
assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowAlways);
assert_data_eq!(
emit_human_diagnostics(diag),
str![[r#"
note: mutable variables should use mixedCase
╭▸ <test.sol>:4:17
│
4 │ uint256 my_var = 0;
│ ━━━━━━
│
╰ help: some footer help msg that should be displayed at the very bottom
help: mutable variables should use mixedCase
╭╴
4 - uint256 my_var = 0;
4 + uint256 myVar = 0;
╰╴
"#]]
);
}
#[test]
fn test_multispan_suggestion() {
let (pub_span, pub_sugg) = (Span::new(BytePos(36), BytePos(42)), "external".into());
let (view_span, view_sugg) = (Span::new(BytePos(43), BytePos(47)), "pure".into());
let mut diag = Diag::new(Level::Warning, "inefficient visibility and mutability");
diag.span(vec![pub_span, view_span]).multipart_suggestion(
"consider changing visibility and mutability",
vec![(pub_span, pub_sugg), (view_span, view_sugg)],
Applicability::MaybeIncorrect,
);
assert_eq!(diag.suggestions[0].substitutions.len(), 1);
assert_eq!(diag.suggestions[0].substitutions[0].parts.len(), 2);
assert_eq!(diag.suggestions[0].applicability, Applicability::MaybeIncorrect);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
assert_data_eq!(
emit_human_diagnostics(diag),
str![[r#"
warning: inefficient visibility and mutability
╭▸ <test.sol>:3:20
│
3 │ function foo() public view {
│ ━━━━━━ ━━━━
╰╴
help: consider changing visibility and mutability
╭╴
3 - function foo() public view {
3 + function foo() external pure {
╰╴
"#]]
);
}
#[test]
#[cfg(feature = "json")]
fn test_json_suggestion() {
let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
diag.span(var_span).span_suggestion(
var_span,
"mutable variables should use mixedCase",
var_sugg,
Applicability::MachineApplicable,
);
assert_eq!(diag.suggestions.len(), 1);
assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
assert_data_eq!(
emit_json_diagnostics(diag),
str![[r#"
{
"$message_type": "diagnostic",
"children": [
{
"children": [],
"code": null,
"level": "help",
"message": "mutable variables should use mixedCase",
"rendered": null,
"spans": [
{
"byte_end": 72,
"byte_start": 66,
"column_end": 23,
"column_start": 17,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 4,
"line_start": 4,
"suggested_replacement": "myVar",
"text": [
{
"highlight_end": 23,
"highlight_start": 17,
"text": " uint256 my_var = 0;"
}
]
}
]
}
],
"code": null,
"level": "note",
"message": "mutable variables should use mixedCase",
"rendered": "note: mutable variables should use mixedCase\n ╭▸ <test.sol>:4:17\n │\n4 │ uint256 my_var = 0;\n ╰╴ ━━━━━━ help: mutable variables should use mixedCase: `myVar`\n\n",
"spans": [
{
"byte_end": 72,
"byte_start": 66,
"column_end": 23,
"column_start": 17,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 4,
"line_start": 4,
"suggested_replacement": null,
"text": [
{
"highlight_end": 23,
"highlight_start": 17,
"text": " uint256 my_var = 0;"
}
]
}
]
}
"#]].is_json()
);
}
#[test]
#[cfg(feature = "json")]
fn test_multispan_json_suggestion() {
let (pub_span, pub_sugg) = (Span::new(BytePos(36), BytePos(42)), "external".into());
let (view_span, view_sugg) = (Span::new(BytePos(43), BytePos(47)), "pure".into());
let mut diag = Diag::new(Level::Warning, "inefficient visibility and mutability");
diag.span(vec![pub_span, view_span]).multipart_suggestion(
"consider changing visibility and mutability",
vec![(pub_span, pub_sugg), (view_span, view_sugg)],
Applicability::MaybeIncorrect,
);
assert_eq!(diag.suggestions[0].substitutions.len(), 1);
assert_eq!(diag.suggestions[0].substitutions[0].parts.len(), 2);
assert_eq!(diag.suggestions[0].applicability, Applicability::MaybeIncorrect);
assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
assert_data_eq!(
emit_json_diagnostics(diag),
str![[r#"
{
"$message_type": "diagnostic",
"children": [
{
"children": [],
"code": null,
"level": "help",
"message": "consider changing visibility and mutability",
"rendered": null,
"spans": [
{
"byte_end": 42,
"byte_start": 36,
"column_end": 26,
"column_start": 20,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 3,
"line_start": 3,
"suggested_replacement": "external",
"text": [
{
"highlight_end": 26,
"highlight_start": 20,
"text": " function foo() public view {"
}
]
},
{
"byte_end": 47,
"byte_start": 43,
"column_end": 31,
"column_start": 27,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 3,
"line_start": 3,
"suggested_replacement": "pure",
"text": [
{
"highlight_end": 31,
"highlight_start": 27,
"text": " function foo() public view {"
}
]
}
]
}
],
"code": null,
"level": "warning",
"message": "inefficient visibility and mutability",
"rendered": "warning: inefficient visibility and mutability\n ╭▸ <test.sol>:3:20\n │\n3 │ function foo() public view {\n │ ━━━━━━ ━━━━\n ╰╴\nhelp: consider changing visibility and mutability\n ╭╴\n3 - function foo() public view {\n3 + function foo() external pure {\n ╰╴\n\n",
"spans": [
{
"byte_end": 42,
"byte_start": 36,
"column_end": 26,
"column_start": 20,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 3,
"line_start": 3,
"suggested_replacement": null,
"text": [
{
"highlight_end": 26,
"highlight_start": 20,
"text": " function foo() public view {"
}
]
},
{
"byte_end": 47,
"byte_start": 43,
"column_end": 31,
"column_start": 27,
"file_name": "<test.sol>",
"is_primary": true,
"label": null,
"line_end": 3,
"line_start": 3,
"suggested_replacement": null,
"text": [
{
"highlight_end": 31,
"highlight_start": 27,
"text": " function foo() public view {"
}
]
}
]
}
"#]].is_json()
);
}
const CONTRACT: &str = r#"
contract Test {
function foo() public view {
uint256 my_var = 0;
}
}"#;
fn emit_human_diagnostics(diag: Diag) -> String {
let sm = source_map::SourceMap::empty();
sm.new_source_file(source_map::FileName::custom("test.sol"), CONTRACT.to_string()).unwrap();
let dcx = DiagCtxt::with_buffer_emitter(Some(std::sync::Arc::new(sm)), ColorChoice::Never);
let _ = dcx.emit_diagnostic(diag);
dcx.emitted_diagnostics().unwrap().0
}
#[cfg(feature = "json")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "json")]
#[derive(Clone)]
struct SharedWriter(Arc<Mutex<Vec<u8>>>);
#[cfg(feature = "json")]
impl std::io::Write for SharedWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.lock().unwrap().flush()
}
}
#[cfg(feature = "json")]
fn emit_json_diagnostics(diag: Diag) -> String {
let sm = Arc::new(source_map::SourceMap::empty());
sm.new_source_file(source_map::FileName::custom("test.sol"), CONTRACT.to_string()).unwrap();
let writer = Arc::new(Mutex::new(Vec::new()));
let emitter = JsonEmitter::new(Box::new(SharedWriter(writer.clone())), Arc::clone(&sm))
.rustc_like(true);
let dcx = DiagCtxt::new(Box::new(emitter));
let _ = dcx.emit_diagnostic(diag);
let buffer = writer.lock().unwrap();
String::from_utf8(buffer.clone()).expect("JSON output was not valid UTF-8")
}
}