This commit is contained in:
nora 2022-12-17 22:17:34 +01:00
parent a9e488f3e3
commit 0f05ef625a
11 changed files with 153 additions and 70 deletions

13
Cargo.lock generated
View file

@ -156,6 +156,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustfix", "rustfix",
"serde",
"serde_json", "serde_json",
"syn", "syn",
"walkdir", "walkdir",
@ -1107,18 +1108,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.145" version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.145" version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1194,9 +1195,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.102" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -16,6 +16,7 @@ prettyplease = "0.1.19"
proc-macro2 = { version = "1.0.48", features = ["span-locations"] } proc-macro2 = { version = "1.0.48", features = ["span-locations"] }
quote = "1.0.21" quote = "1.0.21"
rustfix = "0.6.1" rustfix = "0.6.1"
serde = { version = "1.0.151", features = ["derive"] }
serde_json = "1.0.90" 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,6 +1,7 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use rustfix::diagnostics::Diagnostic; use rustfix::diagnostics::Diagnostic;
use std::{collections::HashSet, fmt::Display, path::PathBuf}; use serde::Deserialize;
use std::{collections::HashSet, fmt::Display, path::PathBuf, process::Command};
use crate::Options; use crate::Options;
@ -35,9 +36,16 @@ impl Build {
} }
pub fn build(&self) -> Result<BuildResult> { pub fn build(&self) -> Result<BuildResult> {
if self.no_verify {
return Ok(BuildResult {
reproduces_issue: false,
no_verify: true,
});
}
let reproduces_issue = match &self.mode { let reproduces_issue = match &self.mode {
BuildMode::Cargo => { BuildMode::Cargo => {
let mut cmd = std::process::Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg("build"); cmd.arg("build");
let output = let output =
@ -47,12 +55,12 @@ impl Build {
output.contains("internal compiler error") output.contains("internal compiler error")
} }
BuildMode::Script(script_path) => { BuildMode::Script(script_path) => {
let mut cmd = std::process::Command::new(script_path); let mut cmd = Command::new(script_path);
cmd.output().context("spawning script")?.status.success() cmd.output().context("spawning script")?.status.success()
} }
BuildMode::Rustc => { BuildMode::Rustc => {
let mut cmd = std::process::Command::new("rustc"); let mut cmd = Command::new("rustc");
cmd.args(["--edition", "2018"]); cmd.args(["--edition", "2018"]);
cmd.arg(&self.input_path); cmd.arg(&self.input_path);
@ -71,11 +79,26 @@ impl Build {
} }
pub fn get_suggestions(&self) -> Result<(Vec<Diagnostic>, Vec<rustfix::Suggestion>)> { pub fn get_suggestions(&self) -> Result<(Vec<Diagnostic>, Vec<rustfix::Suggestion>)> {
match self.mode { let diags = match self.mode {
BuildMode::Cargo => { BuildMode::Cargo => {
todo!(); let mut cmd = Command::new("cargo");
cmd.args(["build", "--message-format=json"]);
let cmd_output = cmd.output()?;
let output = String::from_utf8(cmd_output.stdout.clone())?;
let messages = serde_json::Deserializer::from_str(&output)
.into_iter::<CargoJsonCompileMessage>()
.collect::<Result<Vec<_>, _>>()?;
let diags = messages
.into_iter()
.filter(|msg| msg.reason == "compiler-message")
.flat_map(|msg| msg.message)
.collect();
diags
} }
BuildMode::Script(_) => todo!(),
BuildMode::Rustc => { BuildMode::Rustc => {
let mut cmd = std::process::Command::new("rustc"); let mut cmd = std::process::Command::new("rustc");
cmd.args(["--edition", "2018", "--error-format=json"]); cmd.args(["--edition", "2018", "--error-format=json"]);
@ -84,18 +107,26 @@ impl Build {
let output = cmd.output()?.stderr; let output = cmd.output()?.stderr;
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;
let diags = serde_json::Deserializer::from_str(&output).into_iter::<Diagnostic>().collect::<Result<_, _>>()?; let diags = serde_json::Deserializer::from_str(&output)
.into_iter::<Diagnostic>()
.collect::<Result<_, _>>()?;
let suggestions = rustfix::get_suggestions_from_json( diags
&output,
&HashSet::new(),
rustfix::Filter::Everything,
)
.context("reading output as rustfix suggestions")?;
Ok((diags, suggestions))
} }
BuildMode::Script(_) => todo!(),
};
let mut suggestions = Vec::new();
for cargo_msg in &diags {
// One diagnostic line might have multiple suggestions
suggestions.extend(rustfix::collect_suggestions(
cargo_msg,
&HashSet::new(),
rustfix::Filter::Everything,
));
} }
Ok((diags, suggestions))
} }
} }
@ -114,8 +145,14 @@ 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, self.no_verify) { match (self.reproduces_issue, self.no_verify) {
(true, _) => f.write_str("yes"), (true, _) => f.write_str("yes"),
(false, true) => f.write_str("no (ignore)"), (false, true) => f.write_str("yes (no-verify)"),
(false, false) => f.write_str("no"), (false, false) => f.write_str("no"),
} }
} }
} }
#[derive(Debug, Deserialize)]
pub struct CargoJsonCompileMessage {
pub reason: String,
pub message: Option<Diagnostic>,
}

