This commit is contained in:
nora 2022-12-17 21:34:04 +01:00
parent 002bad34ae
commit a9e488f3e3
11 changed files with 433 additions and 40 deletions

10
Cargo.lock generated
View file

@ -155,6 +155,8 @@ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustfix",
"serde_json",
"syn", "syn",
"walkdir", "walkdir",
] ]
@ -965,9 +967,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.46" version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" checksum = "e9d89e5dba24725ae5678020bf8f1357a9aa7ff10736b551adbcd3f8d17d766f"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1134,9 +1136,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.85" version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",

View file

@ -13,7 +13,9 @@ anyhow = "1.0.65"
cargo = "0.65.0" cargo = "0.65.0"
clap = { version = "4.0.29", features = ["derive"] } clap = { version = "4.0.29", features = ["derive"] }
prettyplease = "0.1.19" prettyplease = "0.1.19"
proc-macro2 = "1.0.46" proc-macro2 = { version = "1.0.48", features = ["span-locations"] }
quote = "1.0.21" quote = "1.0.21"
rustfix = "0.6.1"
serde_json = "1.0.90"
syn = { version = "1.0.101", features = ["full", "visit", "visit-mut"] } syn = { version = "1.0.101", features = ["full", "visit", "visit-mut"] }
walkdir = "2.3.2" walkdir = "2.3.2"

View file

@ -1,5 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{fmt::Display, path::PathBuf}; use rustfix::diagnostics::Diagnostic;
use std::{collections::HashSet, fmt::Display, path::PathBuf};
use crate::Options; use crate::Options;
@ -7,6 +8,7 @@ use crate::Options;
pub struct Build { pub struct Build {
mode: BuildMode, mode: BuildMode,
input_path: PathBuf, input_path: PathBuf,
no_verify: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -28,6 +30,7 @@ impl Build {
Self { Self {
mode, mode,
input_path: options.path.clone(), input_path: options.path.clone(),
no_verify: options.no_verify,
} }
} }
@ -61,19 +64,58 @@ impl Build {
} }
}; };
Ok(BuildResult { reproduces_issue }) Ok(BuildResult {
reproduces_issue,
no_verify: self.no_verify,
})
}
pub fn get_suggestions(&self) -> Result<(Vec<Diagnostic>, Vec<rustfix::Suggestion>)> {
match self.mode {
BuildMode::Cargo => {
todo!();
}
BuildMode::Script(_) => todo!(),
BuildMode::Rustc => {
let mut cmd = std::process::Command::new("rustc");
cmd.args(["--edition", "2018", "--error-format=json"]);
cmd.arg(&self.input_path);
let output = cmd.output()?.stderr;
let output = String::from_utf8(output)?;
let diags = serde_json::Deserializer::from_str(&output).into_iter::<Diagnostic>().collect::<Result<_, _>>()?;
let suggestions = rustfix::get_suggestions_from_json(
&output,
&HashSet::new(),
rustfix::Filter::Everything,
)
.context("reading output as rustfix suggestions")?;
Ok((diags, suggestions))
}
}
} }
} }
pub struct BuildResult { pub struct BuildResult {
pub reproduces_issue: bool, reproduces_issue: bool,
no_verify: bool,
}
impl BuildResult {
pub fn reproduces_issue(&self) -> bool {
self.reproduces_issue || self.no_verify
}
} }
impl Display for BuildResult { impl Display for BuildResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.reproduces_issue { match (self.reproduces_issue, self.no_verify) {
true => f.write_str("yes"), (true, _) => f.write_str("yes"),
false => f.write_str("no"), (false, true) => f.write_str("no (ignore)"),
(false, false) => f.write_str("no"),
} }
} }
} }

View file

