#![warn(missing_docs)]
pub mod explain;
pub mod json;
pub mod lsp;
pub mod render;
use bhc_span::{FileId, SourceFile};
pub use bhc_span::{FullSpan, Span};
use serde::{Deserialize, Serialize};
use std::io::Write;
pub use explain::{all_error_codes, format_explanation, get_explanation, print_explanation};
pub use json::{diagnostic_to_json, diagnostics_to_json, to_json_lines, to_json_string};
pub use json::{JsonApplicability, JsonDiagnostic, JsonSeverity, JsonSpan, JsonSuggestion};
pub use lsp::{
publish_diagnostics, to_code_actions, to_hover, to_lsp_diagnostic, to_lsp_diagnostics,
LspCodeAction, LspDiagnostic, LspHover, LspRange, LspSeverity, LspTextEdit,
PublishDiagnosticsParams,
};
pub use render::{colors, CargoRenderer, RenderConfig};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Severity {
Bug,
Error,
Warning,
Note,
Help,
}
impl Severity {
#[must_use]
pub fn color(self) -> &'static str {
match self {
Self::Bug => "\x1b[1;35m", Self::Error => "\x1b[1;31m", Self::Warning => "\x1b[1;33m", Self::Note => "\x1b[1;36m", Self::Help => "\x1b[1;32m", }
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Bug => "internal compiler error",
Self::Error => "error",
Self::Warning => "warning",
Self::Note => "note",
Self::Help => "help",
}
}
}
#[derive(Clone, Debug)]
pub struct Label {
pub span: FullSpan,
pub message: String,
pub primary: bool,
}
impl Label {
#[must_use]
pub fn primary(span: FullSpan, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
primary: true,
}
}
#[must_use]
pub fn secondary(span: FullSpan, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
primary: false,
}
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub code: Option<String>,
pub labels: Vec<Label>,
pub notes: Vec<String>,
pub suggestions: Vec<Suggestion>,
}
impl Diagnostic {
#[must_use]
pub fn error(message: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
message: message.into(),
code: None,
labels: Vec::new(),
notes: Vec::new(),
suggestions: Vec::new(),
}
}
#[must_use]
pub fn warning(message: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
message: message.into(),
code: None,
labels: Vec::new(),
notes: Vec::new(),
suggestions: Vec::new(),
}
}
#[must_use]
pub fn bug(message: impl Into<String>) -> Self {
Self {
severity: Severity::Bug,
message: message.into(),
code: None,
labels: Vec::new(),
notes: Vec::new(),
suggestions: Vec::new(),
}
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
#[must_use]
pub fn with_label(mut self, span: FullSpan, message: impl Into<String>) -> Self {
self.labels.push(Label::primary(span, message));
self
}
#[must_use]
pub fn with_secondary_label(mut self, span: FullSpan, message: impl Into<String>) -> Self {
self.labels.push(Label::secondary(span, message));
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self {
self.suggestions.push(suggestion);
self
}
#[must_use]
pub fn is_error(&self) -> bool {
matches!(self.severity, Severity::Error | Severity::Bug)
}
}
#[derive(Clone, Debug)]
pub struct Suggestion {
pub message: String,
pub span: FullSpan,
pub replacement: String,
pub applicability: Applicability,
}
impl Suggestion {
#[must_use]
pub fn new(
message: impl Into<String>,
span: FullSpan,
replacement: impl Into<String>,
applicability: Applicability,
) -> Self {
Self {
message: message.into(),
span,
replacement: replacement.into(),
applicability,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Applicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
Unspecified,
}
#[derive(Debug, Default)]
pub struct DiagnosticHandler {
diagnostics: Vec<Diagnostic>,
error_count: usize,
warning_count: usize,
}
impl DiagnosticHandler {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn emit(&mut self, diagnostic: Diagnostic) {
match diagnostic.severity {
Severity::Error | Severity::Bug => self.error_count += 1,
Severity::Warning => self.warning_count += 1,
_ => {}
}
self.diagnostics.push(diagnostic);
}
#[must_use]
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
#[must_use]
pub fn error_count(&self) -> usize {
self.error_count
}
#[must_use]
pub fn warning_count(&self) -> usize {
self.warning_count
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
self.error_count = 0;
self.warning_count = 0;
std::mem::take(&mut self.diagnostics)
}
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_file(&mut self, name: String, src: String) -> FileId {
let id = FileId::new(self.files.len() as u32);
self.files.push(SourceFile::new(id, name, src));
id
}
#[must_use]
pub fn get_file(&self, id: FileId) -> Option<&SourceFile> {
self.files.get(id.0 as usize)
}
#[must_use]
pub fn len(&self) -> usize {
self.files.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
pub struct DiagnosticRenderer<'a> {
source_map: &'a SourceMap,
use_colors: bool,
}
impl<'a> DiagnosticRenderer<'a> {
#[must_use]
pub fn new(source_map: &'a SourceMap) -> Self {
Self {
source_map,
use_colors: true,
}
}
#[must_use]
pub fn without_colors(mut self) -> Self {
self.use_colors = false;
self
}
pub fn render(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
let reset = if self.use_colors { "\x1b[0m" } else { "" };
let color = if self.use_colors {
diagnostic.severity.color()
} else {
""
};
write!(w, "{}{}", color, diagnostic.severity.label())?;
if let Some(code) = &diagnostic.code {
write!(w, "[{code}]")?;
}
writeln!(w, "{reset}: {}", diagnostic.message)?;
for label in &diagnostic.labels {
if let Some(file) = self.source_map.get_file(label.span.file) {
let loc = file.lookup_line_col(label.span.span.lo);
let arrow = if label.primary { "-->" } else { " " };
writeln!(w, " {arrow} {}:{}:{}", file.name, loc.line, loc.col)?;
if !label.span.span.is_dummy() {
let source = file.source_text(label.span.span);
writeln!(w, " |")?;
writeln!(w, " | {source}")?;
writeln!(w, " | {}", "^".repeat(source.len().max(1)))?;
if !label.message.is_empty() {
writeln!(w, " | {}", label.message)?;
}
}
}
}
for note in &diagnostic.notes {
writeln!(w, " = note: {note}")?;
}
for suggestion in &diagnostic.suggestions {
writeln!(w, " = help: {}", suggestion.message)?;
if !suggestion.replacement.is_empty() {
writeln!(w, " |")?;
writeln!(w, " | {}", suggestion.replacement)?;
}
}
writeln!(w)?;
Ok(())
}
pub fn render_all(&self, diagnostics: &[Diagnostic]) {
let mut stderr = std::io::stderr().lock();
for diag in diagnostics {
let _ = self.render(diag, &mut stderr);
}
}
}
pub trait IntoDiagnostic {
fn into_diagnostic(self) -> Diagnostic;
}
impl IntoDiagnostic for Diagnostic {
fn into_diagnostic(self) -> Diagnostic {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_builder() {
let span = FullSpan::new(FileId::new(0), Span::from_raw(10, 20));
let diag = Diagnostic::error("type mismatch")
.with_code("E0001")
.with_label(span, "expected `Int`, found `String`")
.with_note("consider using `show` to convert to String");
assert!(diag.is_error());
assert_eq!(diag.code, Some("E0001".to_string()));
assert_eq!(diag.labels.len(), 1);
assert_eq!(diag.notes.len(), 1);
}
#[test]
fn test_diagnostic_handler() {
let mut handler = DiagnosticHandler::new();
handler.emit(Diagnostic::error("error 1"));
handler.emit(Diagnostic::warning("warning 1"));
handler.emit(Diagnostic::error("error 2"));
assert!(handler.has_errors());
assert_eq!(handler.error_count(), 2);
assert_eq!(handler.warning_count(), 1);
}
}