1#![warn(missing_docs)]
30
31pub mod explain;
32pub mod json;
33pub mod lsp;
34pub mod render;
35
36use bhc_span::{FileId, SourceFile};
37pub use bhc_span::{FullSpan, Span};
38use serde::{Deserialize, Serialize};
39use std::io::Write;
40
41pub use explain::{all_error_codes, format_explanation, get_explanation, print_explanation};
43pub use json::{diagnostic_to_json, diagnostics_to_json, to_json_lines, to_json_string};
44pub use json::{JsonApplicability, JsonDiagnostic, JsonSeverity, JsonSpan, JsonSuggestion};
45pub use lsp::{
46 publish_diagnostics, to_code_actions, to_hover, to_lsp_diagnostic, to_lsp_diagnostics,
47 LspCodeAction, LspDiagnostic, LspHover, LspRange, LspSeverity, LspTextEdit,
48 PublishDiagnosticsParams,
49};
50pub use render::{colors, CargoRenderer, RenderConfig};
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub enum Severity {
55 Bug,
57 Error,
59 Warning,
61 Note,
63 Help,
65}
66
67impl Severity {
68 #[must_use]
70 pub fn color(self) -> &'static str {
71 match self {
72 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", }
78 }
79
80 #[must_use]
82 pub fn label(self) -> &'static str {
83 match self {
84 Self::Bug => "internal compiler error",
85 Self::Error => "error",
86 Self::Warning => "warning",
87 Self::Note => "note",
88 Self::Help => "help",
89 }
90 }
91}
92
93#[derive(Clone, Debug)]
95pub struct Label {
96 pub span: FullSpan,
98 pub message: String,
100 pub primary: bool,
102}
103
104impl Label {
105 #[must_use]
107 pub fn primary(span: FullSpan, message: impl Into<String>) -> Self {
108 Self {
109 span,
110 message: message.into(),
111 primary: true,
112 }
113 }
114
115 #[must_use]
117 pub fn secondary(span: FullSpan, message: impl Into<String>) -> Self {
118 Self {
119 span,
120 message: message.into(),
121 primary: false,
122 }
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct Diagnostic {
129 pub severity: Severity,
131 pub message: String,
133 pub code: Option<String>,
135 pub labels: Vec<Label>,
137 pub notes: Vec<String>,
139 pub suggestions: Vec<Suggestion>,
141}
142
143impl Diagnostic {
144 #[must_use]
146 pub fn error(message: impl Into<String>) -> Self {
147 Self {
148 severity: Severity::Error,
149 message: message.into(),
150 code: None,
151 labels: Vec::new(),
152 notes: Vec::new(),
153 suggestions: Vec::new(),
154 }
155 }
156
157 #[must_use]
159 pub fn warning(message: impl Into<String>) -> Self {
160 Self {
161 severity: Severity::Warning,
162 message: message.into(),
163 code: None,
164 labels: Vec::new(),
165 notes: Vec::new(),
166 suggestions: Vec::new(),
167 }
168 }
169
170 #[must_use]
172 pub fn bug(message: impl Into<String>) -> Self {
173 Self {
174 severity: Severity::Bug,
175 message: message.into(),
176 code: None,
177 labels: Vec::new(),
178 notes: Vec::new(),
179 suggestions: Vec::new(),
180 }
181 }
182
183 #[must_use]
185 pub fn with_code(mut self, code: impl Into<String>) -> Self {
186 self.code = Some(code.into());
187 self
188 }
189
190 #[must_use]
192 pub fn with_label(mut self, span: FullSpan, message: impl Into<String>) -> Self {
193 self.labels.push(Label::primary(span, message));
194 self
195 }
196
197 #[must_use]
199 pub fn with_secondary_label(mut self, span: FullSpan, message: impl Into<String>) -> Self {
200 self.labels.push(Label::secondary(span, message));
201 self
202 }
203
204 #[must_use]
206 pub fn with_note(mut self, note: impl Into<String>) -> Self {
207 self.notes.push(note.into());
208 self
209 }
210
211 #[must_use]
213 pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self {
214 self.suggestions.push(suggestion);
215 self
216 }
217
218 #[must_use]
220 pub fn is_error(&self) -> bool {
221 matches!(self.severity, Severity::Error | Severity::Bug)
222 }
223}
224
225#[derive(Clone, Debug)]
227pub struct Suggestion {
228 pub message: String,
230 pub span: FullSpan,
232 pub replacement: String,
234 pub applicability: Applicability,
236}
237
238impl Suggestion {
239 #[must_use]
241 pub fn new(
242 message: impl Into<String>,
243 span: FullSpan,
244 replacement: impl Into<String>,
245 applicability: Applicability,
246 ) -> Self {
247 Self {
248 message: message.into(),
249 span,
250 replacement: replacement.into(),
251 applicability,
252 }
253 }
254}
255
256#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
258pub enum Applicability {
259 MachineApplicable,
261 MaybeIncorrect,
263 HasPlaceholders,
265 Unspecified,
267}
268
269#[derive(Debug, Default)]
271pub struct DiagnosticHandler {
272 diagnostics: Vec<Diagnostic>,
273 error_count: usize,
274 warning_count: usize,
275}
276
277impl DiagnosticHandler {
278 #[must_use]
280 pub fn new() -> Self {
281 Self::default()
282 }
283
284 pub fn emit(&mut self, diagnostic: Diagnostic) {
286 match diagnostic.severity {
287 Severity::Error | Severity::Bug => self.error_count += 1,
288 Severity::Warning => self.warning_count += 1,
289 _ => {}
290 }
291 self.diagnostics.push(diagnostic);
292 }
293
294 #[must_use]
296 pub fn has_errors(&self) -> bool {
297 self.error_count > 0
298 }
299
300 #[must_use]
302 pub fn error_count(&self) -> usize {
303 self.error_count
304 }
305
306 #[must_use]
308 pub fn warning_count(&self) -> usize {
309 self.warning_count
310 }
311
312 #[must_use]
314 pub fn diagnostics(&self) -> &[Diagnostic] {
315 &self.diagnostics
316 }
317
318 pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
320 self.error_count = 0;
321 self.warning_count = 0;
322 std::mem::take(&mut self.diagnostics)
323 }
324}
325
326#[derive(Debug, Default)]
328pub struct SourceMap {
329 files: Vec<SourceFile>,
330}
331
332impl SourceMap {
333 #[must_use]
335 pub fn new() -> Self {
336 Self::default()
337 }
338
339 pub fn add_file(&mut self, name: String, src: String) -> FileId {
341 let id = FileId::new(self.files.len() as u32);
342 self.files.push(SourceFile::new(id, name, src));
343 id
344 }
345
346 #[must_use]
348 pub fn get_file(&self, id: FileId) -> Option<&SourceFile> {
349 self.files.get(id.0 as usize)
350 }
351
352 #[must_use]
354 pub fn len(&self) -> usize {
355 self.files.len()
356 }
357
358 #[must_use]
360 pub fn is_empty(&self) -> bool {
361 self.files.is_empty()
362 }
363}
364
365pub struct DiagnosticRenderer<'a> {
367 source_map: &'a SourceMap,
368 use_colors: bool,
369}
370
371impl<'a> DiagnosticRenderer<'a> {
372 #[must_use]
374 pub fn new(source_map: &'a SourceMap) -> Self {
375 Self {
376 source_map,
377 use_colors: true,
378 }
379 }
380
381 #[must_use]
383 pub fn without_colors(mut self) -> Self {
384 self.use_colors = false;
385 self
386 }
387
388 pub fn render(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
390 let reset = if self.use_colors { "\x1b[0m" } else { "" };
391 let color = if self.use_colors {
392 diagnostic.severity.color()
393 } else {
394 ""
395 };
396
397 write!(w, "{}{}", color, diagnostic.severity.label())?;
399 if let Some(code) = &diagnostic.code {
400 write!(w, "[{code}]")?;
401 }
402 writeln!(w, "{reset}: {}", diagnostic.message)?;
403
404 for label in &diagnostic.labels {
406 if let Some(file) = self.source_map.get_file(label.span.file) {
407 let loc = file.lookup_line_col(label.span.span.lo);
408 let arrow = if label.primary { "-->" } else { " " };
409 writeln!(w, " {arrow} {}:{}:{}", file.name, loc.line, loc.col)?;
410
411 if !label.span.span.is_dummy() {
413 let source = file.source_text(label.span.span);
414 writeln!(w, " |")?;
415 writeln!(w, " | {source}")?;
416 writeln!(w, " | {}", "^".repeat(source.len().max(1)))?;
417 if !label.message.is_empty() {
418 writeln!(w, " | {}", label.message)?;
419 }
420 }
421 }
422 }
423
424 for note in &diagnostic.notes {
426 writeln!(w, " = note: {note}")?;
427 }
428
429 for suggestion in &diagnostic.suggestions {
431 writeln!(w, " = help: {}", suggestion.message)?;
432 if !suggestion.replacement.is_empty() {
433 writeln!(w, " |")?;
434 writeln!(w, " | {}", suggestion.replacement)?;
435 }
436 }
437
438 writeln!(w)?;
439 Ok(())
440 }
441
442 pub fn render_all(&self, diagnostics: &[Diagnostic]) {
444 let mut stderr = std::io::stderr().lock();
445 for diag in diagnostics {
446 let _ = self.render(diag, &mut stderr);
447 }
448 }
449}
450
451pub trait IntoDiagnostic {
453 fn into_diagnostic(self) -> Diagnostic;
455}
456
457impl IntoDiagnostic for Diagnostic {
458 fn into_diagnostic(self) -> Diagnostic {
459 self
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_diagnostic_builder() {
469 let span = FullSpan::new(FileId::new(0), Span::from_raw(10, 20));
470
471 let diag = Diagnostic::error("type mismatch")
472 .with_code("E0001")
473 .with_label(span, "expected `Int`, found `String`")
474 .with_note("consider using `show` to convert to String");
475
476 assert!(diag.is_error());
477 assert_eq!(diag.code, Some("E0001".to_string()));
478 assert_eq!(diag.labels.len(), 1);
479 assert_eq!(diag.notes.len(), 1);
480 }
481
482 #[test]
483 fn test_diagnostic_handler() {
484 let mut handler = DiagnosticHandler::new();
485
486 handler.emit(Diagnostic::error("error 1"));
487 handler.emit(Diagnostic::warning("warning 1"));
488 handler.emit(Diagnostic::error("error 2"));
489
490 assert!(handler.has_errors());
491 assert_eq!(handler.error_count(), 2);
492 assert_eq!(handler.warning_count(), 1);
493 }
494}