mirror of
https://github.com/astro/deadnix.git
synced 2024-11-24 09:12:50 +03:00
implement editing source files
This commit is contained in:
parent
c60397ecd3
commit
3faa57f165
205
src/edit.rs
Normal file
205
src/edit.rs
Normal file
@ -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<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
|
||||
}
|
||||
|
||||
/// Deletes `nodes` from content
|
||||
///
|
||||
/// assumes `node` to be presorted
|
||||
pub fn edit_dead_code(original: &str, node: SyntaxNode<NixLanguage>, dead: impl Iterator<Item = DeadCode>) -> String {
|
||||
let mut dead = dead.map(|result| (result.binding.node.clone(), result))
|
||||
.collect::<HashMap<_, _>>();
|
||||
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<NixLanguage>, dead: &mut HashMap<SyntaxNode<NixLanguage>, DeadCode>, edits: &mut Vec<Edit>) {
|
||||
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<NixLanguage>, edits: &mut Vec<Edit>) {
|
||||
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);
|
||||
}
|
||||
}
|
140
src/edit_tests.rs
Normal file
140
src/edit_tests.rs
Normal file
@ -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");
|
||||
}
|
17
src/main.rs
17
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user