Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions crates/plotnik-cli/src/commands/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::fs;
use std::io::{self, Read};

use plotnik_lib::Query;
use plotnik_lib::RenderOptions;

use source::{dump_source, load_source, parse_tree, resolve_lang};

Expand Down Expand Up @@ -97,12 +96,7 @@ pub fn run(args: DebugArgs) {
if let Some(ref q) = query
&& !q.is_valid()
{
let options = if args.color {
RenderOptions::colored()
} else {
RenderOptions::plain()
};
eprint!("{}", q.render_diagnostics(options));
eprint!("{}", q.render_diagnostics_colored(args.color));
}
}

Expand Down
118 changes: 118 additions & 0 deletions crates/plotnik-lib/src/diagnostics/message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Diagnostic message types and related structures.

use rowan::TextRange;

/// Severity level of a diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Severity {
#[default]
Error,
Warning,
}

impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
}
}
}

/// A suggested fix for a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fix {
pub(crate) replacement: String,
pub(crate) description: String,
}

impl Fix {
pub fn new(replacement: impl Into<String>, description: impl Into<String>) -> Self {
Self {
replacement: replacement.into(),
description: description.into(),
}
}
}

/// Related location information for a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedInfo {
pub(crate) range: TextRange,
pub(crate) message: String,
}

impl RelatedInfo {
pub fn new(range: TextRange, message: impl Into<String>) -> Self {
Self {
range,
message: message.into(),
}
}
}

/// A diagnostic message with location, message, severity, and optional fix.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DiagnosticMessage {
pub(crate) severity: Severity,
pub(crate) range: TextRange,
pub(crate) message: String,
pub(crate) fix: Option<Fix>,
pub(crate) related: Vec<RelatedInfo>,
}

impl DiagnosticMessage {
pub(crate) fn error(range: TextRange, message: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
range,
message: message.into(),
fix: None,
related: Vec::new(),
}
}

pub(crate) fn warning(range: TextRange, message: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
range,
message: message.into(),
fix: None,
related: Vec::new(),
}
}

pub(crate) fn is_error(&self) -> bool {
self.severity == Severity::Error
}

pub(crate) fn is_warning(&self) -> bool {
self.severity == Severity::Warning
}
}

impl std::fmt::Display for DiagnosticMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} at {}..{}: {}",
self.severity,
u32::from(self.range.start()),
u32::from(self.range.end()),
self.message
)?;
if let Some(fix) = &self.fix {
write!(f, " (fix: {})", fix.description)?;
}
for related in &self.related {
write!(
f,
" (related: {} at {}..{})",
related.message,
u32::from(related.range.start()),
u32::from(related.range.end())
)?;
}
Ok(())
}
}
99 changes: 99 additions & 0 deletions crates/plotnik-lib/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Compiler diagnostics infrastructure.
//!
//! This module provides types for collecting and rendering diagnostic messages.

mod message;
mod printer;

#[cfg(test)]
mod tests;

use rowan::TextRange;

pub use message::Severity;
pub use printer::DiagnosticsPrinter;

use message::{DiagnosticMessage, Fix, RelatedInfo};

/// Collection of diagnostic messages from parsing and analysis.
#[derive(Debug, Clone, Default)]
pub struct Diagnostics {
messages: Vec<DiagnosticMessage>,
}

/// Builder for constructing a diagnostic message.
#[must_use = "diagnostic not emitted, call .emit()"]
pub struct DiagnosticBuilder<'a> {
diagnostics: &'a mut Diagnostics,
message: DiagnosticMessage,
}

impl Diagnostics {
pub fn new() -> Self {
Self {
messages: Vec::new(),
}
}

pub fn error(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
DiagnosticBuilder {
diagnostics: self,
message: DiagnosticMessage::error(range, msg),
}
}

pub fn warning(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
DiagnosticBuilder {
diagnostics: self,
message: DiagnosticMessage::warning(range, msg),
}
}

pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}

