1use harn_lexer::Span;
2
3use crate::ParserError;
4
5pub fn edit_distance(a: &str, b: &str) -> usize {
7 let a_chars: Vec<char> = a.chars().collect();
8 let b_chars: Vec<char> = b.chars().collect();
9 let n = b_chars.len();
10 let mut prev = (0..=n).collect::<Vec<_>>();
11 let mut curr = vec![0; n + 1];
12 for (i, ac) in a_chars.iter().enumerate() {
13 curr[0] = i + 1;
14 for (j, bc) in b_chars.iter().enumerate() {
15 let cost = if ac == bc { 0 } else { 1 };
16 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
17 }
18 std::mem::swap(&mut prev, &mut curr);
19 }
20 prev[n]
21}
22
23pub fn find_closest_match<'a>(
25 name: &str,
26 candidates: impl Iterator<Item = &'a str>,
27 max_dist: usize,
28) -> Option<&'a str> {
29 candidates
30 .filter(|c| c.len().abs_diff(name.len()) <= max_dist)
31 .min_by_key(|c| edit_distance(name, c))
32 .filter(|c| edit_distance(name, c) <= max_dist && *c != name)
33}
34
35pub fn render_diagnostic(
46 source: &str,
47 filename: &str,
48 span: &Span,
49 severity: &str,
50 message: &str,
51 label: Option<&str>,
52 help: Option<&str>,
53) -> String {
54 let mut out = String::new();
55
56 out.push_str(severity);
58 out.push_str(": ");
59 out.push_str(message);
60 out.push('\n');
61
62 let line_num = span.line;
64 let col_num = span.column;
65
66 let gutter_width = line_num.to_string().len();
67
68 out.push_str(&format!(
69 "{:>width$}--> {filename}:{line_num}:{col_num}\n",
70 " ",
71 width = gutter_width + 1,
72 ));
73
74 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
76
77 let source_line_opt = source.lines().nth(line_num.wrapping_sub(1));
79 if let Some(source_line) = source_line_opt.filter(|_| line_num > 0) {
80 out.push_str(&format!(
81 "{:>width$} | {source_line}\n",
82 line_num,
83 width = gutter_width + 1,
84 ));
85
86 if let Some(label_text) = label {
88 let span_len = if span.end > span.start && span.start <= source.len() {
90 let span_text = &source[span.start.min(source.len())..span.end.min(source.len())];
91 span_text.chars().count().max(1)
92 } else {
93 1
94 };
95 let col_num = col_num.max(1); let padding = " ".repeat(col_num - 1);
97 let carets = "^".repeat(span_len);
98 out.push_str(&format!(
99 "{:>width$} | {padding}{carets} {label_text}\n",
100 " ",
101 width = gutter_width + 1,
102 ));
103 }
104 }
105
106 if let Some(help_text) = help {
108 out.push_str(&format!(
109 "{:>width$} = help: {help_text}\n",
110 " ",
111 width = gutter_width + 1,
112 ));
113 }
114
115 out
116}
117
118pub fn parser_error_message(err: &ParserError) -> String {
119 match err {
120 ParserError::Unexpected { got, expected, .. } => {
121 format!("expected {expected}, found {got}")
122 }
123 ParserError::UnexpectedEof { expected, .. } => {
124 format!("unexpected end of file, expected {expected}")
125 }
126 }
127}
128
129pub fn parser_error_label(err: &ParserError) -> &'static str {
130 match err {
131 ParserError::Unexpected { got, .. } if got == "Newline" => "line break not allowed here",
132 ParserError::Unexpected { .. } => "unexpected token",
133 ParserError::UnexpectedEof { .. } => "file ends here",
134 }
135}
136
137pub fn parser_error_help(err: &ParserError) -> Option<&'static str> {
138 match err {
139 ParserError::UnexpectedEof { expected, .. } | ParserError::Unexpected { expected, .. } => {
140 match expected.as_str() {
141 "}" => Some("add a closing `}` to finish this block"),
142 ")" => Some("add a closing `)` to finish this expression or parameter list"),
143 "]" => Some("add a closing `]` to finish this list or subscript"),
144 "fn, struct, enum, or pipeline after pub" => {
145 Some("use `pub fn`, `pub pipeline`, `pub enum`, or `pub struct`")
146 }
147 _ => None,
148 }
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_basic_diagnostic() {
159 let source = "pipeline default(task) {\n let y = x + 1\n}";
160 let span = Span {
161 start: 28,
162 end: 29,
163 line: 2,
164 column: 13,
165 end_line: 2,
166 };
167 let output = render_diagnostic(
168 source,
169 "example.harn",
170 &span,
171 "error",
172 "undefined variable `x`",
173 Some("not found in this scope"),
174 None,
175 );
176 assert!(output.contains("error: undefined variable `x`"));
177 assert!(output.contains("--> example.harn:2:13"));
178 assert!(output.contains("let y = x + 1"));
179 assert!(output.contains("^ not found in this scope"));
180 }
181
182 #[test]
183 fn test_diagnostic_with_help() {
184 let source = "let y = xx + 1";
185 let span = Span {
186 start: 8,
187 end: 10,
188 line: 1,
189 column: 9,
190 end_line: 1,
191 };
192 let output = render_diagnostic(
193 source,
194 "test.harn",
195 &span,
196 "error",
197 "undefined variable `xx`",
198 Some("not found in this scope"),
199 Some("did you mean `x`?"),
200 );
201 assert!(output.contains("help: did you mean `x`?"));
202 }
203
204 #[test]
205 fn test_multiline_source() {
206 let source = "line1\nline2\nline3";
207 let span = Span::with_offsets(6, 11, 2, 1); let result = render_diagnostic(
209 source,
210 "test.harn",
211 &span,
212 "error",
213 "bad line",
214 Some("here"),
215 None,
216 );
217 assert!(result.contains("line2"));
218 assert!(result.contains("^^^^^"));
219 }
220
221 #[test]
222 fn test_single_char_span() {
223 let source = "let x = 42";
224 let span = Span::with_offsets(4, 5, 1, 5); let result = render_diagnostic(
226 source,
227 "test.harn",
228 &span,
229 "warning",
230 "unused",
231 Some("never used"),
232 None,
233 );
234 assert!(result.contains("^"));
235 assert!(result.contains("never used"));
236 }
237
238 #[test]
239 fn test_with_help() {
240 let source = "let y = reponse";
241 let span = Span::with_offsets(8, 15, 1, 9);
242 let result = render_diagnostic(
243 source,
244 "test.harn",
245 &span,
246 "error",
247 "undefined",
248 None,
249 Some("did you mean `response`?"),
250 );
251 assert!(result.contains("help:"));
252 assert!(result.contains("response"));
253 }
254
255 #[test]
256 fn test_parser_error_helpers_for_eof() {
257 let err = ParserError::UnexpectedEof {
258 expected: "}".into(),
259 span: Span::with_offsets(10, 10, 3, 1),
260 };
261 assert_eq!(
262 parser_error_message(&err),
263 "unexpected end of file, expected }"
264 );
265 assert_eq!(parser_error_label(&err), "file ends here");
266 assert_eq!(
267 parser_error_help(&err),
268 Some("add a closing `}` to finish this block")
269 );
270 }
271}