1use 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
58pub 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 ¬es,
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}