use std::io::IsTerminal;
use annotate_snippets::{Level, Renderer, Snippet};
use banshee_hir::{Diagnostic, Severity};
use super::io::{InputFile, json_escape, line_col};
use super::rules_cmd;
fn level_of(severity: Severity) -> Level {
match severity {
Severity::Error => Level::Error,
Severity::Warning => Level::Warning,
Severity::Info => Level::Info,
Severity::Hint => Level::Note,
_ => Level::Warning,
}
}
pub fn use_color(force_no_color: bool) -> bool {
if force_no_color || std::env::var_os("NO_COLOR").is_some() {
return false;
}
std::io::stdout().is_terminal()
}
#[must_use]
pub fn render(origin: &str, source: &str, diagnostics: &[Diagnostic], color: bool) -> String {
let renderer = if color {
Renderer::styled()
} else {
Renderer::plain()
};
let mut out = String::new();
for diag in diagnostics {
let level = level_of(diag.severity);
let (start, end) = diag
.range
.map(|r| (u32::from(r.start()) as usize, u32::from(r.end()) as usize))
.unwrap_or((0, 0));
let start = start.min(source.len());
let end = end.min(source.len()).max(start);
let mut snippet = Snippet::source(source)
.origin(origin)
.fold(true)
.annotation(level.span(start..end));
for rel in &diag.related {
if let Some(r) = rel.range {
let rs = (u32::from(r.start()) as usize).min(source.len());
let re = (u32::from(r.end()) as usize).min(source.len()).max(rs);
snippet = snippet.annotation(Level::Info.span(rs..re).label(&rel.message));
}
}
let mut message = level.title(&diag.message).snippet(snippet);
if let Some(code) = diag.code {
message = message.id(code.as_str());
}
let help_note;
if let Some(help) = &diag.help {
help_note = Level::Help.title(help.as_str());
message = message.footer(help_note);
}
for rel in diag.related.iter().filter(|rel| rel.range.is_none()) {
message = message.footer(Level::Note.title(rel.message.as_str()));
}
let fix_text;
let fix_note;
if let Some(fix) = diag.fixes.first() {
fix_text = format!("{} — run `banshee fix`", fix.title);
fix_note = Level::Note.title(&fix_text);
message = message.footer(fix_note);
}
out.push_str(&renderer.render(message).to_string());
out.push('\n');
}
let mut codes: Vec<&'static str> = Vec::new();
for diag in diagnostics {
if let Some(code) = diag.code {
let code = code.as_str();
if !codes.contains(&code) {
codes.push(code);
}
}
}
if let Some(first) = codes.first() {
let summary = format!(
"Some lints have detailed explanations: {}.\nFor more information, try `banshee explain {}`.",
codes.join(", "),
first
);
out.push_str(&renderer.render(Level::Note.title(&summary)).to_string());
out.push('\n');
}
out
}
pub fn render_sarif(files: &[(&InputFile, Vec<Diagnostic>)]) -> String {
let rules: Vec<String> = rules_cmd::RULES
.iter()
.map(|r| {
format!(
r#"{{"id":{},"shortDescription":{{"text":{}}}}}"#,
json_escape(r.code),
json_escape(r.summary)
)
})
.collect();
let mut results: Vec<String> = Vec::new();
for (input, diags) in files {
for d in diags {
let (sl, sc) = d
.range
.map(|r| line_col(&input.text, u32::from(r.start())))
.unwrap_or((1, 1));
let (el, ec) = d
.range
.map(|r| line_col(&input.text, u32::from(r.end())))
.unwrap_or((1, 1));
let level = match d.severity {
Severity::Error => "error",
Severity::Info | Severity::Hint => "note",
_ => "warning",
};
let text = match &d.help {
Some(h) => format!("{} (help: {h})", d.message),
None => d.message.clone(),
};
let rule_id = d.code.map(|c| c.as_str()).unwrap_or("syntax");
results.push(format!(
r#"{{"ruleId":{},"level":"{}","message":{{"text":{}}},"locations":[{{"physicalLocation":{{"artifactLocation":{{"uri":{}}},"region":{{"startLine":{},"startColumn":{},"endLine":{},"endColumn":{}}}}}}}]}}"#,
json_escape(rule_id),
level,
json_escape(&text),
json_escape(&input.label),
sl,
sc,
el,
ec,
));
}
}
format!(
r#"{{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","version":"2.1.0","runs":[{{"tool":{{"driver":{{"name":"banshee","rules":[{}]}}}},"results":[{}]}}]}}"#,
rules.join(","),
results.join(",")
)
}