use crate::source::{SourceFile, Span};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Label {
pub span: Span,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: String,
pub message: String,
pub primary: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<Label>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
}
impl Diagnostic {
#[must_use]
pub fn error(code: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Error,
code: code.into(),
message: message.into(),
primary: span,
labels: Vec::new(),
help: None,
}
}
#[must_use]
pub fn warning(code: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Warning,
code: code.into(),
message: message.into(),
primary: span,
labels: Vec::new(),
help: None,
}
}
#[must_use]
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
self.labels.push(Label {
span,
message: message.into(),
});
self
}
#[must_use]
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
#[must_use]
pub fn render(&self, source: &SourceFile) -> String {
let (line, column) = source.line_col(self.primary.start);
let kind = match self.severity {
Severity::Error => "error",
Severity::Warning => "warning",
};
let mut out = format!(
"{kind}[{}]: {}\n --> {}:{line}:{column}\n",
self.code,
self.message,
source.name_at(self.primary.start)
);
let text = source.line_text_at(self.primary.start);
if !text.is_empty() {
let _ = writeln!(out, " |\n{line:>3}| {text}");
let width = source
.text
.get(self.primary.start..self.primary.end)
.and_then(|text| text.lines().next())
.map_or(1, |text| text.chars().count().max(1));
let _ = writeln!(
out,
" | {}{}",
" ".repeat(column.saturating_sub(1)),
"^".repeat(width.min(text.len().saturating_add(1)))
);
}
for label in &self.labels {
let (label_line, label_column) = source.line_col(label.span.start);
let _ = writeln!(
out,
" = related {}:{label_line}:{label_column}: {}",
source.name_at(label.span.start),
label.message
);
}
if let Some(help) = &self.help {
let _ = writeln!(out, " = help: {help}");
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unicode_highlight_width_uses_scalar_columns_not_bytes() {
let source = SourceFile::new("test.tes", "module 한글\n");
let start = "module ".len();
let diagnostic =
Diagnostic::error("T0001", "sample", Span::new(start, start + "한글".len()));
let rendered = diagnostic.render(&source);
let marker = rendered.lines().find(|line| line.contains('^')).unwrap();
assert_eq!(
marker.chars().filter(|character| *character == '^').count(),
2
);
}
}