@ -8,11 +8,11 @@ mod expand;
mod privatize; mod privatize;
mod processor; mod processor;
use anyhow::Result; use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use processor::Minimizer; use processor::Minimizer;
use crate::{everybody_loops::EverybodyLoops, processor::Processor}; use crate::{everybody_loops::EverybodyLoops, processor::Processor, privatize::Privatize};
#[derive(clap::Parser)] #[derive(clap::Parser)]
pub struct Options { pub struct Options {
@ -20,6 +20,8 @@ pub struct Options {
verify_error_path: Option<PathBuf>, verify_error_path: Option<PathBuf>,
#[arg(long)] #[arg(long)]
cargo: bool, cargo: bool,
#[arg(long)]
no_verify: bool,
path: PathBuf, path: PathBuf,
} }
@ -28,15 +30,19 @@ pub fn minimize() -> Result<()> {
let build = build::Build::new(&options); let build = build::Build::new(&options);
let mut minimizer = Minimizer::new_glob_dir(&options.path, build); let mut minimizer = Minimizer::new_glob_dir(&options.path, build, &options);
println!("{minimizer:?}"); println!("{minimizer:?}");
minimizer.delete_dead_code().context("deleting dead code")?;
minimizer.run_passes([ minimizer.run_passes([
//Box::new(Privarize::default()) as Box<dyn Processor>, Box::new(Privatize::default()) as Box<dyn Processor>,
Box::new(EverybodyLoops::default()) as Box<dyn Processor>, Box::new(EverybodyLoops::default()) as Box<dyn Processor>,
])?; ])?;
minimizer.delete_dead_code().context("deleting dead code")?;
/* /*
let file = expand::expand(&dir).context("during expansion")?; let file = expand::expand(&dir).context("during expansion")?;

View file

@ -26,9 +26,9 @@ impl VisitMut for Visitor {
} }
#[derive(Default)] #[derive(Default)]
pub struct Privarize {} pub struct Privatize {}
impl Processor for Privarize { impl Processor for Privatize {
fn process_file(&mut self, krate: &mut syn::File, _: &mut ProcessChecker) -> bool { fn process_file(&mut self, krate: &mut syn::File, _: &mut ProcessChecker) -> bool {
let mut visitor = Visitor::new(); let mut visitor = Visitor::new();
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);

80
src/processor/files.rs Normal file
View file

@ -0,0 +1,80 @@
use anyhow::{Context, Result};
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug)]
pub struct SourceFile {
pub path: PathBuf,
}
#[derive(Default)]
pub struct Changes {
any_change: bool,
}
pub struct FileChange<'a, 'b> {
pub path: &'a Path,
content: String,
changes: &'b mut Changes,
has_written_change: bool,
}
impl FileChange<'_, '_> {
pub fn before_content(&self) -> &str {
&self.content
}
pub fn write(&mut self, new: &str) -> Result<()> {
self.has_written_change = true;
fs::write(&self.path, new).with_context(|| format!("writing file {}", self.path.display()))
}
pub fn rollback(mut self) -> Result<()> {
assert!(self.has_written_change);
self.has_written_change = false;
fs::write(self.path, &self.content)
.with_context(|| format!("writing file {}", self.path.display()))
}
pub fn commit(mut self) {
assert!(self.has_written_change);
self.has_written_change = false;
self.changes.any_change = true;
}
}
impl Drop for FileChange<'_, '_> {
fn drop(&mut self) {
if self.has_written_change {
fs::write(&self.path, self.before_content()).ok();
if !std::thread::panicking() {
panic!("File contains unsaved changes!");
}
}
}
}
impl SourceFile {
pub fn try_change<'file, 'change>(
&'file self,
changes: &'change mut Changes,
) -> Result<FileChange<'file, 'change>> {
let path = &self.path;
Ok(FileChange {
path,
changes,
has_written_change: false,
content: fs::read_to_string(path)
.with_context(|| format!("opening file {}", path.display()))?,
})
}
}
impl Changes {
pub fn had_changes(&self) -> bool {
self.any_change
}
}

View file

@ -1,11 +1,13 @@
use std::{ mod files;
ffi::OsStr, mod reaper;
path::{Path, PathBuf},
}; use std::{ffi::OsStr, path::Path};
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
use crate::build::Build; use crate::{build::Build, processor::files::Changes, Options};
use self::files::SourceFile;
pub trait Processor { pub trait Processor {
fn process_file(&mut self, krate: &mut syn::File, checker: &mut ProcessChecker) -> bool; fn process_file(&mut self, krate: &mut syn::File, checker: &mut ProcessChecker) -> bool;
@ -15,12 +17,13 @@ pub trait Processor {
#[derive(Debug)] #[derive(Debug)]
pub struct Minimizer { pub struct Minimizer {
files: Vec<PathBuf>, files: Vec<SourceFile>,
build: Build, build: Build,
no_verify: bool,
} }
impl Minimizer { impl Minimizer {
pub fn new_glob_dir(path: &Path, build: Build) -> Self { pub fn new_glob_dir(path: &Path, build: Build, options: &Options) -> Self {
let walk = walkdir::WalkDir::new(path); let walk = walkdir::WalkDir::new(path);
let files = walk let files = walk
@ -33,10 +36,16 @@ impl Minimizer {
} }
}) })
.filter(|entry| entry.path().extension() == Some(OsStr::new("rs"))) .filter(|entry| entry.path().extension() == Some(OsStr::new("rs")))
.map(|entry| entry.into_path()) .map(|entry| SourceFile {
path: entry.into_path(),
})
.collect(); .collect();
Self { files, build } Self {
files,
build,
no_verify: options.no_verify,
}
} }
pub fn run_passes<'a>( pub fn run_passes<'a>(
@ -45,23 +54,24 @@ impl Minimizer {
) -> Result<()> { ) -> Result<()> {
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
println!("Initial build: {}", inital_build); println!("Initial build: {}", inital_build);
ensure!( if !self.no_verify {
inital_build.reproduces_issue, ensure!(
"Initial build must reproduce issue" inital_build.reproduces_issue(),
); "Initial build must reproduce issue"
);
}
for mut pass in passes { for mut pass in passes {
'pass: loop { 'pass: loop {
println!("Starting a round of {}", pass.name()); println!("Starting a round of {}", pass.name());
let mut any_change = false; let mut changes = Changes::default();
for file in &self.files { for file in &self.files {
let file_display = file.display(); let file_display = file.path.display();
let before_string = std::fs::read_to_string(file) let mut change = file.try_change(&mut changes)?;
.with_context(|| format!("opening file {file_display}"))?;
let mut krate = syn::parse_file(&before_string) let mut krate = syn::parse_file(change.before_content())
.with_context(|| format!("parsing file {file_display}"))?; .with_context(|| format!("parsing file {file_display}"))?;
let has_made_change = pass.process_file(&mut krate, &mut ProcessChecker {}); let has_made_change = pass.process_file(&mut krate, &mut ProcessChecker {});
@ -69,23 +79,23 @@ impl Minimizer {
if has_made_change { if has_made_change {
let result = prettyplease::unparse(&krate); let result = prettyplease::unparse(&krate);
std::fs::write(file, &result)?; change.write(&result)?;
let after = self.build.build()?; let after = self.build.build()?;
println!("{file_display}: After {}: {after}", pass.name()); println!("{file_display}: After {}: {after}", pass.name());
if after.reproduces_issue { if after.reproduces_issue() {
any_change = true; change.commit();
} else { } else {
std::fs::write(file, before_string)?; change.rollback()?;
} }
} else { } else {
println!("{file_display}: After {}: no change", pass.name()); println!("{file_display}: After {}: no change", pass.name());
} }
} }
if !any_change { if !changes.had_changes() {
println!("Finished {}", pass.name()); println!("Finished {}", pass.name());
break 'pass; break 'pass;
} }

229
src/processor/reaper.rs Normal file
View file

@ -0,0 +1,229 @@
//! Deletes dead code.
use super::{files::Changes, Minimizer, Processor};
use anyhow::{ensure, Context, Result};
use proc_macro2::Span;
use rustfix::{diagnostics::Diagnostic, Suggestion};
use std::{collections::HashMap, ops::Range, path::Path};
use syn::{visit_mut::VisitMut, ImplItem, Item};
fn file_for_suggestion(suggestion: &Suggestion) -> &str {
&suggestion.solutions[0].replacements[0].snippet.file_name
}
impl Minimizer {
pub fn delete_dead_code(&mut self) -> Result<()> {
let inital_build = self.build.build()?;
println!("Before reaper: {}", inital_build);
if !self.no_verify {
ensure!(
inital_build.reproduces_issue(),
"Initial build must reproduce issue"
);
}
let (diags, suggestions) = self
.build
.get_suggestions()
.context("getting suggestions from rustc")?;
let mut suggestions_for_file = HashMap::<_, Vec<_>>::new();
for suggestion in &suggestions {
suggestions_for_file
.entry(file_for_suggestion(suggestion))
.or_default()
.push(suggestion);
}
// Always unconditionally apply unused imports.
self.apply_unused_imports(&suggestions_for_file)?;
self.run_passes([Box::new(DeleteUnusedFunctions(diags)) as Box<dyn Processor>])
.context("deleting unused functions")?;
Ok(())
}
fn apply_unused_imports<'a>(
&mut self,
suggestions: &HashMap<&str, Vec<&Suggestion>>,
) -> Result<()> {
for (file, suggestions) in suggestions {
let file = self
.files
.iter()
.find(|source| source.path == Path::new(file))
.expect("unknown file");
let mut changes = &mut Changes::default();
let mut change = file.try_change(&mut changes)?;
let desired_suggestions = suggestions
.iter()
.filter(|sugg| sugg.message.contains("unused import"))
.cloned()
.cloned()
.collect::<Vec<_>>();
let result = rustfix::apply_suggestions(change.before_content(), &desired_suggestions)?;
change.write(&result)?;
let after = self.build.build()?;
println!("{}: After reaper: {after}", file.path.display());
if after.reproduces_issue() {
change.commit();
} else {
change.rollback()?;
}
}
Ok(())
}
}
struct DeleteUnusedFunctions(Vec<Diagnostic>);
impl Processor for DeleteUnusedFunctions {
fn process_file(&mut self, krate: &mut syn::File, _: &mut super::ProcessChecker) -> bool {
let mut visitor = FindUnusedFunction::new(self.0.iter());
visitor.visit_file_mut(krate);
visitor.has_change
}
fn name(&self) -> &'static str {
"delete-unused-functions"
}
}
#[derive(Debug)]
struct Unused {
name: String,
line: usize,
column: Range<usize>,
}
impl Unused {
fn span_matches(&self, ident_span: Span) -> bool {
let (start, end) = (ident_span.start(), ident_span.end());
assert_eq!(start.line, end.line);
let line_matches = self.line == start.line;
let column_matches = self.column.start <= start.column && self.column.end >= end.column;
line_matches && column_matches
}
}
struct FindUnusedFunction {
unused_functions: Vec<Unused>,
has_change: bool,
}
impl FindUnusedFunction {
fn new<'a>(diags: impl Iterator<Item = &'a Diagnostic>) -> Self {
let unused_functions = diags
.filter_map(|diag| {
// FIXME: use `code` correctly
if diag
.code
.as_ref()
.map_or(false, |code| code.code != "dead_code")
{
return None;
}
if !diag.message.contains("function") {
return None;
}
let name = diag.message.split("`").nth(1)?.to_owned();
let span = &diag.spans[0];
assert_eq!(
span.line_start, span.line_end,
"encountered multiline span in dead_code"
);
Some(Unused {
name,
line: span.line_start,
column: (span.column_start - 1)..(span.column_end - 1),
})
})
.collect();
Self {
unused_functions,
has_change: false,
}
}
fn should_retain_item(&mut self, span: Span) -> bool {
let span_matches = self
.unused_functions
.iter()
.map(|a| a.span_matches(span))
.filter(|&matches| matches)
.count();
assert!(
span_matches < 2,
"multiple dead_code spans matched identifier: {span_matches}"
);
if span_matches == 1 {
self.has_change = true;
}
span_matches == 0
}
}
impl VisitMut for FindUnusedFunction {
fn visit_item_impl_mut(&mut self, item_impl: &mut syn::ItemImpl) {
item_impl.items.retain(|item| match item {
ImplItem::Method(method) => {
let span = method.sig.ident.span();
self.should_retain_item(span)
}
_ => true,
});
syn::visit_mut::visit_item_impl_mut(self, item_impl);
}
fn visit_file_mut(&mut self, krate: &mut syn::File) {
krate.items.retain(|item| match item {
Item::Fn(func) => {
let span = func.sig.ident.span();
self.should_retain_item(span)
}
_ => true,
});
syn::visit_mut::visit_file_mut(self, krate);
}
fn visit_item_mod_mut(&mut self, module: &mut syn::ItemMod) {
if let Some((_, content)) = &mut module.content {
content.retain(|item| match item {
Item::Fn(func) => {
let span = func.sig.ident.span();
self.should_retain_item(span)
}
_ => true,
})
}
syn::visit_mut::visit_item_mod_mut(self, module);
}
}

7
test-cases/unused-code/Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hello-world"
version = "0.1.0"

View file

@ -0,0 +1,10 @@
[workspace]
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View file

@ -0,0 +1,5 @@
fn unused() {}
fn main() {
unused();
}