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 end_line: 2,
99 };
100 let output = render_diagnostic(
101 source,
102 "example.harn",
103 &span,
104 "error",
105 "undefined variable `x`",
106 Some("not found in this scope"),
107 None,
108 );
109 assert!(output.contains("error: undefined variable `x`"));
110 assert!(output.contains("--> example.harn:2:13"));
111 assert!(output.contains("let y = x + 1"));
112 assert!(output.contains("^ not found in this scope"));
113 }
114
115 #[test]
116 fn test_diagnostic_with_help() {
117 let source = "let y = xx + 1";
118 let span = Span {
119 start: 8,
120 end: 10,
121 line: 1,
122 column: 9,
123 end_line: 1,
124 };
125 let output = render_diagnostic(
126 source,
127 "test.harn",
128 &span,
129 "error",
130 "undefined variable `xx`",
131 Some("not found in this scope"),
132 Some("did you mean `x`?"),
133 );
134 assert!(output.contains("help: did you mean `x`?"));
135 }
136
137 #[test]
138 fn test_multiline_source() {
139 let source = "line1\nline2\nline3";
140 let span = Span::with_offsets(6, 11, 2, 1); let result = render_diagnostic(
142 source,
143 "test.harn",
144 &span,
145 "error",
146 "bad line",
147 Some("here"),
148 None,
149 );
150 assert!(result.contains("line2"));
151 assert!(result.contains("^^^^^"));
152 }
153
154 #[test]
155 fn test_single_char_span() {
156 let source = "let x = 42";
157 let span = Span::with_offsets(4, 5, 1, 5); let result = render_diagnostic(
159 source,
160 "test.harn",
161 &span,
162 "warning",
163 "unused",
164 Some("never used"),
165 None,
166 );
167 assert!(result.contains("^"));
168 assert!(result.contains("never used"));
169 }
170
171 #[test]
172 fn test_with_help() {
173 let source = "let y = reponse";
174 let span = Span::with_offsets(8, 15, 1, 9);
175 let result = render_diagnostic(
176 source,
177 "test.harn",
178 &span,
179 "error",
180 "undefined",
181 None,
182 Some("did you mean `response`?"),
183 );
184 assert!(result.contains("help:"));
185 assert!(result.contains("response"));
186 }
187}