use harn_lexer::{FixEdit, Span};
use harn_parser::{DiagnosticCode as Code, SNode};
use crate::diagnostic::{LintDiagnostic, LintSeverity};
use crate::harndoc::{collect_comment_tokens, LegacyCommentTok};
use crate::naming::{build_line_starts, is_import_item, is_top_level_item};
pub(crate) fn check_blank_line_between_items(
source: &str,
program: &[SNode],
diagnostics: &mut Vec<LintDiagnostic>,
) {
if program.len() < 2 {
return;
}
let comment_tokens = collect_comment_tokens(source);
let comments_by_line: std::collections::HashMap<usize, &LegacyCommentTok> =
comment_tokens.iter().map(|c| (c.line, c)).collect();
let line_starts = build_line_starts(source);
for pair in program.windows(2) {
let prev = &pair[0];
let next = &pair[1];
if is_import_item(&prev.node) && is_import_item(&next.node) {
continue;
}
if !is_top_level_item(&prev.node) && !is_import_item(&prev.node) {
continue;
}
if !is_top_level_item(&next.node) && !is_import_item(&next.node) {
continue;
}
if prev.span.line == 0 || next.span.line == 0 {
continue;
}
let mut first_line = next.span.line;
let mut probe = next.span.line;
while probe > 1 {
let above = probe - 1;
if comments_by_line.contains_key(&above) {
first_line = above;
probe = above;
continue;
}
break;
}
let prev_end_line = prev.span.end_line.max(prev.span.line);
if first_line <= prev_end_line + 1 {
let insert_line = prev_end_line + 1;
let Some(&insert_offset) = line_starts.get(insert_line.saturating_sub(1)) else {
continue;
};
let span = Span::with_offsets(insert_offset, insert_offset, insert_line, 1);
diagnostics.push(LintDiagnostic {
code: Code::LintBlankLineBetweenItems,
rule: "blank-line-between-items".into(),
message: "top-level items should be separated by a blank line".to_string(),
span,
severity: LintSeverity::Warning,
suggestion: Some(
"insert a blank line above the next item (doc comments \
stay glued to the item they describe)"
.to_string(),
),
fix: Some(vec![FixEdit {
span,
replacement: "\n".to_string(),
}]),
});
}
}
}