cargo-minimize/src/processor/mod.rs
moxian b4e587506f Fix delete-unused-functions panics
The pass used to (?) track invalidated files itself,
but now that functionality has been moved up one level,
but also kinda not really.

So here we clarify this by:
- making reaper not care about tracking invalidated files anymore
- making processor yes care about tracking invalidated files, and
    ensuring that it does not call process_file again after gettin
    ProcessState::FileInvalidated, as it advertizes to do.
2025-03-30 22:54:08 -07:00

301 lines
9.6 KiB
Rust

mod checker;
mod files;
mod reaper;
pub(crate) use self::files::SourceFile;
use crate::{build::Build, processor::files::Changes, Options};
use anyhow::{bail, Context, Result};
use owo_colors::OwoColorize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::{collections::HashSet, ffi::OsStr, fmt::Debug, sync::atomic::AtomicBool};
pub(crate) use self::checker::PassController;
pub(crate) trait Pass {
fn refresh_state(&mut self) -> Result<()> {
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 this function on the same file again.
fn process_file(
&mut self,
krate: &mut syn::File,
file: &SourceFile,
checker: &mut PassController,
) -> ProcessState;
fn name(&self) -> &'static str;
fn boxed(self) -> Box<dyn Pass>
where
Self: Sized + 'static,
{
Box::new(self)
}
}
impl Debug for dyn Pass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ProcessState {
NoChange,
Changed,
FileInvalidated,
}
#[derive(Debug)]
pub(crate) struct Minimizer {
files: Vec<SourceFile>,
build: Build,
options: Options,
cancel: Arc<AtomicBool>,
}
impl Minimizer {
fn pass_disabled(&self, name: &str) -> bool {
if let Some(passes) = &self.options.passes {
if !passes.split(",").any(|allowed| name == allowed) {
return true;
}
}
false
}
pub(crate) fn new_glob_dir(
options: Options,
build: Build,
cancel: Arc<AtomicBool>,
) -> Result<Self> {
let path = &options.path;
let walk = walkdir::WalkDir::new(path);
let files = walk
.into_iter()
.filter_map(|entry| match entry {
Ok(entry) => Some(entry),
Err(err) => {
warn!("Error during walkdir: {err}");
None
}
})
.filter(|entry| entry.path().extension() == Some(OsStr::new("rs")))
.filter(|entry| {
if options
.ignore_file
.iter()
.any(|ignored| entry.path().starts_with(ignored))
{
info!("Ignoring file: {}", entry.path().display());
false
} else {
true
}
})
.map(|entry| SourceFile::open(entry.into_path()))
.inspect(|file| {
if let Ok(file) = file {
info!("Collecting file: {file:?}");
}
})
.collect::<Result<Vec<_>>>()?;
if files.is_empty() {
bail!("Did not find any files for path {}", path.display());
}
if options.rustc && files.len() > 1 {
bail!("Found more than one file. --rustc only works with a single file.");
}
Ok(Self {
files,
build,
options,
cancel,
})
}
pub(crate) fn run_passes<'a>(
&self,
passes: impl IntoIterator<Item = Box<dyn Pass + 'a>>,
) -> Result<()> {
let inital_build = self.build.build()?;
info!("Initial build: {inital_build}");
inital_build.require_reproduction("Initial")?;
for mut pass in passes {
if self.pass_disabled(pass.name()) {
continue;
}
self.run_pass(&mut *pass)?;
}
Ok(())
}
fn run_pass(&self, pass: &mut dyn Pass) -> Result<()> {
let mut invalidated_files = HashSet::new();
let mut refresh_and_try_again = false;
loop {
let span = info_span!("Starting round of pass", name = pass.name());
let _enter = span.enter();
let mut changes = Changes::default();
for file in &self.files {
if invalidated_files.contains(file) {
continue;
}
self.process_file(pass, file, &mut invalidated_files, &mut changes)?;
}
if !changes.had_changes() {
if !refresh_and_try_again && !invalidated_files.is_empty() {
pass.refresh_state().context("refreshing state for pass")?;
invalidated_files.clear();
refresh_and_try_again = true;
info!("Refreshing files for {}", pass.name());
continue;
}
info!("Finished {}", pass.name());
return Ok(());
} else {
refresh_and_try_again = false;
}
}
}
#[instrument(skip(self, pass, invalidated_files, changes), fields(pass = %pass.name()), level = "debug")]
fn process_file<'file>(
&self,
pass: &mut dyn Pass,
file: &'file SourceFile,
invalidated_files: &mut HashSet<&'file SourceFile>,
changes: &mut Changes,
) -> Result<()> {
// The core logic of minimization.
// Here we process a single file (a unit of work) for a single pass.
// For this, we repeatedly try to apply a pass to a subset of a file until we've exhausted all options.
// The logic for bisecting down lives in PassController.
let mut checker = PassController::new(self.options.clone());
loop {
let mut change = file.try_change(changes)?;
let (_, krate) = change.before_content();
let mut krate = krate.clone();
let has_made_change = pass.process_file(&mut krate, file, &mut checker);
match has_made_change {
ProcessState::Changed | ProcessState::FileInvalidated => {
change.write(krate)?;
let after = self.build.build()?;
info!("{file:?}: After {}: {after}", pass.name());
if after.reproduces_issue() {
change.commit();
checker.reproduces();
if has_made_change == ProcessState::FileInvalidated {
invalidated_files.insert(file);
break;
}
} else {
change.rollback()?;
checker.does_not_reproduce();
}
}
ProcessState::NoChange => {
if self.options.no_color {
info!("{file:?}: After {}: no changes", pass.name());
} else {
info!("{file:?}: After {}: {}", pass.name(), "no changes".yellow());
}
checker.no_change();
}
}
if self.cancel.load(Ordering::SeqCst) {
info!("Exiting early.");
std::process::exit(0);
}
if checker.is_finished() {
break;
}
}
Ok(())
}
}
macro_rules! tracking {
() => {
tracking!(visit_item_fn_mut);
tracking!(visit_impl_item_method_mut);
tracking!(visit_item_impl_mut);
tracking!(visit_item_mod_mut);
tracking!(visit_field_mut);
tracking!(visit_item_struct_mut);
tracking!(visit_item_trait_mut);
};
(visit_item_fn_mut) => {
fn visit_item_fn_mut(&mut self, func: &mut syn::ItemFn) {
self.current_path.push(func.sig.ident.to_string());
syn::visit_mut::visit_item_fn_mut(self, func);
self.current_path.pop();
}
};
(visit_impl_item_method_mut) => {
fn visit_impl_item_method_mut(&mut self, method: &mut syn::ImplItemMethod) {
self.current_path.push(method.sig.ident.to_string());
syn::visit_mut::visit_impl_item_method_mut(self, method);
self.current_path.pop();
}
};
(visit_item_impl_mut) => {
fn visit_item_impl_mut(&mut self, item: &mut syn::ItemImpl) {
self.current_path
.push(item.self_ty.clone().into_token_stream().to_string());
syn::visit_mut::visit_item_impl_mut(self, item);
self.current_path.pop();
}
};
(visit_item_mod_mut) => {
fn visit_item_mod_mut(&mut self, module: &mut syn::ItemMod) {
self.current_path.push(module.ident.to_string());
syn::visit_mut::visit_item_mod_mut(self, module);
self.current_path.pop();
}
};
(visit_field_mut) => {
fn visit_field_mut(&mut self, field: &mut syn::Field) {
if let Some(ident) = &field.ident {
self.current_path.push(ident.to_string());
syn::visit_mut::visit_field_mut(self, field);
self.current_path.pop();
}
}
};
(visit_item_struct_mut) => {
fn visit_item_struct_mut(&mut self, struct_: &mut syn::ItemStruct) {
self.current_path.push(struct_.ident.to_string());
syn::visit_mut::visit_item_struct_mut(self, struct_);
self.current_path.pop();
}
};
(visit_item_trait_mut) => {
fn visit_item_trait_mut(&mut self, trait_: &mut syn::ItemTrait) {
self.current_path.push(trait_.ident.to_string());
syn::visit_mut::visit_item_trait_mut(self, trait_);
self.current_path.pop();
}
};
}
pub(crate) use tracking;