mirror of
https://github.com/astro/deadnix.git
synced 2024-10-03 21:28:43 +03:00
PoC
This commit is contained in:
commit
9fdad3260a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target/
|
107
Cargo.lock
generated
Normal file
107
Cargo.lock
generated
Normal file
@ -0,0 +1,107 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "cbitset"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "328b822bdcba4d4e402be8d9adb6eebf269f969f8eadef977a553ff3c4fbcb58"
|
||||
|
||||
[[package]]
|
||||
name = "deadnix"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rnix",
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rnix"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61f259c8a68822e61b46f7417158b7e70297aa8c0a45955aba49cd6fec9fd3e1"
|
||||
dependencies = [
|
||||
"cbitset",
|
||||
"rowan",
|
||||
"smol_str",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1b36e449f3702f3b0c821411db1cbdf30fb451726a9456dce5dabcd44420043"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"memoffset",
|
||||
"rustc-hash",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "deadnix"
|
||||
description = "Find unused code in Nix projects"
|
||||
version = "0.1.0"
|
||||
authors = ["Astro <astro@spaceboyz.net>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
homepage = "https://github.com/astro/deadnix"
|
||||
repository = "https://github.com/astro/deadnix.git"
|
||||
documentation = "https://docs.rs/deadnix"
|
||||
|
||||
[dependencies]
|
||||
rowan = "0.12"
|
||||
rnix = "0.10"
|
91
flake.lock
Normal file
91
flake.lock
Normal file
@ -0,0 +1,91 @@
|
||||
{
|
||||
"nodes": {
|
||||
"mozillapkgs": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1637337116,
|
||||
"narHash": "sha256-LKqAcdL+woWeYajs02bDQ7q8rsqgXuzhC354NoRaV80=",
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"rev": "cbc7435f5b0b3d17b16fb1d20cf7b616eec5e093",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1638203339,
|
||||
"narHash": "sha256-Sz3iCvbWrVWOD/XfYQeRJgP/7MVYL3/VKsNXvDeWBFc=",
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"rev": "c3e56b8a4ffb6d906cdfcfee034581f9a8ece571",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nmattia",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1638722875,
|
||||
"narHash": "sha256-B1BSlq6Mg4WLw7eLLW/JCM8xPkNsIkKRYgzJz6YPtEY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4722a8e10edcf46aaeb0b9f887bb756e25c6930e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1638722875,
|
||||
"narHash": "sha256-B1BSlq6Mg4WLw7eLLW/JCM8xPkNsIkKRYgzJz6YPtEY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4722a8e10edcf46aaeb0b9f887bb756e25c6930e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"mozillapkgs": "mozillapkgs",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1638122382,
|
||||
"narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
52
flake.nix
Normal file
52
flake.nix
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
inputs = {
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nmattia/naersk";
|
||||
mozillapkgs.url = "github:mozilla/nixpkgs-mozilla";
|
||||
mozillapkgs.flake = false;
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, naersk, mozillapkgs }:
|
||||
utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = nixpkgs.legacyPackages."${system}";
|
||||
mozilla = pkgs.callPackage (mozillapkgs + "/package-set.nix") {};
|
||||
rust = (mozilla.rustChannelOf {
|
||||
channel = "stable";
|
||||
date = "2021-12-02"; # "2021-11-01";
|
||||
sha256 = "0rqgx90k9lhfwaf63ccnm5qskzahmr4q18i18y6kdx48y26w3xz8";
|
||||
}).rust;
|
||||
|
||||
# Override the version used in naersk
|
||||
naersk-lib = naersk.lib."${system}".override {
|
||||
cargo = rust;
|
||||
rustc = rust;
|
||||
};
|
||||
in rec {
|
||||
# `nix build`
|
||||
packages.deadnix = naersk-lib.buildPackage {
|
||||
pname = "deadnix";
|
||||
src = ./.;
|
||||
};
|
||||
defaultPackage = packages.deadnix;
|
||||
|
||||
checks = packages;
|
||||
|
||||
# `nix run`
|
||||
apps.deadnix = utils.lib.mkApp {
|
||||
drv = packages.deadnix;
|
||||
};
|
||||
defaultApp = apps.deadnix;
|
||||
|
||||
# `nix develop`
|
||||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = with defaultPackage;
|
||||
nativeBuildInputs ++ buildInputs;
|
||||
};
|
||||
}) // {
|
||||
overlay = final: prev: {
|
||||
deadnix = self.packages.${prev.system};
|
||||
};
|
||||
|
||||
nixosModule = import ./nixos-module.nix { inherit self; };
|
||||
};
|
||||
}
|
266
src/main.rs
Normal file
266
src/main.rs
Normal file
@ -0,0 +1,266 @@
|
||||
use std::{env::args, fmt, fs};
|
||||
use rowan::api::SyntaxNode;
|
||||
use rnix::{
|
||||
NixLanguage,
|
||||
SyntaxKind,
|
||||
types::{
|
||||
AttrSet,
|
||||
EntryHolder, Ident, Lambda, LetIn,
|
||||
Pattern,
|
||||
TokenWrapper,
|
||||
TypedNode,
|
||||
},
|
||||
};
|
||||
|
||||
enum ResultKind {
|
||||
LambdaAt,
|
||||
LambdaPattern,
|
||||
LambdaArg,
|
||||
LetInEntry,
|
||||
LetInInherit,
|
||||
}
|
||||
|
||||
impl fmt::Display for ResultKind {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ResultKind::LambdaAt =>
|
||||
write!(fmt, "lambda @-binding"),
|
||||
ResultKind::LambdaPattern =>
|
||||
write!(fmt, "lambda pattern"),
|
||||
ResultKind::LambdaArg =>
|
||||
write!(fmt, "lambda argument"),
|
||||
ResultKind::LetInEntry =>
|
||||
write!(fmt, "let in binding"),
|
||||
ResultKind::LetInInherit =>
|
||||
write!(fmt, "let in inherit binding"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResultItem {
|
||||
kind: ResultKind,
|
||||
name: Ident,
|
||||
node: SyntaxNode<NixLanguage>,
|
||||
}
|
||||
|
||||
/// find out if `name` is used in `node`
|
||||
fn find_usage(name: &Ident, node: SyntaxNode<NixLanguage>) -> bool {
|
||||
// TODO: return false if shadowed by other let/rec/param binding
|
||||
|
||||
if node.kind() == SyntaxKind::NODE_IDENT {
|
||||
Ident::cast(node).expect("Ident::cast").as_str() == name.as_str()
|
||||
} else {
|
||||
node.children().any(|node| find_usage(name, node))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_dead_code(node: SyntaxNode<NixLanguage>, results: &mut Vec<ResultItem>) {
|
||||
match node.kind() {
|
||||
SyntaxKind::NODE_LAMBDA => {
|
||||
let lambda = Lambda::cast(node.clone())
|
||||
.expect("Lambda::cast");
|
||||
if let Some(arg) = lambda.arg() {
|
||||
match arg.kind() {
|
||||
SyntaxKind::NODE_IDENT => {
|
||||
let name = Ident::cast(arg.clone())
|
||||
.expect("Ident::cast");
|
||||
if !find_usage(&name, node.clone()) {
|
||||
results.push(ResultItem {
|
||||
kind: ResultKind::LambdaArg,
|
||||
name,
|
||||
node: arg,
|
||||
});
|
||||
}
|
||||
}
|
||||
SyntaxKind::NODE_PATTERN => {
|
||||
let pattern = Pattern::cast(arg)
|
||||
.expect("Pattern::cast");
|
||||
if let Some(name) = pattern.at() {
|
||||
// check if used in the pattern bindings, or the body
|
||||
if !pattern.entries().any(|entry| find_usage(&name, entry.node().clone()))
|
||||
&& !find_usage(&name, lambda.body().expect("body"))
|
||||
{
|
||||
results.push(ResultItem {
|
||||
kind: ResultKind::LambdaAt,
|
||||
node: name.node().clone(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
if pattern.ellipsis() {
|
||||
// `...` means args can be dropped
|
||||
for entry in pattern.entries() {
|
||||
let name = entry.name()
|
||||
.expect("entry.name()");
|
||||
// check if used in the other pattern bindings, or the body
|
||||
if !pattern.entries().any(|entry| {
|
||||
let other_name = entry.name().expect("entry.name()");
|
||||
other_name.as_str() != name.as_str() &&
|
||||
find_usage(&name, entry.node().clone())
|
||||
})
|
||||
&& !find_usage(&name, lambda.body().expect("lambda.body()")) {
|
||||
results.push(ResultItem {
|
||||
kind: ResultKind::LambdaPattern,
|
||||
node: name.node().clone(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("Unhandled arg kind: {:?}", arg.kind()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxKind::NODE_LET_IN => {
|
||||
let let_in = LetIn::cast(node.clone())
|
||||
.expect("LetIn::cast");
|
||||
if let Some(body) = let_in.body() {
|
||||
for key_value in let_in.entries() {
|
||||
let key = key_value.key()
|
||||
.expect("key_value.key()");
|
||||
let name_node = key.path().next()
|
||||
.expect("key.path()");
|
||||
let name = Ident::cast(name_node.clone())
|
||||
.expect("Ident::cast");
|
||||
if !let_in.entries().any(|entry| {
|
||||
let other_name = entry.key().expect("entry.key()")
|
||||
.path().next().expect("path().next()");
|
||||
let other_name = Ident::cast(other_name)
|
||||
.expect("Ident::cast");
|
||||
other_name.as_str() != name.as_str() &&
|
||||
find_usage(&name, entry.node().clone())
|
||||
})
|
||||
&& !let_in.inherits().any(|inherit|
|
||||
inherit.from().map(|from|
|
||||
find_usage(&name, from.node().clone())
|
||||
).unwrap_or(false))
|
||||
&& !find_usage(&name, body.clone()) {
|
||||
results.push(ResultItem {
|
||||
kind: ResultKind::LetInEntry,
|
||||
node: name_node,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
for inherit in let_in.inherits() {
|
||||
for ident in inherit.idents() {
|
||||
let name_node = ident.node();
|
||||
let name = Ident::cast(name_node.clone())
|
||||
.expect("Ident::cast");
|
||||
if !let_in.entries().any(|entry| find_usage(&name, entry.node().clone()))
|
||||
&& !let_in.inherits().any(|inherit|
|
||||
inherit.from().map(|from|
|
||||
find_usage(&name, from.node().clone())
|
||||
).unwrap_or(false))
|
||||
&& !find_usage(&name, body.clone()) {
|
||||
results.push(ResultItem {
|
||||
kind: ResultKind::LetInInherit,
|
||||
node: name_node.clone(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for child in node.children() {
|
||||
find_dead_code(child, results);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
for path in args().skip(1) {
|
||||
let content = match fs::read_to_string(&path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
eprintln!("Error reading file: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ast = rnix::parse(&content);
|
||||
let mut failed = false;
|
||||
for error in ast.errors() {
|
||||
eprintln!("Parse error: {}", error);
|
||||
failed = true;
|
||||
}
|
||||
if failed {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
find_dead_code(ast.node(), &mut results);
|
||||
|
||||
if results.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let mut offset = 0;
|
||||
while let Some(next) = content[offset..].find('\n') {
|
||||
let line = &content[offset..offset + next];
|
||||
lines.push((offset, line));
|
||||
offset += next + 1;
|
||||
}
|
||||
lines.push((offset, &content[offset..]));
|
||||
|
||||
let mut last_line = 0;
|
||||
let mut result_by_lines = Vec::new();
|
||||
for result in results {
|
||||
let range = result.node.text_range();
|
||||
let start = usize::from(range.start());
|
||||
let line_number = lines.iter().filter(|(offset, _)| *offset <= start).count();
|
||||
if line_number != last_line {
|
||||
last_line = line_number;
|
||||
result_by_lines.push((line_number, Vec::new()));
|
||||
}
|
||||
let result_by_lines_len = result_by_lines.len();
|
||||
let line_results = &mut result_by_lines[result_by_lines_len - 1].1;
|
||||
line_results.push(result);
|
||||
}
|
||||
for (line_number, results) in result_by_lines.iter_mut() {
|
||||
// file location
|
||||
println!("{}:{}:", path, line_number);
|
||||
// line
|
||||
println!("> {}", lines[*line_number - 1].1);
|
||||
results.sort_unstable_by_key(|result| result.node.text_range().start());
|
||||
|
||||
// underscores ^^^^^^^^^
|
||||
let line_start = lines[*line_number - 1].0;
|
||||
let mut pos = line_start;
|
||||
print!("> ");
|
||||
for result in results.iter() {
|
||||
let range = result.node.text_range();
|
||||
let start = usize::from(range.start());
|
||||
let end = usize::from(range.end());
|
||||
print!("{0: <1$}{2:^<3$}", "", start - pos, "", end - start);
|
||||
pos = end;
|
||||
}
|
||||
println!("");
|
||||
|
||||
let mut bars = String::new();
|
||||
let mut pos = line_start;
|
||||
for result in results.iter() {
|
||||
let range = result.node.text_range();
|
||||
let start = usize::from(range.start());
|
||||
bars = format!("{}{1: <2$}|", bars, "", start - pos);
|
||||
pos = start + 1;
|
||||
}
|
||||
println!("> {}", bars);
|
||||
|
||||
// messages
|
||||
for i in (0..results.len()).rev() {
|
||||
let result = &results[i];
|
||||
let range = result.node.text_range();
|
||||
let start = usize::from(range.start());
|
||||
println!("> {}unused {}: {}", &bars[..start - line_start], result.kind, result.name.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
test.nix
Normal file
17
test.nix
Normal file
@ -0,0 +1,17 @@
|
||||
unusedArgs@{ unusedArg, usedArg, ... }:
|
||||
let
|
||||
inherit (builtins) unused_inherit;
|
||||
inherit (used2) used_inherit;
|
||||
unused = "fnord";
|
||||
used1 = "important";
|
||||
used2 = usedArg;
|
||||
used3 = used4: "k.${used4}";
|
||||
used4 = { t = used_inherit; };
|
||||
shadowed = 42;
|
||||
in {
|
||||
x = { unusedArg2, x ? args.y, ... }@args: used1 + x;
|
||||
inherit used2;
|
||||
"${used3}" = true;
|
||||
y = used4.t;
|
||||
z = let shadowed = 23; in shadowed;
|
||||
}
|
Loading…
Reference in New Issue
Block a user