use crate::{dead_code::DeadCode, scope::Scope};
use rnix::{
ast::{HasEntry, Inherit, LetIn},
NixLanguage, SyntaxKind,
};
use rowan::{api::SyntaxNode, ast::AstNode};
#[derive(Debug)]
struct Edit {
start: usize,
end: usize,
replacement: String,
}
fn apply_edits<'a>(src: &str, edits: impl Iterator<Item = &'a Edit>) -> String {
let mut pos = 0;
let mut result = String::with_capacity(src.len());
for edit in edits {
if pos <= edit.end {
result.push_str(&src[pos..edit.start]);
result.push_str(&edit.replacement);
pos = edit.end;
}
}
result.push_str(&src[pos..]);
result
}
pub fn edit_dead_code(original: &str, dead: impl Iterator<Item = DeadCode>) -> (String, bool) {
let mut edits = dead.filter_map(dead_to_edit).collect::<Vec<_>>();
edits.sort_unstable_by(|e1, e2| {
if e1.start == e2.start {
e1.end.cmp(&e2.end)
} else {
e1.start.cmp(&e2.start)
}
});
let has_changes = !edits.is_empty();
let edited = apply_edits(original, edits.iter());
let ast = rnix::Root::parse(&edited);
let mut let_in_edits = Vec::new();
remove_empty_scopes(&ast.syntax(), &mut let_in_edits);
if let_in_edits.is_empty() {
(edited, has_changes)
} else {
(apply_edits(&edited, let_in_edits.iter()), true)
}
}
fn dead_to_edit(dead_code: DeadCode) -> Option<Edit> {
let range = dead_code.binding.decl_node.text_range();
let mut start = usize::from(range.start());
let mut end = usize::from(range.end());
let mut replace_node = dead_code.binding.decl_node.clone();
let mut replacement = None;
match dead_code.scope {
Scope::LambdaPattern(pattern, _) => {
if pattern.pat_bind().is_some_and(|at| {
at.ident().expect("at.ident").syntax() == &dead_code.binding.decl_node
}) {
if let Some(pattern_bind_node) = pattern
.syntax()
.children()
.find(|child| child.kind() == SyntaxKind::NODE_PAT_BIND)
{
let pattern_bind_range = pattern_bind_node.text_range();
start = usize::from(pattern_bind_range.start());
end = usize::from(pattern_bind_range.end());
if let Some(next) = pattern_bind_node.next_sibling_or_token() {
if next.kind() == SyntaxKind::TOKEN_WHITESPACE {
end = usize::from(next.text_range().end());
}
}
replacement = Some(String::new());
replace_node = pattern_bind_node;
}
} else {
let mut tokens = pattern.syntax().children_with_tokens().skip_while(|node| {
node.as_node()
.is_none_or(|node| *node != dead_code.binding.decl_node)
});
tokens.next();
for token in tokens {
replacement = Some(String::new());
if token.kind() == SyntaxKind::TOKEN_COMMA {
end = usize::from(token.text_range().end());
break;
} else if token.kind() == SyntaxKind::TOKEN_R_BRACE {
let Some(mut prev) = dead_code.binding.decl_node.prev_sibling_or_token() else {
break;
};
while prev.kind() == SyntaxKind::TOKEN_WHITESPACE {
match prev.prev_sibling_or_token() {
Some(prev_token) => prev = prev_token,
None => break,
}
}
if prev.kind() == SyntaxKind::TOKEN_COMMA {
start = usize::from(prev.text_range().start());
}
break;
} else if token.kind() != SyntaxKind::TOKEN_WHITESPACE {
break;
}
}
}
}
Scope::LambdaArg(name, _) => {
replacement = Some(format!("_{name}"));
}
Scope::LetIn(let_in) => {
if let_in
.attrpath_values()
.any(|entry| *entry.syntax() == dead_code.binding.decl_node)
{
replacement = Some(String::new());
} else if let Some(ident) = let_in
.inherits()
.flat_map(|inherit| {
inherit
.attrs()
.filter(|attr| attr.syntax() == &dead_code.binding.decl_node)
})
.next()
{
let range = ident.syntax().text_range();
start = usize::from(range.start());
end = usize::from(range.end());
replace_node = ident.syntax().clone();
replacement = Some(String::new());
}
}
Scope::RecAttrSet(_) => {}
}
replacement.map(|replacement| {
if let Some(prev) = replace_node.prev_sibling_or_token() {
if prev.kind() == SyntaxKind::TOKEN_WHITESPACE {
let whitespace_start = usize::from(prev.text_range().start());
if whitespace_start < start {
start = whitespace_start;
}
}
}
Edit {
start,
end,
replacement,
}
})
}
fn remove_empty_scopes(node: &SyntaxNode<NixLanguage>, edits: &mut Vec<Edit>) {
match node.kind() {
SyntaxKind::NODE_LET_IN => {
let let_in = LetIn::cast(node.clone()).expect("LetIn::cast");
if let_in
.inherits()
.all(|inherit| inherit.attrs().next().is_none())
&& let_in.attrpath_values().next().is_none()
{
let start = usize::from(node.text_range().start());
let end = usize::from(
let_in
.body()
.expect("let_in.body")
.syntax()
.text_range()
.start(),
);
edits.push(Edit {
start,
end,
replacement: String::new(),
});
}
}
SyntaxKind::NODE_INHERIT => {
let inherit = Inherit::cast(node.clone()).expect("Inherit::cast");
if inherit.attrs().next().is_none() {
let mut start = usize::from(node.text_range().start());
if let Some(prev) = node.prev_sibling_or_token() {
if prev.kind() == SyntaxKind::TOKEN_WHITESPACE {
start = usize::from(prev.text_range().start());
}
}
let end = usize::from(node.text_range().end());
edits.push(Edit {
start,
end,
replacement: String::new(),
});
}
}
_ => {}
}
for child in node.children() {
remove_empty_scopes(&child, edits);
}
}