use harn_lexer::{FixEdit, Span};
use harn_parser::{DiagnosticCode as Code, Node, SNode};
use crate::diagnostic::{LintDiagnostic, LintSeverity};
use crate::naming::is_import_item;
pub(crate) fn check_import_order(
source: &str,
program: &[SNode],
diagnostics: &mut Vec<LintDiagnostic>,
) {
let mut imports: Vec<&SNode> = Vec::new();
for node in program {
if is_import_item(&node.node) {
imports.push(node);
} else {
break;
}
}
if imports.len() < 2 {
return;
}
let mut sorted = imports.clone();
sorted.sort_by_key(|a| import_sort_key(a));
let already_sorted = imports
.iter()
.zip(sorted.iter())
.all(|(a, b)| std::ptr::eq(*a, *b));
if already_sorted {
return;
}
let first = imports.first().unwrap();
let last = imports.last().unwrap();
let replace_start = import_source_start(source, first).unwrap_or(first.span.start);
let replace_span = Span::with_offsets(
replace_start,
last.span.end,
first.span.line,
first.span.column,
);
let fix = if import_block_has_comments(source, first.span.start, last.span.end) {
None
} else {
let replacement = sorted
.iter()
.map(|n| render_import_source(source, n))
.collect::<Option<Vec<_>>>()
.map(|imports| imports.join("\n"));
replacement.map(|replacement| {
vec![FixEdit {
span: replace_span,
replacement,
}]
})
};
diagnostics.push(LintDiagnostic {
code: Code::LintImportOrder,
rule: "import-order".into(),
message: "imports are not in canonical order (stdlib first, then alphabetical by path)"
.to_string(),
span: replace_span,
severity: LintSeverity::Warning,
suggestion: Some(
"reorder imports: std/ first, then third-party and local paths alphabetically"
.to_string(),
),
fix,
});
}
fn import_sort_key(node: &SNode) -> (u8, String, u8, String) {
match &node.node {
Node::ImportDecl { path, .. } => (
u8::from(!path.starts_with("std/")),
path.clone(),
0,
String::new(),
),
Node::NamespaceImport { alias, path, .. } => (
u8::from(!path.starts_with("std/")),
path.clone(),
1,
alias.clone(),
),
Node::SelectiveImport { names, path, .. } => {
let mut sorted_names = names.clone();
sorted_names.sort();
(
u8::from(!path.starts_with("std/")),
path.clone(),
2,
sorted_names.join(","),
)
}
_ => (3, String::new(), 3, String::new()),
}
}
fn render_import_source(source: &str, node: &SNode) -> Option<String> {
let start = import_source_start(source, node)?;
Some(source.get(start..node.span.end)?.to_string())
}
fn import_source_start(source: &str, node: &SNode) -> Option<usize> {
if !import_is_public(&node.node) {
return Some(node.span.start);
}
let source_before_import = source.get(..node.span.start)?;
let line_start = source_before_import
.rfind('\n')
.map_or(0, |newline| newline + 1);
let prefix = source.get(line_start..node.span.start)?;
let pub_offset = prefix.find("pub")?;
let before_pub = prefix.get(..pub_offset)?;
let after_pub = prefix.get(pub_offset + "pub".len()..)?;
(before_pub.trim().is_empty() && after_pub.trim().is_empty()).then_some(line_start + pub_offset)
}
fn import_is_public(node: &Node) -> bool {
match node {
Node::ImportDecl { is_pub, .. }
| Node::NamespaceImport { is_pub, .. }
| Node::SelectiveImport { is_pub, .. } => *is_pub,
_ => false,
}
}
fn import_block_has_comments(source: &str, start: usize, end: usize) -> bool {
source.get(start..end).is_some_and(|block| {
block
.lines()
.any(|line| line.trim_start().starts_with("//") || line.contains("/*"))
})
}