use std::fmt;
use crate::check::FileId;
use crate::span::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Label {
pub file: Option<FileId>,
pub span: Span,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub code: &'static str,
pub severity: Severity,
pub message: String,
pub span: Span,
pub help: Option<String>,
pub labels: Vec<Label>,
pub notes: Vec<String>,
}
impl Diagnostic {
pub fn error(code: &'static str, span: Span, message: impl Into<String>) -> Self {
Self {
code,
severity: Severity::Error,
message: message.into(),
span,
help: None,
labels: Vec::new(),
notes: Vec::new(),
}
}
pub fn warning(code: &'static str, span: Span, message: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
..Self::error(code, span, message)
}
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn with_optional_help(mut self, help: Option<String>) -> Self {
self.help = help;
self
}
pub fn with_label(
mut self,
file: Option<FileId>,
span: Span,
message: impl Into<String>,
) -> Self {
self.labels.push(Label {
file,
span,
message: message.into(),
});
self
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileDiagnostic {
pub file: Option<FileId>,
pub diagnostic: Diagnostic,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NamedSource {
pub(crate) name: String,
pub(crate) text: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Diagnostics {
pub(crate) files: Vec<NamedSource>,
pub(crate) items: Vec<FileDiagnostic>,
}
impl Diagnostics {
pub fn iter(&self) -> impl Iterator<Item = &FileDiagnostic> {
self.items.iter()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn has_errors(&self) -> bool {
self.items
.iter()
.any(|item| item.diagnostic.severity == Severity::Error)
}
pub fn file_name(&self, file: FileId) -> Option<&str> {
self.files.get(file.index()).map(|f| f.name.as_str())
}
pub fn source(&self, file: FileId) -> Option<&str> {
self.files.get(file.index()).map(|f| f.text.as_str())
}
fn location(&self, file: Option<FileId>, span: Span) -> String {
let Some(source) = file.and_then(|file| self.files.get(file.index())) else {
return format!("{}..{}", span.start, span.end);
};
let (line, column) = line_column(&source.text, span.start);
format!("{}:{line}:{column}", source.name)
}
}
fn line_column(text: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(text.len());
let before = text.get(..offset).unwrap_or(text);
let line = before.matches('\n').count() + 1;
let start = before.rfind('\n').map_or(0, |i| i + 1);
let column = before[start..].chars().count() + 1;
(line, column)
}
impl fmt::Display for Diagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for item in &self.items {
let d = &item.diagnostic;
let kind = match d.severity {
Severity::Error => "error",
Severity::Warning => "warning",
};
writeln!(f, "{kind}[{}]: {}", d.code, d.message)?;
if item.file.is_some() {
writeln!(f, " --> {}", self.location(item.file, d.span))?;
}
for label in &d.labels {
let file = label.file.or(item.file);
writeln!(
f,
" label: {}: {}",
self.location(file, label.span),
label.message
)?;
}
for note in &d.notes {
writeln!(f, " note: {note}")?;
}
if let Some(help) = &d.help {
writeln!(f, " help: {help}")?;
}
}
Ok(())
}
}
#[cfg(test)]
#[path = "diagnostic_tests.rs"]
mod tests;