Skip to content

Commit ae80097

Browse files
authored
refactor: Diagnostics handling (#32)
1 parent d251832 commit ae80097

35 files changed

Lines changed: 1117 additions & 1313 deletions

crates/plotnik-cli/src/commands/debug/mod.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::fs;
44
use std::io::{self, Read};
55

66
use plotnik_lib::Query;
7-
use plotnik_lib::RenderOptions;
87

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

@@ -97,12 +96,7 @@ pub fn run(args: DebugArgs) {
9796
if let Some(ref q) = query
9897
&& !q.is_valid()
9998
{
100-
let options = if args.color {
101-
RenderOptions::colored()
102-
} else {
103-
RenderOptions::plain()
104-
};
105-
eprint!("{}", q.render_diagnostics(options));
99+
eprint!("{}", q.render_diagnostics_colored(args.color));
106100
}
107101
}
108102

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! Diagnostic message types and related structures.
2+
3+
use rowan::TextRange;
4+
5+
/// Severity level of a diagnostic.
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7+
pub enum Severity {
8+
#[default]
9+
Error,
10+
Warning,
11+
}
12+
13+
impl std::fmt::Display for Severity {
14+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15+
match self {
16+
Severity::Error => write!(f, "error"),
17+
Severity::Warning => write!(f, "warning"),
18+
}
19+
}
20+
}
21+
22+
/// A suggested fix for a diagnostic.
23+
#[derive(Debug, Clone, PartialEq, Eq)]
24+
pub struct Fix {
25+
pub(crate) replacement: String,
26+
pub(crate) description: String,
27+
}
28+
29+
impl Fix {
30+
pub fn new(replacement: impl Into<String>, description: impl Into<String>) -> Self {
31+
Self {
32+
replacement: replacement.into(),
33+
description: description.into(),
34+
}
35+
}
36+
}
37+
38+
/// Related location information for a diagnostic.
39+
#[derive(Debug, Clone, PartialEq, Eq)]
40+
pub struct RelatedInfo {
41+
pub(crate) range: TextRange,
42+
pub(crate) message: String,
43+
}
44+
45+
impl RelatedInfo {
46+
pub fn new(range: TextRange, message: impl Into<String>) -> Self {
47+
Self {
48+
range,
49+
message: message.into(),
50+
}
51+
}
52+
}
53+
54+
/// A diagnostic message with location, message, severity, and optional fix.
55+
#[derive(Debug, Clone, PartialEq, Eq)]
56+
pub(crate) struct DiagnosticMessage {
57+
pub(crate) severity: Severity,
58+
pub(crate) range: TextRange,
59+
pub(crate) message: String,
60+
pub(crate) fix: Option<Fix>,
61+
pub(crate) related: Vec<RelatedInfo>,
62+
}
63+
64+
impl DiagnosticMessage {
65+
pub(crate) fn error(range: TextRange, message: impl Into<String>) -> Self {
66+
Self {
67+
severity: Severity::Error,
68+
range,
69+
message: message.into(),
70+
fix: None,
71+
related: Vec::new(),
72+
}
73+
}
74+
75+
pub(crate) fn warning(range: TextRange, message: impl Into<String>) -> Self {
76+
Self {
77+
severity: Severity::Warning,
78+
range,
79+
message: message.into(),
80+
fix: None,
81+
related: Vec::new(),
82+
}
83+
}
84+
85+
pub(crate) fn is_error(&self) -> bool {
86+
self.severity == Severity::Error
87+
}
88+
89+
pub(crate) fn is_warning(&self) -> bool {
90+
self.severity == Severity::Warning
91+
}
92+
}
93+
94+
impl std::fmt::Display for DiagnosticMessage {
95+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96+
write!(
97+
f,
98+
"{} at {}..{}: {}",
99+
self.severity,
100+
u32::from(self.range.start()),
101+
u32::from(self.range.end()),
102+
self.message
103+
)?;
104+
if let Some(fix) = &self.fix {
105+
write!(f, " (fix: {})", fix.description)?;
106+
}
107+
for related in &self.related {
108+
write!(
109+
f,
110+
" (related: {} at {}..{})",
111+
related.message,
112+
u32::from(related.range.start()),
113+
u32::from(related.range.end())
114+
)?;
115+
}
116+
Ok(())
117+
}
118+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Compiler diagnostics infrastructure.
2+
//!
3+
//! This module provides types for collecting and rendering diagnostic messages.
4+
5+
mod message;
6+
mod printer;
7+
8+
#[cfg(test)]
9+
mod tests;
10+
11+
use rowan::TextRange;
12+
13+
pub use message::Severity;
14+
pub use printer::DiagnosticsPrinter;
15+
16+
use message::{DiagnosticMessage, Fix, RelatedInfo};
17+
18+
/// Collection of diagnostic messages from parsing and analysis.
19+
#[derive(Debug, Clone, Default)]
20+
pub struct Diagnostics {
21+
messages: Vec<DiagnosticMessage>,
22+
}
23+
24+
/// Builder for constructing a diagnostic message.
25+
#[must_use = "diagnostic not emitted, call .emit()"]
26+
pub struct DiagnosticBuilder<'a> {
27+
diagnostics: &'a mut Diagnostics,
28+
message: DiagnosticMessage,
29+
}
30+
31+
impl Diagnostics {
32+
pub fn new() -> Self {
33+
Self {
34+
messages: Vec::new(),
35+
}
36+
}
37+
38+
pub fn error(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
39+
DiagnosticBuilder {
40+
diagnostics: self,
41+
message: DiagnosticMessage::error(range, msg),
42+
}
43+
}
44+
45+
pub fn warning(&mut self, msg: impl Into<String>, range: TextRange) -> DiagnosticBuilder<'_> {
46+
DiagnosticBuilder {
47+
diagnostics: self,
48+
message: DiagnosticMessage::warning(range, msg),
49+
}
50+
}
51+
52+
pub fn is_empty(&self) -> bool {
53+
self.messages.is_empty()
54+
}
55+
56+
pub fn len(&self) -> usize {
57+
self.messages.len()
58+
}
59+
60+
pub fn has_errors(&self) -> bool {
61+
self.messages.iter().any(|d| d.is_error())
62+
}
63+
64+
pub fn has_warnings(&self) -> bool {
65+
self.messages.iter().any(|d| d.is_warning())
66+
}
67+
68+
pub fn error_count(&self) -> usize {
69+
self.messages.iter().filter(|d| d.is_error()).count()
70+
}
71+
72+
pub fn warning_count(&self) -> usize {
73+
self.messages.iter().filter(|d| d.is_warning()).count()
74+
}
75+
76+
pub fn printer<'a>(&'a self, source: &'a str) -> DiagnosticsPrinter<'a> {
77+
DiagnosticsPrinter::new(&self.messages, source)
78+
}
79+
80+
pub fn extend(&mut self, other: Diagnostics) {
81+
self.messages.extend(other.messages);
82+
}
83+
}
84+
85+
impl<'a> DiagnosticBuilder<'a> {
86+
pub fn related_to(mut self, msg: impl Into<String>, range: TextRange) -> Self {
87+
self.message.related.push(RelatedInfo::new(range, msg));
88+
self
89+
}
90+
91+
pub fn fix(mut self, description: impl Into<String>, replacement: impl Into<String>) -> Self {
92+
self.message.fix = Some(Fix::new(replacement, description));
93+
self
94+
}
95+
96+
pub fn emit(self) {
97+
self.diagnostics.messages.push(self.message);
98+
}
99+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Builder-pattern printer for rendering diagnostics.
2+
3+
use std::fmt::Write;
4+
5+
use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet};
6+
use rowan::TextRange;
7+
8+
use super::message::{DiagnosticMessage, Severity};
9+
10+
pub struct DiagnosticsPrinter<'a> {
11+
diagnostics: &'a [DiagnosticMessage],
12+
source: &'a str,
13+
path: Option<&'a str>,
14+
colored: bool,
15+
}
16+
17+
impl<'a> DiagnosticsPrinter<'a> {
18+
pub(crate) fn new(diagnostics: &'a [DiagnosticMessage], source: &'a str) -> Self {
19+
Self {
20+
diagnostics,
21+
source,
22+
path: None,
23+
colored: false,
24+
}
25+
}
26+
27+
pub fn path(mut self, path: &'a str) -> Self {
28+
self.path = Some(path);
29+
self
30+
}
31+
32+
pub fn colored(mut self, value: bool) -> Self {
33+
self.colored = value;
34+
self
35+
}
36+
37+
pub fn render(&self) -> String {
38+
let mut out = String::new();
39+
self.format(&mut out).expect("String write never fails");
40+
out
41+
}
42+
43+
pub fn format(&self, w: &mut impl Write) -> std::fmt::Result {
44+
let renderer = if self.colored {
45+
Renderer::styled()
46+
} else {
47+
Renderer::plain()
48+
};
49+
50+
for (i, diag) in self.diagnostics.iter().enumerate() {
51+
let range = adjust_range(diag.range, self.source.len());
52+
53+
let mut snippet = Snippet::source(self.source).line_start(1).annotation(
54+
AnnotationKind::Primary
55+
.span(range.clone())
56+
.label(&diag.message),
57+
);
58+
59+
if let Some(p) = self.path {
60+
snippet = snippet.path(p);
61+
}
62+
63+
for related in &diag.related {
64+
snippet = snippet.annotation(
65+
AnnotationKind::Context
66+
.span(adjust_range(related.range, self.source.len()))
67+
.label(&related.message),
68+
);
69+
}
70+
71+
let level = severity_to_level(diag.severity);
72+
let title_group = level.primary_title(&diag.message).element(snippet);
73+
74+
let mut report: Vec<Group> = vec![title_group];
75+
76+
if let Some(fix) = &diag.fix {
77+
report.push(
78+
Level::HELP.secondary_title(&fix.description).element(
79+
Snippet::source(self.source)
80+
.line_start(1)
81+
.patch(Patch::new(range, &fix.replacement)),
82+
),
83+
);
84+
}
85+
86+
if i > 0 {
87+
w.write_char('\n')?;
88+
}
89+
write!(w, "{}", renderer.render(&report))?;
90+
}
91+
92+
Ok(())
93+
}
94+
}
95+
96+
fn severity_to_level(severity: Severity) -> Level<'static> {
97+
match severity {
98+
Severity::Error => Level::ERROR,
99+
Severity::Warning => Level::WARNING,
100+
}
101+
}
102+
103+
fn adjust_range(range: TextRange, limit: usize) -> std::ops::Range<usize> {
104+
let start: usize = range.start().into();
105+
let end: usize = range.end().into();
106+
107+
if start == end {
108+
return start..(start + 1).min(limit);
109+
}
110+
111+
start..end
112+
}

0 commit comments

Comments
 (0)