View file

@ -1,14 +1,14 @@
use quote::ToTokens; use quote::ToTokens;
use syn::{parse_quote, visit_mut::VisitMut}; use syn::{parse_quote, visit_mut::VisitMut};
use crate::processor::{ProcessChecker, Processor}; use crate::processor::{ProcessChecker, ProcessState, Processor};
struct Visitor<'a> { struct Visitor<'a> {
current_path: Vec<String>, current_path: Vec<String>,
checker: &'a mut ProcessChecker, checker: &'a mut ProcessChecker,
loop_expr: syn::Block, loop_expr: syn::Block,
has_made_change: bool, process_state: ProcessState,
} }
impl<'a> Visitor<'a> { impl<'a> Visitor<'a> {
@ -16,7 +16,7 @@ impl<'a> Visitor<'a> {
Self { Self {
current_path: Vec::new(), current_path: Vec::new(),
checker, checker,
has_made_change: false, process_state: ProcessState::NoChange,
loop_expr: parse_quote! { { loop {} } }, loop_expr: parse_quote! { { loop {} } },
} }
} }
@ -30,7 +30,7 @@ impl VisitMut for Visitor<'_> {
}))] if loop_body.stmts.is_empty() => {} }))] if loop_body.stmts.is_empty() => {}
_ => { _ => {
*block = self.loop_expr.clone(); *block = self.loop_expr.clone();
self.has_made_change = true; self.process_state = ProcessState::Changed;
} }
} }
} }
@ -65,10 +65,14 @@ impl VisitMut for Visitor<'_> {
pub struct EverybodyLoops; pub struct EverybodyLoops;
impl Processor for EverybodyLoops { impl Processor for EverybodyLoops {
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,
) -> ProcessState {
let mut visitor = Visitor::new(checker); let mut visitor = Visitor::new(checker);
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);
visitor.has_made_change visitor.process_state
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {

View file

@ -1,16 +1,16 @@
use syn::{parse_quote, visit_mut::VisitMut, Visibility}; use syn::{parse_quote, visit_mut::VisitMut, Visibility};
use crate::processor::{ProcessChecker, Processor}; use crate::processor::{ProcessChecker, Processor, ProcessState};
struct Visitor { struct Visitor {
pub_crate: Visibility, pub_crate: Visibility,
has_made_change: bool, process_state: ProcessState,
} }
impl Visitor { impl Visitor {
fn new() -> Self { fn new() -> Self {
Self { Self {
has_made_change: false, process_state: ProcessState::NoChange,
pub_crate: parse_quote! { pub(crate) }, pub_crate: parse_quote! { pub(crate) },
} }
} }
@ -19,7 +19,7 @@ impl Visitor {
impl VisitMut for Visitor { impl VisitMut for Visitor {
fn visit_visibility_mut(&mut self, vis: &mut Visibility) { fn visit_visibility_mut(&mut self, vis: &mut Visibility) {
if let Visibility::Public(_) = vis { if let Visibility::Public(_) = vis {
self.has_made_change = true; self.process_state = ProcessState::Changed;
*vis = self.pub_crate.clone(); *vis = self.pub_crate.clone();
} }
} }
@ -29,10 +29,10 @@ impl VisitMut for Visitor {
pub struct Privatize {} pub struct Privatize {}
impl Processor for Privatize { 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) -> ProcessState {
let mut visitor = Visitor::new(); let mut visitor = Visitor::new();
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);
visitor.has_made_change visitor.process_state
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {

View file

@ -4,7 +4,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
#[derive(Debug)] #[derive(Debug, PartialEq, Eq, Hash)]
pub struct SourceFile { pub struct SourceFile {
pub path: PathBuf, pub path: PathBuf,
} }

View file

@ -1,7 +1,7 @@
mod files; mod files;
mod reaper; mod reaper;
use std::{ffi::OsStr, path::Path}; use std::{collections::HashSet, ffi::OsStr, path::Path};
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
@ -10,11 +10,19 @@ use crate::{build::Build, processor::files::Changes, Options};
use self::files::SourceFile; 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)
-> ProcessState;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
} }
#[derive(Debug, PartialEq, Eq)]
pub enum ProcessState {
NoChange,
Changed,
FileInvalidated,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Minimizer { pub struct Minimizer {
files: Vec<SourceFile>, files: Vec<SourceFile>,
@ -61,12 +69,18 @@ impl Minimizer {
); );
} }
let mut invalidated_files = HashSet::new();
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 changes = Changes::default(); let mut changes = Changes::default();
for file in &self.files { for file in &self.files {
if invalidated_files.contains(file) {
continue;
}
let file_display = file.path.display(); let file_display = file.path.display();
let mut change = file.try_change(&mut changes)?; let mut change = file.try_change(&mut changes)?;
@ -76,22 +90,29 @@ impl Minimizer {
let has_made_change = pass.process_file(&mut krate, &mut ProcessChecker {}); let has_made_change = pass.process_file(&mut krate, &mut ProcessChecker {});
if has_made_change { match has_made_change {
let result = prettyplease::unparse(&krate); ProcessState::Changed | ProcessState::FileInvalidated => {
let result = prettyplease::unparse(&krate);
change.write(&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() {
change.commit(); change.commit();
} else { } else {
change.rollback()?; change.rollback()?;
}
if has_made_change == ProcessState::FileInvalidated {
invalidated_files.insert(file);
}
}
ProcessState::NoChange => {
println!("{file_display}: After {}: no change", pass.name());
} }
} else {
println!("{file_display}: After {}: no change", pass.name());
} }
} }

View file

@ -1,6 +1,6 @@
//! Deletes dead code. //! Deletes dead code.
use super::{files::Changes, Minimizer, Processor}; use super::{files::Changes, Minimizer, ProcessState, Processor};
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
use proc_macro2::Span; use proc_macro2::Span;
use rustfix::{diagnostics::Diagnostic, Suggestion}; use rustfix::{diagnostics::Diagnostic, Suggestion};
@ -38,7 +38,10 @@ impl Minimizer {
// Always unconditionally apply unused imports. // Always unconditionally apply unused imports.
self.apply_unused_imports(&suggestions_for_file)?; self.apply_unused_imports(&suggestions_for_file)?;
self.run_passes([Box::new(DeleteUnusedFunctions(diags)) as Box<dyn Processor>]) self.run_passes([Box::new(DeleteUnusedFunctions {
diags,
invalid: false,
}) as Box<dyn Processor>])
.context("deleting unused functions")?; .context("deleting unused functions")?;
Ok(()) Ok(())
@ -49,11 +52,12 @@ impl Minimizer {
suggestions: &HashMap<&str, Vec<&Suggestion>>, suggestions: &HashMap<&str, Vec<&Suggestion>>,
) -> Result<()> { ) -> Result<()> {
for (file, suggestions) in suggestions { for (file, suggestions) in suggestions {
let file = self let Some(file) = self
.files .files
.iter() .iter()
.find(|source| source.path == Path::new(file)) .find(|source| source.path == Path::new(file)) else {
.expect("unknown file"); continue;
};
let mut changes = &mut Changes::default(); let mut changes = &mut Changes::default();
@ -85,14 +89,27 @@ impl Minimizer {
} }
} }
struct DeleteUnusedFunctions(Vec<Diagnostic>); struct DeleteUnusedFunctions {
diags: Vec<Diagnostic>,
invalid: bool,
}
impl Processor for DeleteUnusedFunctions { impl Processor for DeleteUnusedFunctions {
fn process_file(&mut self, krate: &mut syn::File, _: &mut super::ProcessChecker) -> bool { fn process_file(
let mut visitor = FindUnusedFunction::new(self.0.iter()); &mut self,
krate: &mut syn::File,
_: &mut super::ProcessChecker,
) -> ProcessState {
assert!(!self.invalid, "processing with invalid state");
let mut visitor = FindUnusedFunction::new(self.diags.iter());
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);
visitor.has_change if visitor.process_state == ProcessState::FileInvalidated {
self.invalid = true;
}
visitor.process_state
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
@ -122,7 +139,7 @@ impl Unused {
struct FindUnusedFunction { struct FindUnusedFunction {
unused_functions: Vec<Unused>, unused_functions: Vec<Unused>,
has_change: bool, process_state: ProcessState,
} }
impl FindUnusedFunction { impl FindUnusedFunction {
@ -160,7 +177,7 @@ impl FindUnusedFunction {
Self { Self {
unused_functions, unused_functions,
has_change: false, process_state: ProcessState::NoChange,
} }
} }
@ -174,11 +191,11 @@ impl FindUnusedFunction {
assert!( assert!(
span_matches < 2, span_matches < 2,
"multiple dead_code spans matched identifier: {span_matches}" "multiple dead_code spans matched identifier: {span_matches}."
); );
if span_matches == 1 { if span_matches == 1 {
self.has_change = true; self.process_state = ProcessState::FileInvalidated;
} }
span_matches == 0 span_matches == 0

View file

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

View file

@ -1,5 +1,8 @@
fn unused() {} fn unused() {
loop {}
fn main() {
unused();
} }
fn main() {
loop {}
}
mod other;

View file

@ -0,0 +1,3 @@
fn unused() {
loop {}
}