harn_parser/
diagnostic.rs1use harn_lexer::Span;
2
3pub fn edit_distance(a: &str, b: &str) -> usize {
5 let a_chars: Vec<char> = a.chars().collect();
6 let b_chars: Vec<char> = b.chars().collect();
7 let n = b_chars.len();
8 let mut prev = (0..=n).collect::<Vec<_>>();
9 let mut curr = vec![0; n + 1];
10 for (i, ac) in a_chars.iter().enumerate() {
11 curr[0] = i + 1;
12 for (j, bc) in b_chars.iter().enumerate() {
13 let cost = if ac == bc { 0 } else { 1 };
14 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
15 }
16 std::mem::swap(&mut prev, &mut curr);
17 }
18 prev[n]
19}
20
21pub fn find_closest_match<'a>(
23 name: &str,
24 candidates: impl Iterator<Item = &'a str>,
25 max_dist: usize,
26) -> Option<&'a str> {
27 candidates
28 .filter(|c| c.len().abs_diff(name.len()) <= max_dist)
29 .min_by_key(|c| edit_distance(name, c))
30 .filter(|c| edit_distance(name, c) <= max_dist && *c != name)
31}
32
33pub fn render_diagnostic(
44 source: &str,
45 filename: &str,
46 span: &Span,
47 severity: &str,
48 message: &str,
49 label: Option<&str>,
50 help: Option<&str>,
51) -> String {
52 let mut out = String::new();
53
54 out.push_str(severity);
56 out.push_str(": ");
57 out.push_str(message);
58 out.push('\n');
59
60 let line_num = span.line;
62 let col_num = span.column;
63
64 let gutter_width = line_num.to_string().len();
65
66 out.push_str(&format!(
67 "{:>width$}--> {filename}:{line_num}:{col_num}\n",
68 " ",
69 width = gutter_width + 1,
70 ));
71
72 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
74
75 let source_line_opt = source.lines().nth(line_num.wrapping_sub(1));
77 if let Some(source_line) = source_line_opt.filter(|_| line_num > 0) {
78 out.push_str(&format!(
79 "{:>width$} | {source_line}\n",
80 line_num,
81 width = gutter_width + 1,
82 ));
83
84 if let Some(label_text) = label {
86 let span_len = if span.end > span.start && span.start <= source.len() {
88 let span_text = &source[span.start.min(source.len())..span.end.min(source.len())];
89 span_text.chars().count().max(1)
90 } else {
91 1
92 };
93 let col_num = col_num.max(1); let padding = " ".repeat(col_num - 1);
95 let carets = "^".repeat(span_len);
96 out.push_str(&format!(
97 "{:>width$} | {padding}{carets} {label_text}\n",
98 " ",
99 width = gutter_width + 1,
100 ));
101 }
102 }
103
104 if let Some(help_text) = help {
106 out.push_str(&format!(
107 "{:>width$} = help: {help_text}\n",
108 " ",
109 width = gutter_width + 1,
110 ));
111 }
112
113 out
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_basic_diagnostic() {
122 let source = "pipeline default(task) {\n let y = x + 1\n}";
123 let span = Span {
124 start: 28,
125 end: 29,
126 line: 2,
127 column: 13,
128 end_line: 2,
129 };
130 let output = render_diagnostic(
131 source,
132 "example.harn",
133 &span,
134 "error",
135 "undefined variable `x`",
136 Some("not found in this scope"),
137 None,
138 );
139 assert!(output.contains("error: undefined variable `x`"));
140 assert!(output.contains("--> example.harn:2:13"));
141 assert!(output.contains("let y = x + 1"));
142 assert!(output.contains("^ not found in this scope"));
143 }
144
145 #[test]
146 fn test_diagnostic_with_help() {
147 let source = "let y = xx + 1";
148 let span = Span {
149 start: 8,
150 end: 10,
151 line: 1,
152 column: 9,
153 end_line: 1,
154 };
155 let output = render_diagnostic(
156 source,
157 "test.harn",
158 &span,
159 "error",
160 "undefined variable `xx`",
161 Some("not found in this scope"),
162 Some("did you mean `x`?"),
163 );
164 assert!(output.contains("help: did you mean `x`?"));
165 }
166
167 #[test]
168 fn test_multiline_source() {
169 let source = "line1\nline2\nline3";
170 let span = Span::with_offsets(6, 11, 2, 1); let result = render_diagnostic(
172 source,
173 "test.harn",
174 &span,
175 "error",
176 "bad line",
177 Some("here"),
178 None,
179 );
180 assert!(result.contains("line2"));
181 assert!(result.contains("^^^^^"));
182 }
183
184 #[test]
185 fn test_single_char_span() {
186 let source = "let x = 42";
187 let span = Span::with_offsets(4, 5, 1, 5); let result = render_diagnostic(
189 source,
190 "test.harn",
191 &span,
192 "warning",
193 "unused",
194 Some("never used"),
195 None,
196 );
197 assert!(result.contains("^"));
198 assert!(result.contains("never used"));
199 }
200
201 #[test]
202 fn test_with_help() {
203 let source = "let y = reponse";
204 let span = Span::with_offsets(8, 15, 1, 9);
205 let result = render_diagnostic(
206 source,
207 "test.harn",
208 &span,
209 "error",
210 "undefined",
211 None,
212 Some("did you mean `response`?"),
213 );
214 assert!(result.contains("help:"));
215 assert!(result.contains("response"));
216 }
217}