Skip to main content

crisp_diagnostics/
format.rs

1//! rustc-style diagnostic formatting (spec §17.4).
2
3use crate::{Diagnostic, Severity};
4use crisp_ast::Span;
5
6#[derive(Debug, Clone)]
7pub struct FormattedDiagnostic {
8    pub code: String,
9    pub message: String,
10    pub severity: Severity,
11    pub span: Span,
12    pub notes: Vec<String>,
13    pub rendered: String,
14}
15
16pub fn format_diagnostic(
17    source: &str,
18    code: &str,
19    message: &str,
20    span: Span,
21    severity: Severity,
22    notes: &[String],
23) -> FormattedDiagnostic {
24    format_diagnostic_at("source", source, code, message, span, severity, notes)
25}
26
27pub fn format_diagnostic_at(
28    file: &str,
29    source: &str,
30    code: &str,
31    message: &str,
32    span: Span,
33    severity: Severity,
34    notes: &[String],
35) -> FormattedDiagnostic {
36    let rendered = render(file, source, code, message, span, severity, notes);
37    FormattedDiagnostic {
38        code: code.to_string(),
39        message: message.to_string(),
40        severity,
41        span,
42        notes: notes.to_vec(),
43        rendered,
44    }
45}
46
47pub fn from_diagnostic(source: &str, diag: &Diagnostic) -> FormattedDiagnostic {
48    format_diagnostic(
49        source,
50        &diag.code,
51        &diag.message,
52        diag.span,
53        diag.severity,
54        &diag.notes,
55    )
56}
57
58/// Format an unresolved-name diagnostic with optional import / module hint.
59pub fn format_unresolved_name(
60    file: &str,
61    source: &str,
62    name: &str,
63    span: Span,
64    hint: Option<&str>,
65) -> FormattedDiagnostic {
66    let mut notes = Vec::new();
67    if let Some(h) = hint {
68        notes.push(format!("help: {h}"));
69    } else {
70        notes.push(
71            "help: check spelling, `use` imports, and that the defining module is imported".into(),
72        );
73    }
74    format_diagnostic_at(
75        file,
76        source,
77        "E0035",
78        &format!("unresolved name `{name}`"),
79        span,
80        Severity::Error,
81        &notes,
82    )
83}
84
85fn line_col(source: &str, offset: u32) -> (usize, usize) {
86    let mut line = 1usize;
87    let mut col = 1usize;
88    for (i, ch) in source.char_indices() {
89        if i as u32 >= offset {
90            break;
91        }
92        if ch == '\n' {
93            line += 1;
94            col = 1;
95        } else {
96            col += 1;
97        }
98    }
99    (line, col)
100}
101
102fn render(
103    file: &str,
104    source: &str,
105    code: &str,
106    message: &str,
107    span: Span,
108    severity: Severity,
109    notes: &[String],
110) -> String {
111    let (line, col) = line_col(source, span.start);
112    let sev = match severity {
113        Severity::Error => "ERROR",
114        Severity::Warning => "WARNING",
115        Severity::Note => "NOTE",
116    };
117    let mut out = format!("{sev} [{code}]: {message}\n  --> {file}:{line}:{col}\n");
118    let lines: Vec<&str> = source.lines().collect();
119    if line > 0 && line <= lines.len() {
120        let src_line = lines[line - 1];
121        out.push_str(&format!("   |\n{line:>3} | {src_line}\n   | "));
122        let pad = col.saturating_sub(1);
123        for _ in 0..pad {
124            out.push(' ');
125        }
126        let highlight_len = span.len().max(1) as usize;
127        for _ in 0..highlight_len.min(src_line.len().saturating_sub(pad)) {
128            out.push('^');
129        }
130        out.push('\n');
131    }
132    for note in notes {
133        if let Some(rest) = note.strip_prefix("help:") {
134            out.push_str(&format!("   = help:{rest}\n"));
135        } else if let Some(rest) = note.strip_prefix("note:") {
136            out.push_str(&format!("   = note:{rest}\n"));
137        } else {
138            out.push_str(&format!("   = note: {note}\n"));
139        }
140    }
141    out
142}
143
144pub fn format_ownership_contradiction(
145    source: &str,
146    name: &str,
147    inferred: &str,
148    annotated: &str,
149    span: Span,
150) -> FormattedDiagnostic {
151    format_diagnostic(
152        source,
153        "E0050",
154        &format!(
155            "ownership contradicts annotation on `{name}`: inferred `{inferred}`, annotated `{annotated}`"
156        ),
157        span,
158        Severity::Error,
159        &[format!(
160            "either drop the `{annotated}` annotation or clone before the move"
161        )],
162    )
163}
164
165pub fn format_type_mismatch(
166    source: &str,
167    expected: &str,
168    found: &str,
169    span: Span,
170) -> FormattedDiagnostic {
171    format_diagnostic(
172        source,
173        "E0041",
174        &format!("type mismatch: expected `{expected}`, found `{found}`"),
175        span,
176        Severity::Error,
177        &[],
178    )
179}