use codespan_reporting::diagnostic as cs;
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term::{self, Config};
use super::{Diagnostic, Severity, SourceMap, Span};
use crate::lint;
pub fn render(diags: &[Diagnostic], sources: &SourceMap) -> String {
let mut files = SimpleFiles::new();
let mut file_count = 0usize;
for (path, text) in sources.iter_files() {
files.add(path, text);
file_count += 1;
}
let config = Config::default();
let mut out = String::new();
for diag in diags {
let rendered = to_codespan(diag, file_count);
term::emit_to_string(&mut out, &config, &files, &rendered)
.expect("rendering to an in-memory string cannot fail");
}
out
}
fn to_codespan(diag: &Diagnostic, file_count: usize) -> cs::Diagnostic<usize> {
let severity = match diag.severity {
Severity::Error => cs::Severity::Error,
Severity::Warning => cs::Severity::Warning,
Severity::Info => cs::Severity::Note,
};
let in_range = |span: &Span| (span.file.0 as usize) < file_count;
let mut labels = Vec::new();
if in_range(&diag.primary) {
labels.push(label(&diag.primary, cs::LabelStyle::Primary, ""));
}
for secondary in &diag.labels {
if in_range(&secondary.span) {
labels.push(label(
&secondary.span,
cs::LabelStyle::Secondary,
&secondary.message,
));
}
}
for fixit in &diag.fixits {
if in_range(&fixit.span) {
labels.push(label(&fixit.span, cs::LabelStyle::Secondary, &fixit.label));
}
}
let mut notes: Vec<String> = diag
.fixits
.iter()
.map(|fixit| {
format!(
"suggestion: replace with `{}` — {}",
fixit.replacement, fixit.label
)
})
.collect();
if let Some(name) = lint::lint_of(diag.code).and_then(|entry| entry.lint) {
notes.push(format!(
"lint: `{name}` (set its level in `[lints]` in ridl.toml)"
));
}
let mut rendered = cs::Diagnostic::new(severity)
.with_message(&diag.message)
.with_labels(labels)
.with_notes(notes);
if !diag.code.is_empty() {
rendered = rendered.with_code(diag.code.as_str());
}
rendered
}
fn label(span: &Span, style: cs::LabelStyle, message: &str) -> cs::Label<usize> {
let start = u32::from(span.range.start()) as usize;
let end = u32::from(span.range.end()) as usize;
let built = cs::Label::new(style, span.file.0 as usize, start..end);
if message.is_empty() {
built
} else {
built.with_message(message)
}
}
#[cfg(test)]
mod tests {
use super::super::{DiagCode, Diagnostic, FixIt, Severity, SourceMap, Span};
use rowan::{TextRange, TextSize};
fn span(map: &mut SourceMap, path: &str, text: &str, start: u32, end: u32) -> Span {
let file = map.file_id(path, text);
Span {
file,
range: TextRange::new(TextSize::new(start), TextSize::new(end)),
}
}
#[test]
fn two_diagnostics_at_one_offset_render_cleanly() {
let text = "package p\ntype X: integer [0..10ms]\n";
let mut map = SourceMap::new();
let at_duration = span(&mut map, "demo.typl", text, 30, 34);
let diags = vec![
Diagnostic {
code: DiagCode::FORM_101,
severity: Severity::Error,
message: "expected `]`".to_string(),
primary: at_duration,
labels: Vec::new(),
fixits: Vec::new(),
},
Diagnostic {
code: DiagCode::TYPL_302,
severity: Severity::Error,
message: "duration literal in typl context".to_string(),
primary: at_duration,
labels: Vec::new(),
fixits: Vec::new(),
},
];
let rendered = super::render(&diags, &map);
insta::assert_snapshot!("two_diagnostics_same_offset", rendered);
}
#[test]
fn fixit_renders_its_suggestion() {
let text = "package p\ntype Speed: km/h\n";
let mut map = SourceMap::new();
let at_name = span(&mut map, "demo.typl", text, 15, 20); let diags = vec![Diagnostic {
code: DiagCode::NONE,
severity: Severity::Warning,
message: "type name should be capitalised".to_string(),
primary: at_name,
labels: Vec::new(),
fixits: vec![FixIt {
span: at_name,
replacement: "Velocity".to_string(),
label: "rename to `Velocity`".to_string(),
}],
}];
let rendered = super::render(&diags, &map);
assert!(
rendered.contains("suggestion: replace with `Velocity`"),
"the rendered output must spell the suggested replacement, got:\n{rendered}",
);
assert!(rendered.contains("rename to `Velocity`"));
}
#[test]
fn lint_diagnostic_renders_its_lint_note() {
let text = "package p\ninterface S {\n signal speed: Speed\n}\n";
let mut map = SourceMap::new();
let at_signal = span(&mut map, "demo.ridl", text, 32, 37); let diags = vec![Diagnostic {
code: DiagCode::RIDL_100,
severity: Severity::Warning,
message: "signal without a timing annotation".to_string(),
primary: at_signal,
labels: Vec::new(),
fixits: vec![FixIt {
span: at_signal,
replacement: "speed: Speed @10ms".to_string(),
label: "write the timing".to_string(),
}],
}];
let rendered = super::render(&diags, &map);
assert!(
rendered.lines().any(|line| line
== " = lint: `missing-timing` (set its level in `[lints]` in ridl.toml)"),
"the rendered output must hold the lint note line, got:\n{rendered}",
);
insta::assert_snapshot!("lint_diagnostic_note", rendered);
}
#[test]
fn cross_line_span_renders_without_panic() {
let text = "type A: m\n\ntype B: s\n";
let mut map = SourceMap::new();
let across = span(&mut map, "demo.typl", text, 9, 15);
let diags = vec![Diagnostic {
code: DiagCode::FORM_103,
severity: Severity::Error,
message: "unclosed `{`".to_string(),
primary: across,
labels: Vec::new(),
fixits: Vec::new(),
}];
let rendered = super::render(&diags, &map);
assert!(
rendered.contains("FORM-103"),
"the rendered output must carry the code, got:\n{rendered}",
);
}
#[test]
fn no_diagnostics_render_to_empty_string() {
let map = SourceMap::new();
assert_eq!(super::render(&[], &map), "");
}
#[test]
fn detached_diagnostic_renders_without_a_source_snippet() {
use super::super::FileId;
let diags = vec![Diagnostic {
code: DiagCode::MANI_101,
severity: Severity::Error,
message: "failed to fetch `https://registry.example.com/foo@v1`".to_string(),
primary: Span {
file: FileId::DETACHED,
range: TextRange::default(),
},
labels: Vec::new(),
fixits: Vec::new(),
}];
let map = SourceMap::new();
let rendered = super::render(&diags, &map);
assert!(
rendered.contains("MANI-101"),
"the rendered output must carry the code, got:\n{rendered}",
);
assert!(
rendered.contains("failed to fetch"),
"the rendered output must carry the message, got:\n{rendered}",
);
}
}