From 3faa57f16587ae6a8db9c967a4bde6e2f738e31a Mon Sep 17 00:00:00 2001 From: Astro Date: Wed, 8 Dec 2021 01:15:28 +0100 Subject: [PATCH] implement editing source files --- src/edit.rs | 205 ++++++++++++++++++++++++++++++++++++++++++++++ src/edit_tests.rs | 140 +++++++++++++++++++++++++++++++ src/main.rs | 17 ++++ 3 files changed, 362 insertions(+) create mode 100644 src/edit.rs create mode 100644 src/edit_tests.rs diff --git a/src/edit.rs b/src/edit.rs new file mode 100644 index 0000000..3b66412 --- /dev/null +++ b/src/edit.rs @@ -0,0 +1,205 @@ +use std::collections::HashMap; +use rowan::api::SyntaxNode; +use rnix::{ + NixLanguage, + SyntaxKind, + types::{ + EntryHolder, + Inherit, + LetIn, + TokenWrapper, TypedNode + }, +}; +use crate::{ + dead_code::DeadCode, + scope::Scope, +}; + + +#[derive(Debug)] +struct Edit { + start: usize, + end: usize, + replacement: String, +} + +fn apply_edits<'a>(src: &str, edits: impl Iterator) -> 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 +} + +/// Deletes `nodes` from content +/// +/// assumes `node` to be presorted +pub fn edit_dead_code(original: &str, node: SyntaxNode, dead: impl Iterator) -> String { + let mut dead = dead.map(|result| (result.binding.node.clone(), result)) + .collect::>(); + let mut edits = Vec::with_capacity(dead.len()); + scan(node, &mut dead, &mut edits); + + edits.sort_unstable_by(|e1, e2| { + if e1.start == e2.start { + e1.end.cmp(&e2.end) + } else { + e1.start.cmp(&e2.start) + } + }); + + let edited = apply_edits(original, edits.iter()); + + // remove empty `let in` + let ast = rnix::parse(&edited); + let mut let_in_edits = Vec::new(); + remove_empty_scopes(ast.node(), &mut let_in_edits); + if edits.len() > 0 { + apply_edits(&edited, let_in_edits.iter()) + } else { + edited + } +} + +fn scan(node: SyntaxNode, dead: &mut HashMap, DeadCode>, edits: &mut Vec) { + if let Some(dead_code) = dead.remove(&node) { + let range = dead_code.binding.node.text_range(); + let mut start = usize::from(range.start()); + let mut end = usize::from(range.end()); + let mut replace_node = node.clone(); + let mut replacement = None; + match dead_code.scope { + Scope::LambdaPattern(pattern, _) => { + if pattern.at().map(|at| *at.node() == node).unwrap_or(false) { + if let Some(next) = node.next_sibling_or_token() { + // dead@{ ... } form + if next.kind() == SyntaxKind::TOKEN_AT { + end = usize::from(next.text_range().end()); + replacement = Some("".to_string()); + } + } + if replacement.is_none() { + if let Some(prev) = node.prev_sibling_or_token() { + // { ... }@dead form + if prev.kind() == SyntaxKind::TOKEN_AT { + start = usize::from(prev.text_range().start()); + replacement = Some("".to_string()); + } + } + } + } else { + if let Some(next) = node.next_sibling_or_token() { + // { dead, ... } form + if next.kind() == SyntaxKind::TOKEN_COMMA { + end = usize::from(next.text_range().end()); + replacement = Some("".to_string()); + } + } + } + } + + Scope::LambdaArg(_, _) => { + replacement = Some("_".to_string()); + } + + Scope::LetIn(let_in) => { + if let_in.entries().any(|entry| + *entry.node() == node + ) { + replacement = Some("".to_string()); + } else if let Some(inherit) = let_in.inherits().find(|inherit| + *inherit.node() == node + ) { + if let Some(ident) = inherit.idents().find(|ident| + ident.as_str() == dead_code.binding.name.as_str() + ) { + let range = ident.node().text_range(); + start = usize::from(range.start()); + end = usize::from(range.end()); + replace_node = ident.node().clone(); + replacement = Some("".to_string()); + } + } + } + + Scope::RecAttrSet(_) => {} + } + + if let Some(replacement) = replacement { + // remove whitespace before node + if let Some(prev) = replace_node.prev_sibling_or_token() { + if prev.kind() == SyntaxKind::TOKEN_WHITESPACE { + start = usize::from(prev.text_range().start()); + } + } + + edits.push(Edit { + start, end, + replacement, + }); + } + } + + // recurse through the AST + for child in node.children() { + scan(child, dead, edits); + } +} + +fn remove_empty_scopes(node: SyntaxNode, edits: &mut Vec) { + match node.kind() { + // remove empty `let in` constructs + SyntaxKind::NODE_LET_IN => { + let let_in = LetIn::cast(node.clone()) + .expect("LetIn::cast"); + if let_in.inherits().all(|inherit| inherit.idents().next().is_none()) + && let_in.entries().next().is_none() { + let mut start = usize::from(node.text_range().start()); + // remove whitespace before node + 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(let_in.body().expect("let_in.body").text_range().start()); + edits.push(Edit { + start, end, + replacement: "".to_string(), + }); + } + } + + // remove empty `inherit;` and `inherit (...);` constructs + SyntaxKind::NODE_INHERIT => { + let inherit = Inherit::cast(node.clone()) + .expect("Inherit::cast"); + if inherit.idents().next().is_none() { + let mut start = usize::from(node.text_range().start()); + // remove whitespace before node + 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: "".to_string(), + }); + } + } + + _ => {} + } + + // recurse through the AST + for child in node.children() { + remove_empty_scopes(child, edits); + } +} diff --git a/src/edit_tests.rs b/src/edit_tests.rs new file mode 100644 index 0000000..eb02e50 --- /dev/null +++ b/src/edit_tests.rs @@ -0,0 +1,140 @@ +#![cfg(test)] + +use crate::dead_code::Settings; + +fn run(content: &str) -> String { + let ast = rnix::parse(&content); + assert_eq!(0, ast.errors().len()); + + let results = Settings { + no_lambda_arg: false, + no_underscore: false, + }.find_dead_code(ast.node()); + crate::edit::edit_dead_code( + content, + ast.node(), + results.into_iter() + ) +} + +macro_rules! no_edits { + ($s: expr) => { + let s = $s.to_string(); + assert_eq!(run(&s), s); + } +} + +#[test] +fn let_in_alive() { + no_edits!("let alive = 23; in alive"); +} + +#[test] +fn let_in_alive_deep() { + no_edits!("let alive = 23; in if true then 42 else { ... }: alive"); +} + +#[test] +fn let_in_alive_dead() { + let results = run("let alive = 42; dead = 23; in alive"); + assert_eq!(results, "let alive = 42; in alive"); +} + +#[test] +fn let_in_dead_only() { + let results = run("let dead = 42; in alive"); + assert_eq!(results, "alive"); +} + +#[test] +fn let_inherit_in_alive() { + no_edits!("let inherit (x) alive; in alive"); +} + +#[test] +fn let_inherit_in_alive_dead() { + let results = run("let inherit alive dead; in alive"); + assert_eq!(results, "let inherit alive; in alive"); +} + +#[test] +fn let_inherit_dead_let_alive_in_dead() { + let results = run("let inherit dead; alive = true; in alive"); + assert_eq!(results, "let alive = true; in alive"); +} + +#[test] +fn let_inherit_in_dead_only() { + let results = run("let inherit dead; in alive"); + assert_eq!(results, "alive"); +} + +#[test] +fn let_inherit_from_in_alive() { + no_edits!("let inherit (x) alive; in alive"); +} + +#[test] +fn let_inherit_from_in_alive_dead() { + let results = run("let inherit (x) alive dead; in alive"); + assert_eq!(results, "let inherit (x) alive; in alive"); +} + +#[test] +fn let_inherit_from_dead_let_alive_in_dead() { + let results = run("let inherit (x) dead; alive = true; in alive"); + assert_eq!(results, "let alive = true; in alive"); +} + +#[test] +fn let_inherit_from_in_dead_only() { + let results = run("let inherit (x) dead; in alive"); + assert_eq!(results, "alive"); +} + +#[test] +fn lambda_arg_alive() { + no_edits!("alive: alive"); +} + +#[test] +fn lambda_arg_dead() { + let results = run("dead: false"); + assert_eq!(results, "_: false"); +} + +#[test] +fn lambda_at_pattern_dead() { + let results = run("dead@{ dead2 ? dead, ... }: false"); + assert_eq!(results, "{ ... }: false"); +} + +#[test] +fn lambda_lead_at_dead() { + let results = run("dead@{ ... }: false"); + assert_eq!(results, "{ ... }: false"); +} + +#[test] +fn lambda_trail_at_dead() { + let results = run("{ ... }@dead: false"); + assert_eq!(results, "{ ... }: false"); +} + +#[test] +fn lambda_at_shadowed() { + let results = run("dead@{ ... }: dead@{ ... }: dead"); + assert_eq!(results, "{ ... }: dead@{ ... }: dead"); +} + +#[test] +fn lambda_pattern_dead() { + let results = run("alive@{ dead, ... }: alive"); + assert_eq!(results, "alive@{ ... }: alive"); +} + +#[test] +fn lambda_pattern_mixed() { + let results = run("dead1@{ dead2, alive, ... }: alive"); + assert_eq!(results, "{ alive, ... }: alive"); +} diff --git a/src/main.rs b/src/main.rs index e4375b2..03061bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ mod usage; mod dead_code; mod dead_code_tests; mod report; +mod edit; +mod edit_tests; fn main() { @@ -28,6 +30,11 @@ fn main() { .long("quiet") .help("Don't print dead code report") ) + .arg(clap::Arg::with_name("EDIT") + .short("e") + .long("edit") + .help("Remove unused code and write to source file") + ) .arg(clap::Arg::with_name("FILE_PATHS") .multiple(true) .help(".nix files") @@ -39,6 +46,7 @@ fn main() { no_underscore: matches.is_present("NO_UNDERSCORE"), }; let quiet = matches.is_present("QUIET"); + let edit = matches.is_present("EDIT"); let file_paths = matches.values_of("FILE_PATHS") .expect("FILE_PATHS"); @@ -66,5 +74,14 @@ fn main() { crate::report::Report::new(file_path.to_string(), &content, results.clone()) .print(); } + if edit { + let new_ast = crate::edit::edit_dead_code( + &content, + ast.node(), + results.into_iter() + ); + fs::write(file_path, new_ast) + .expect("fs::write"); + } } }