pub fn len(&self) -> usize {
self.messages.len()
}

pub fn has_errors(&self) -> bool {
self.messages.iter().any(|d| d.is_error())
}

pub fn has_warnings(&self) -> bool {
self.messages.iter().any(|d| d.is_warning())
}

pub fn error_count(&self) -> usize {
self.messages.iter().filter(|d| d.is_error()).count()
}

pub fn warning_count(&self) -> usize {
self.messages.iter().filter(|d| d.is_warning()).count()
}

pub fn printer<'a>(&'a self, source: &'a str) -> DiagnosticsPrinter<'a> {
DiagnosticsPrinter::new(&self.messages, source)
}

pub fn extend(&mut self, other: Diagnostics) {
self.messages.extend(other.messages);
}
}

impl<'a> DiagnosticBuilder<'a> {
pub fn related_to(mut self, msg: impl Into<String>, range: TextRange) -> Self {
self.message.related.push(RelatedInfo::new(range, msg));
self
}

pub fn fix(mut self, description: impl Into<String>, replacement: impl Into<String>) -> Self {
self.message.fix = Some(Fix::new(replacement, description));
self
}

pub fn emit(self) {
self.diagnostics.messages.push(self.message);
}
}
112 changes: 112 additions & 0 deletions crates/plotnik-lib/src/diagnostics/printer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Builder-pattern printer for rendering diagnostics.

use std::fmt::Write;

use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet};
use rowan::TextRange;

use super::message::{DiagnosticMessage, Severity};

pub struct DiagnosticsPrinter<'a> {
diagnostics: &'a [DiagnosticMessage],
source: &'a str,
path: Option<&'a str>,
colored: bool,
}

impl<'a> DiagnosticsPrinter<'a> {
pub(crate) fn new(diagnostics: &'a [DiagnosticMessage], source: &'a str) -> Self {
Self {
diagnostics,
source,
path: None,
colored: false,
}
}

pub fn path(mut self, path: &'a str) -> Self {
self.path = Some(path);
self
}

pub fn colored(mut self, value: bool) -> Self {
self.colored = value;
self
}

pub fn render(&self) -> String {
let mut out = String::new();
self.format(&mut out).expect("String write never fails");
out
}

pub fn format(&self, w: &mut impl Write) -> std::fmt::Result {
let renderer = if self.colored {
Renderer::styled()
} else {
Renderer::plain()
};

for (i, diag) in self.diagnostics.iter().enumerate() {
let range = adjust_range(diag.range, self.source.len());

let mut snippet = Snippet::source(self.source).line_start(1).annotation(
AnnotationKind::Primary
.span(range.clone())
.label(&diag.message),
);

if let Some(p) = self.path {
snippet = snippet.path(p);
}

for related in &diag.related {
snippet = snippet.annotation(
AnnotationKind::Context
.span(adjust_range(related.range, self.source.len()))
.label(&related.message),
);
}

let level = severity_to_level(diag.severity);
let title_group = level.primary_title(&diag.message).element(snippet);

let mut report: Vec<Group> = vec![title_group];

if let Some(fix) = &diag.fix {
report.push(
Level::HELP.secondary_title(&fix.description).element(
Snippet::source(self.source)
.line_start(1)
.patch(Patch::new(range, &fix.replacement)),
),
);
}

if i > 0 {
w.write_char('\n')?;
}
write!(w, "{}", renderer.render(&report))?;
}

Ok(())
}
}

fn severity_to_level(severity: Severity) -> Level<'static> {
match severity {
Severity::Error => Level::ERROR,
Severity::Warning => Level::WARNING,
}
}

fn adjust_range(range: TextRange, limit: usize) -> std::ops::Range<usize> {
let start: usize = range.start().into();
let end: usize = range.end().into();

if start == end {
return start..(start + 1).min(limit);
}

start..end
}
Loading