kinda works

This commit is contained in:
nora 2022-12-17 22:39:05 +01:00
parent 0f05ef625a
commit dcd163aeda
8 changed files with 111 additions and 59 deletions

View file

@ -1,12 +1,17 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use rustfix::diagnostics::Diagnostic; use rustfix::diagnostics::Diagnostic;
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashSet, fmt::Display, path::PathBuf, process::Command}; use std::{collections::HashSet, fmt::Display, path::PathBuf, process::Command, rc::Rc};
use crate::Options; use crate::Options;
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Build { pub struct Build {
inner: Rc<BuildInner>,
}
#[derive(Debug)]
struct BuildInner {
mode: BuildMode, mode: BuildMode,
input_path: PathBuf, input_path: PathBuf,
no_verify: bool, no_verify: bool,
@ -29,21 +34,23 @@ impl Build {
BuildMode::Rustc BuildMode::Rustc
}; };
Self { Self {
mode, inner: Rc::new(BuildInner {
input_path: options.path.clone(), mode,
no_verify: options.no_verify, input_path: options.path.clone(),
no_verify: options.no_verify,
}),
} }
} }
pub fn build(&self) -> Result<BuildResult> { pub fn build(&self) -> Result<BuildResult> {
if self.no_verify { if self.inner.no_verify {
return Ok(BuildResult { return Ok(BuildResult {
reproduces_issue: false, reproduces_issue: false,
no_verify: true, no_verify: true,
}); });
} }
let reproduces_issue = match &self.mode { let reproduces_issue = match &self.inner.mode {
BuildMode::Cargo => { BuildMode::Cargo => {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg("build"); cmd.arg("build");
@ -62,7 +69,7 @@ impl Build {
BuildMode::Rustc => { BuildMode::Rustc => {
let mut cmd = 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.inner.input_path);
cmd.output() cmd.output()
.context("spawning rustc process")? .context("spawning rustc process")?
@ -74,12 +81,12 @@ impl Build {
Ok(BuildResult { Ok(BuildResult {
reproduces_issue, reproduces_issue,
no_verify: self.no_verify, no_verify: self.inner.no_verify,
}) })
} }
pub fn get_suggestions(&self) -> Result<(Vec<Diagnostic>, Vec<rustfix::Suggestion>)> { pub fn get_diags(&self) -> Result<(Vec<Diagnostic>, Vec<rustfix::Suggestion>)> {
let diags = match self.mode { let diags = match self.inner.mode {
BuildMode::Cargo => { BuildMode::Cargo => {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.args(["build", "--message-format=json"]); cmd.args(["build", "--message-format=json"]);
@ -102,7 +109,7 @@ impl Build {
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"]);
cmd.arg(&self.input_path); cmd.arg(&self.inner.input_path);
let output = cmd.output()?.stderr; let output = cmd.output()?.stderr;
let output = String::from_utf8(output)?; let output = String::from_utf8(output)?;

View file

@ -1,7 +1,7 @@
use quote::ToTokens; use quote::ToTokens;
use syn::{parse_quote, visit_mut::VisitMut}; use syn::{parse_quote, visit_mut::VisitMut};
use crate::processor::{ProcessChecker, ProcessState, Processor}; use crate::processor::{ProcessChecker, ProcessState, Processor, SourceFile};
struct Visitor<'a> { struct Visitor<'a> {
current_path: Vec<String>, current_path: Vec<String>,
@ -68,6 +68,7 @@ impl Processor for EverybodyLoops {
fn process_file( fn process_file(
&mut self, &mut self,
krate: &mut syn::File, krate: &mut syn::File,
_: &SourceFile,
checker: &mut ProcessChecker, checker: &mut ProcessChecker,
) -> ProcessState { ) -> ProcessState {
let mut visitor = Visitor::new(checker); let mut visitor = Visitor::new(checker);

View file

@ -12,7 +12,7 @@ use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use processor::Minimizer; use processor::Minimizer;
use crate::{everybody_loops::EverybodyLoops, processor::Processor, privatize::Privatize}; use crate::{everybody_loops::EverybodyLoops, privatize::Privatize, processor::Processor};
#[derive(clap::Parser)] #[derive(clap::Parser)]
pub struct Options { pub struct Options {
@ -30,7 +30,7 @@ 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, &options); let mut minimizer = Minimizer::new_glob_dir(&options.path, build);
println!("{minimizer:?}"); println!("{minimizer:?}");

View file

@ -1,6 +1,6 @@
use syn::{parse_quote, visit_mut::VisitMut, Visibility}; use syn::{parse_quote, visit_mut::VisitMut, Visibility};
use crate::processor::{ProcessChecker, Processor, ProcessState}; use crate::processor::{ProcessChecker, ProcessState, Processor, SourceFile};
struct Visitor { struct Visitor {
pub_crate: Visibility, pub_crate: Visibility,
@ -29,7 +29,7 @@ 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) -> ProcessState { fn process_file(&mut self, krate: &mut syn::File, _: &SourceFile, _: &mut ProcessChecker) -> ProcessState {
let mut visitor = Visitor::new(); let mut visitor = Visitor::new();
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);
visitor.process_state visitor.process_state

View file

@ -4,7 +4,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct SourceFile { pub struct SourceFile {
pub path: PathBuf, pub path: PathBuf,
} }
@ -72,9 +72,8 @@ impl SourceFile {
} }
} }
impl Changes { impl Changes {
pub fn had_changes(&self) -> bool { pub fn had_changes(&self) -> bool {
self.any_change self.any_change
} }
} }

View file

@ -5,13 +5,24 @@ use std::{collections::HashSet, ffi::OsStr, path::Path};
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
use crate::{build::Build, processor::files::Changes, Options}; use crate::{build::Build, processor::files::Changes};
use self::files::SourceFile; pub use self::files::SourceFile;
pub trait Processor { pub trait Processor {
fn process_file(&mut self, krate: &mut syn::File, checker: &mut ProcessChecker) fn refresh_state(&mut self) -> Result<()> {
-> ProcessState; Ok(())
}
/// Process a file. The state of the processor might get invalidated in the process as signaled with
/// `ProcessState::FileInvalidated`. When a file is invalidated, the minimizer will call `Processor::refersh_state`
/// before calling the this function on the same file again.
fn process_file(
&mut self,
krate: &mut syn::File,
file: &SourceFile,
checker: &mut ProcessChecker,
) -> ProcessState;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
} }
@ -27,11 +38,10 @@ pub enum ProcessState {
pub struct Minimizer { pub struct Minimizer {
files: Vec<SourceFile>, files: Vec<SourceFile>,
build: Build, build: Build,
no_verify: bool,
} }
impl Minimizer { impl Minimizer {
pub fn new_glob_dir(path: &Path, build: Build, options: &Options) -> Self { pub fn new_glob_dir(path: &Path, build: Build) -> Self {
let walk = walkdir::WalkDir::new(path); let walk = walkdir::WalkDir::new(path);
let files = walk let files = walk
@ -49,29 +59,25 @@ impl Minimizer {
}) })
.collect(); .collect();
Self { Self { files, build }
files,
build,
no_verify: options.no_verify,
}
} }
pub fn run_passes<'a>( pub fn run_passes<'a>(
&mut self, &mut self,
passes: impl IntoIterator<Item = Box<dyn Processor>>, passes: impl IntoIterator<Item = Box<dyn Processor + 'a>>,
) -> Result<()> { ) -> Result<()> {
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
println!("Initial build: {}", inital_build); println!("Initial build: {}", inital_build);
if !self.no_verify { ensure!(
ensure!( inital_build.reproduces_issue(),
inital_build.reproduces_issue(), "Initial build must reproduce issue"
"Initial build must reproduce issue" );
);
}
let mut invalidated_files = HashSet::new(); let mut invalidated_files = HashSet::new();
for mut pass in passes { for mut pass in passes {
let mut refresh_and_try_again = false;
'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();
@ -88,7 +94,7 @@ impl Minimizer {
let mut krate = syn::parse_file(change.before_content()) 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, file, &mut ProcessChecker {});
match has_made_change { match has_made_change {
ProcessState::Changed | ProcessState::FileInvalidated => { ProcessState::Changed | ProcessState::FileInvalidated => {
@ -117,8 +123,18 @@ impl Minimizer {
} }
if !changes.had_changes() { if !changes.had_changes() {
if !refresh_and_try_again && invalidated_files.len() > 0 {
// A few files have been invalidated, let's refresh and try these again.
pass.refresh_state().context("refreshing state for pass")?;
refresh_and_try_again = true;
println!("Refreshing files for {}", pass.name());
continue;
}
println!("Finished {}", pass.name()); println!("Finished {}", pass.name());
break 'pass; break 'pass;
} else {
refresh_and_try_again = false;
} }
} }
} }

View file

@ -1,10 +1,16 @@
//! Deletes dead code. //! Deletes dead code.
use super::{files::Changes, Minimizer, ProcessState, Processor}; use crate::build::Build;
use super::{files::Changes, Minimizer, ProcessState, Processor, SourceFile};
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};
use std::{collections::HashMap, ops::Range, path::Path}; use std::{
collections::{HashMap, HashSet},
ops::Range,
path::Path,
};
use syn::{visit_mut::VisitMut, ImplItem, Item}; use syn::{visit_mut::VisitMut, ImplItem, Item};
fn file_for_suggestion(suggestion: &Suggestion) -> &str { fn file_for_suggestion(suggestion: &Suggestion) -> &str {
@ -15,16 +21,14 @@ impl Minimizer {
pub fn delete_dead_code(&mut self) -> Result<()> { pub fn delete_dead_code(&mut self) -> Result<()> {
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
println!("Before reaper: {}", inital_build); println!("Before reaper: {}", inital_build);
if !self.no_verify { ensure!(
ensure!( inital_build.reproduces_issue(),
inital_build.reproduces_issue(), "Initial build must reproduce issue"
"Initial build must reproduce issue" );
);
}
let (diags, suggestions) = self let (diags, suggestions) = self
.build .build
.get_suggestions() .get_diags()
.context("getting suggestions from rustc")?; .context("getting suggestions from rustc")?;
let mut suggestions_for_file = HashMap::<_, Vec<_>>::new(); let mut suggestions_for_file = HashMap::<_, Vec<_>>::new();
@ -38,11 +42,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 { self.run_passes([
diags, Box::new(DeleteUnusedFunctions::new(self.build.clone(), diags)) as Box<dyn Processor>,
invalid: false, ])
}) as Box<dyn Processor>]) .context("deleting unused functions")?;
.context("deleting unused functions")?;
Ok(()) Ok(())
} }
@ -91,22 +94,44 @@ impl Minimizer {
struct DeleteUnusedFunctions { struct DeleteUnusedFunctions {
diags: Vec<Diagnostic>, diags: Vec<Diagnostic>,
invalid: bool, build: Build,
invalid: HashSet<SourceFile>,
}
impl DeleteUnusedFunctions {
fn new(build: Build, diags: Vec<Diagnostic>) -> Self {
DeleteUnusedFunctions {
diags,
build,
invalid: HashSet::new(),
}
}
} }
impl Processor for DeleteUnusedFunctions { impl Processor for DeleteUnusedFunctions {
fn refresh_state(&mut self) -> Result<()> {
let (diags, _) = self.build.get_diags().context("getting diagnostics")?;
self.diags = diags;
self.invalid = HashSet::new();
Ok(())
}
fn process_file( fn process_file(
&mut self, &mut self,
krate: &mut syn::File, krate: &mut syn::File,
file: &SourceFile,
_: &mut super::ProcessChecker, _: &mut super::ProcessChecker,
) -> ProcessState { ) -> ProcessState {
assert!(!self.invalid, "processing with invalid state"); assert!(
!self.invalid.contains(file),
"processing with invalid state"
);
let mut visitor = FindUnusedFunction::new(self.diags.iter()); let mut visitor = FindUnusedFunction::new(file, self.diags.iter());
visitor.visit_file_mut(krate); visitor.visit_file_mut(krate);
if visitor.process_state == ProcessState::FileInvalidated { if visitor.process_state == ProcessState::FileInvalidated {
self.invalid = true; self.invalid.insert(file.clone());
} }
visitor.process_state visitor.process_state
@ -143,7 +168,7 @@ struct FindUnusedFunction {
} }
impl FindUnusedFunction { impl FindUnusedFunction {
fn new<'a>(diags: impl Iterator<Item = &'a Diagnostic>) -> Self { fn new<'a>(file: &SourceFile, diags: impl Iterator<Item = &'a Diagnostic>) -> Self {
let unused_functions = diags let unused_functions = diags
.filter_map(|diag| { .filter_map(|diag| {
// FIXME: use `code` correctly // FIXME: use `code` correctly
@ -167,6 +192,10 @@ impl FindUnusedFunction {
"encountered multiline span in dead_code" "encountered multiline span in dead_code"
); );
if Path::new(&span.file_name) != file.path {
return None;
}
Some(Unused { Some(Unused {
name, name,
line: span.line_start, line: span.line_start,

View file

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