use harn_lexer::Span;
use crate::ParserError;
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let n = b_chars.len();
let mut prev = (0..=n).collect::<Vec<_>>();
let mut curr = vec![0; n + 1];
for (i, ac) in a_chars.iter().enumerate() {
curr[0] = i + 1;
for (j, bc) in b_chars.iter().enumerate() {
let cost = if ac == bc { 0 } else { 1 };
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
pub fn find_closest_match<'a>(
name: &str,
candidates: impl Iterator<Item = &'a str>,
max_dist: usize,
) -> Option<&'a str> {
candidates
.filter(|c| c.len().abs_diff(name.len()) <= max_dist)
.min_by_key(|c| edit_distance(name, c))
.filter(|c| edit_distance(name, c) <= max_dist && *c != name)
}
pub fn render_diagnostic(
source: &str,
filename: &str,
span: &Span,
severity: &str,
message: &str,
label: Option<&str>,
help: Option<&str>,
) -> String {
let mut out = String::new();
out.push_str(severity);
out.push_str(": ");
out.push_str(message);
out.push('\n');
let line_num = span.line;
let col_num = span.column;
let gutter_width = line_num.to_string().len();
out.push_str(&format!(
"{:>width$}--> {filename}:{line_num}:{col_num}\n",
" ",
width = gutter_width + 1,
));
out.push_str(&format!("{:>width$} |\n", " ", width = gutter_width + 1));
let source_line_opt = source.lines().nth(line_num.wrapping_sub(1));
if let Some(source_line) = source_line_opt.filter(|_| line_num > 0) {
out.push_str(&format!(
"{:>width$} | {source_line}\n",
line_num,
width = gutter_width + 1,
));
if let Some(label_text) = label {
let span_len = if span.end > span.start && span.start <= source.len() {
let span_text = &source[span.start.min(source.len())..span.end.min(source.len())];
span_text.chars().count().max(1)
} else {
1
};
let col_num = col_num.max(1); let padding = " ".repeat(col_num - 1);
let carets = "^".repeat(span_len);
out.push_str(&format!(
"{:>width$} | {padding}{carets} {label_text}\n",
" ",
width = gutter_width + 1,
));
}
}
if let Some(help_text) = help {
out.push_str(&format!(
"{:>width$} = help: {help_text}\n",
" ",
width = gutter_width + 1,
));
}
out
}
pub fn parser_error_message(err: &ParserError) -> String {
match err {
ParserError::Unexpected { got, expected, .. } => {
format!("expected {expected}, found {got}")
}
ParserError::UnexpectedEof { expected, .. } => {
format!("unexpected end of file, expected {expected}")
}
}
}
pub fn parser_error_label(err: &ParserError) -> &'static str {
match err {
ParserError::Unexpected { got, .. } if got == "Newline" => "line break not allowed here",
ParserError::Unexpected { .. } => "unexpected token",
ParserError::UnexpectedEof { .. } => "file ends here",
}
}
pub fn parser_error_help(err: &ParserError) -> Option<&'static str> {
match err {
ParserError::UnexpectedEof { expected, .. } | ParserError::Unexpected { expected, .. } => {
match expected.as_str() {
"}" => Some("add a closing `}` to finish this block"),
")" => Some("add a closing `)` to finish this expression or parameter list"),
"]" => Some("add a closing `]` to finish this list or subscript"),
"fn, struct, enum, or pipeline after pub" => {
Some("use `pub fn`, `pub pipeline`, `pub enum`, or `pub struct`")
}
_ => None,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_diagnostic() {
let source = "pipeline default(task) {\n let y = x + 1\n}";
let span = Span {
start: 28,
end: 29,
line: 2,
column: 13,
end_line: 2,
};
let output = render_diagnostic(
source,
"example.harn",
&span,
"error",
"undefined variable `x`",
Some("not found in this scope"),
None,
);
assert!(output.contains("error: undefined variable `x`"));
assert!(output.contains("--> example.harn:2:13"));
assert!(output.contains("let y = x + 1"));
assert!(output.contains("^ not found in this scope"));
}
#[test]
fn test_diagnostic_with_help() {
let source = "let y = xx + 1";
let span = Span {
start: 8,
end: 10,
line: 1,
column: 9,
end_line: 1,
};
let output = render_diagnostic(
source,
"test.harn",
&span,
"error",
"undefined variable `xx`",
Some("not found in this scope"),
Some("did you mean `x`?"),
);
assert!(output.contains("help: did you mean `x`?"));
}
#[test]
fn test_multiline_source() {
let source = "line1\nline2\nline3";
let span = Span::with_offsets(6, 11, 2, 1); let result = render_diagnostic(
source,
"test.harn",
&span,
"error",
"bad line",
Some("here"),
None,
);
assert!(result.contains("line2"));
assert!(result.contains("^^^^^"));
}
#[test]
fn test_single_char_span() {
let source = "let x = 42";
let span = Span::with_offsets(4, 5, 1, 5); let result = render_diagnostic(
source,
"test.harn",
&span,
"warning",
"unused",
Some("never used"),
None,
);
assert!(result.contains("^"));
assert!(result.contains("never used"));
}
#[test]
fn test_with_help() {
let source = "let y = reponse";
let span = Span::with_offsets(8, 15, 1, 9);
let result = render_diagnostic(
source,
"test.harn",
&span,
"error",
"undefined",
None,
Some("did you mean `response`?"),
);
assert!(result.contains("help:"));
assert!(result.contains("response"));
}
#[test]
fn test_parser_error_helpers_for_eof() {
let err = ParserError::UnexpectedEof {
expected: "}".into(),
span: Span::with_offsets(10, 10, 3, 1),
};
assert_eq!(
parser_error_message(&err),
"unexpected end of file, expected }"
);
assert_eq!(parser_error_label(&err), "file ends here");
assert_eq!(
parser_error_help(&err),
Some("add a closing `}` to finish this block")
);
}
}