harn_parser/
diagnostic.rs1use harn_lexer::Span;
2
3pub fn render_diagnostic(
14 source: &str,
15 filename: &str,
16 span: &Span,
17 severity: &str,
18 message: &str,
19 label: Option<&str>,
20 help: Option<&str>,
21) -> String {
22 let mut out = String::new();
23
24 out.push_str(severity);
26 out.push_str(": ");
27 out.push_str(message);
28 out.push('\n');
29
30 let line_num = span.line;
32 let col_num = span.column;
33
34 let gutter_width = line_num.to_string().len();
35
36 out.push_str(&format!(
37 "{:>width$}--> {filename}:{line_num}:{col_num}\n",
38 " ",
39 width = gutter_width + 1,
40 ));
41
42 out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
44
45 let source_line_opt = source.lines().nth(line_num.wrapping_sub(1));
47 if let Some(source_line) = source_line_opt.filter(|_| line_num > 0) {
48 out.push_str(&format!(
49 "{:>width$} | {source_line}\n",
50 line_num,
51 width = gutter_width + 1,
52 ));
53
54 if let Some(label_text) = label {
56 let span_len = if span.end > span.start && span.start <= source.len() {
58 let span_text = &source[span.start.min(source.len())..span.end.min(source.len())];
59 span_text.chars().count().max(1)
60 } else {
61 1
62 };
63 let col_num = col_num.max(1); let padding = " ".repeat(col_num - 1);
65 let carets = "^".repeat(span_len);
66 out.push_str(&format!(
67 "{:>width$} | {padding}{carets} {label_text}\n",
68 " ",
69 width = gutter_width + 1,
70 ));
71 }
72 }
73
74 if let Some(help_text) = help {
76 out.push_str(&format!(
77 "{:>width$} = help: {help_text}\n",
78 " ",
79 width = gutter_width + 1,
80 ));
81 }
82
83 out
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_basic_diagnostic() {
92 let source = "pipeline default(task) {\n let y = x + 1\n}";
93 let span = Span {
94 start: 28,
95 end: 29,
96 line: 2,
97 column: 13,
98 };
99 let output = render_diagnostic(
100 source,
101 "example.harn",
102 &span,
103 "error",
104 "undefined variable `x`",
105 Some("not found in this scope"),
106 None,
107 );
108 assert!(output.contains("error: undefined variable `x`"));
109 assert!(output.contains("--> example.harn:2:13"));
110 assert!(output.contains("let y = x + 1"));
111 assert!(output.contains("^ not found in this scope"));
112 }
113
114 #[test]
115 fn test_diagnostic_with_help() {
116 let source = "let y = xx + 1";
117 let span = Span {
118 start: 8,
119 end: 10,
120 line: 1,
121 column: 9,
122 };
123 let output = render_diagnostic(
124 source,
125 "test.harn",
126 &span,
127 "error",
128 "undefined variable `xx`",
129 Some("not found in this scope"),
130 Some("did you mean `x`?"),
131 );
132 assert!(output.contains("help: did you mean `x`?"));
133 }
134
135 #[test]
136 fn test_multiline_source() {
137 let source = "line1\nline2\nline3";
138 let span = Span::with_offsets(6, 11, 2, 1); let result = render_diagnostic(
140 source,
141 "test.harn",
142 &span,
143 "error",
144 "bad line",
145 Some("here"),
146 None,
147 );
148 assert!(result.contains("line2"));
149 assert!(result.contains("^^^^^"));
150 }
151
152 #[test]
153 fn test_single_char_span() {
154 let source = "let x = 42";
155 let span = Span::with_offsets(4, 5, 1, 5); let result = render_diagnostic(
157 source,
158 "test.harn",
159 &span,
160 "warning",
161 "unused",
162 Some("never used"),
163 None,
164 );
165 assert!(result.contains("^"));
166 assert!(result.contains("never used"));
167 }
168
169 #[test]
170 fn test_with_help() {
171 let source = "let y = reponse";
172 let span = Span::with_offsets(8, 15, 1, 9);
173 let result = render_diagnostic(
174 source,
175 "test.harn",
176 &span,
177 "error",
178 "undefined",
179 None,
180 Some("did you mean `response`?"),
181 );
182 assert!(result.contains("help:"));
183 assert!(result.contains("response"));
184 }
185}