This commit is contained in:
Astro 2021-12-06 00:04:52 +01:00
commit 9fdad3260a
7 changed files with 548 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target/

107
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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;
}