item deleter pass

This commit is contained in:
nora 2023-01-22 13:19:18 +01:00
parent 22804c3065
commit fdfde615f6
8 changed files with 162 additions and 5 deletions

View file

@ -315,6 +315,7 @@ impl Build {
} }
} }
#[derive(Debug)]
pub struct BuildResult { pub struct BuildResult {
reproduces_issue: bool, reproduces_issue: bool,
no_verify: bool, no_verify: bool,

View file

@ -74,6 +74,10 @@ pub struct Options {
#[arg(default_value = "src")] #[arg(default_value = "src")]
pub path: PathBuf, pub path: PathBuf,
/// A comma-seperated list of passes that should be enabled. By default, all passes are enabled.
#[arg(long)]
pub passes: Option<String>,
/// A path to a script that is run to check whether code reproduces. When it exits with code 0, the /// A path to a script that is run to check whether code reproduces. When it exits with code 0, the
/// problem reproduces. If `--script-path-lints` isn't set, this script is also run to get lints. /// problem reproduces. If `--script-path-lints` isn't set, this script is also run to get lints.
/// For lints, the `MINIMIZE_LINTS` environment variable will be set to `1`. /// For lints, the `MINIMIZE_LINTS` environment variable will be set to `1`.
@ -119,6 +123,7 @@ pub fn minimize(options: Options) -> Result<()> {
minimizer.run_passes([ minimizer.run_passes([
passes::Privatize::default().boxed(), passes::Privatize::default().boxed(),
passes::EverybodyLoops::default().boxed(), passes::EverybodyLoops::default().boxed(),
passes::ItemDeleter::default().boxed(),
])?; ])?;
minimizer.delete_dead_code().context("deleting dead code")?; minimizer.delete_dead_code().context("deleting dead code")?;
@ -154,6 +159,7 @@ impl Default for Options {
env: Vec::new(), env: Vec::new(),
project_dir: None, project_dir: None,
path: PathBuf::from("/the/wrong/path/you/need/to/change/it"), path: PathBuf::from("/the/wrong/path/you/need/to/change/it"),
passes: None,
script_path: None, script_path: None,
script_path_lints: None, script_path_lints: None,
} }

View file

@ -28,6 +28,8 @@ impl VisitMut for Visitor<'_> {
[syn::Stmt::Expr(syn::Expr::Loop(syn::ExprLoop { [syn::Stmt::Expr(syn::Expr::Loop(syn::ExprLoop {
body: loop_body, .. body: loop_body, ..
}))] if loop_body.stmts.is_empty() => {} }))] if loop_body.stmts.is_empty() => {}
// Empty bodies are empty already, no need to loopify them.
[] => {}
_ if self.checker.can_process(&self.current_path) => { _ if self.checker.can_process(&self.current_path) => {
*block = self.loop_expr.clone(); *block = self.loop_expr.clone();
self.process_state = ProcessState::Changed; self.process_state = ProcessState::Changed;

111
src/passes/item_deleter.rs Normal file
View file

@ -0,0 +1,111 @@
use quote::ToTokens;
use syn::{
visit_mut::VisitMut, Item, ItemConst, ItemEnum, ItemMacro, ItemMacro2, ItemMod, ItemStatic,
ItemStruct, ItemTrait, ItemType, ItemUnion,
};
use crate::processor::{tracking, Pass, PassController, ProcessState, SourceFile};
struct Visitor<'a> {
current_path: Vec<String>,
checker: &'a mut PassController,
process_state: ProcessState,
}
impl<'a> Visitor<'a> {
fn new(checker: &'a mut PassController) -> Self {
Self {
current_path: Vec::new(),
checker,
process_state: ProcessState::NoChange,
}
}
fn should_retain_item(&mut self) -> bool {
let can_process = self.checker.can_process(&self.current_path);
if can_process {
self.process_state = ProcessState::Changed;
}
!can_process
}
fn consider_deleting_item(&mut self, item: &Item) -> bool {
match item {
// N.B. Do not delete ItemFn because that makes testing way harder
// and also the dead_lint should cover it all.
Item::Impl(impl_) => {
self.current_path
.push(impl_.self_ty.clone().into_token_stream().to_string());
let should_retain = self.should_retain_item();
self.current_path.pop();
should_retain
}
Item::Struct(ItemStruct { ident, .. })
| Item::Enum(ItemEnum { ident, .. })
| Item::Union(ItemUnion { ident, .. })
| Item::Const(ItemConst { ident, .. })
| Item::Type(ItemType { ident, .. })
| Item::Trait(ItemTrait { ident, .. })
| Item::Macro(ItemMacro {
ident: Some(ident), ..
})
| Item::Macro2(ItemMacro2 { ident, .. })
| Item::Static(ItemStatic { ident, .. })
| Item::Mod(ItemMod { ident, .. }) => {
self.current_path.push(ident.to_string());
let should_retain = self.should_retain_item();
self.current_path.pop();
should_retain
}
_ => true,
}
}
}
impl VisitMut for Visitor<'_> {
fn visit_file_mut(&mut self, file: &mut syn::File) {
file.items
.retain_mut(|item| self.consider_deleting_item(item));
syn::visit_mut::visit_file_mut(self, file);
}
fn visit_item_mod_mut(&mut self, module: &mut syn::ItemMod) {
self.current_path.push(module.ident.to_string());
if let Some((_, items)) = &mut module.content {
items.retain(|item| self.consider_deleting_item(item));
}
syn::visit_mut::visit_item_mod_mut(self, module);
self.current_path.pop();
}
tracking!(visit_item_fn_mut);
tracking!(visit_impl_item_method_mut);
tracking!(visit_item_impl_mut);
}
#[derive(Default)]
pub struct ItemDeleter;
impl Pass for ItemDeleter {
fn process_file(
&mut self,
krate: &mut syn::File,
_: &SourceFile,
checker: &mut PassController,
) -> ProcessState {
let mut visitor = Visitor::new(checker);
visitor.visit_file_mut(krate);
visitor.process_state
}
fn name(&self) -> &'static str {
"item-deleter"
}
}

View file

@ -1,4 +1,5 @@
mod everybody_loops; mod everybody_loops;
mod item_deleter;
mod privatize; mod privatize;
pub use self::{everybody_loops::EverybodyLoops, privatize::Privatize}; pub use self::{everybody_loops::EverybodyLoops, item_deleter::ItemDeleter, privatize::Privatize};

View file

@ -56,6 +56,15 @@ pub(crate) struct Minimizer {
} }
impl Minimizer { 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) -> Result<Self> { pub(crate) fn new_glob_dir(options: Options, build: Build) -> Result<Self> {
let path = &options.path; let path = &options.path;
let walk = walkdir::WalkDir::new(path); let walk = walkdir::WalkDir::new(path);
@ -102,6 +111,9 @@ impl Minimizer {
inital_build.require_reproduction("Initial")?; inital_build.require_reproduction("Initial")?;
for mut pass in passes { for mut pass in passes {
if self.pass_disabled(pass.name()) {
continue;
}
self.run_pass(&mut *pass)?; self.run_pass(&mut *pass)?;
} }

View file

@ -18,8 +18,14 @@ fn file_for_suggestion(suggestion: &Suggestion) -> &str {
&suggestion.solutions[0].replacements[0].snippet.file_name &suggestion.solutions[0].replacements[0].snippet.file_name
} }
const PASS_NAME: &str = "delete-unused-functions";
impl Minimizer { impl Minimizer {
pub fn delete_dead_code(&mut self) -> Result<()> { pub fn delete_dead_code(&mut self) -> Result<()> {
if self.pass_disabled(PASS_NAME) {
return Ok(());
}
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
info!("Before reaper: {inital_build}"); info!("Before reaper: {inital_build}");
@ -139,7 +145,7 @@ impl Pass for DeleteUnusedFunctions {
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"delete-unused-functions" PASS_NAME
} }
} }

View file

@ -47,6 +47,26 @@ fn unused() -> Result<()> {
) )
} }
#[test]
fn impls() -> Result<()> {
// Delete unused impls
run_test(
r##"
pub trait Uwu {}
impl Uwu for () {}
impl Uwu for u8 {}
fn main() {}
"##,
r##"
fn main() {}
"##,
|opts| {
opts.no_verify = true;
},
)
}
#[test] #[test]
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn custom_script_success() -> Result<()> { fn custom_script_success() -> Result<()> {
@ -61,9 +81,7 @@ fn custom_script_success() -> Result<()> {
fn main() {} fn main() {}
"##, "##,
r##" r##"
fn main() { fn main() {}
loop {}
}
"##, "##,
|opts| { |opts| {
opts.script_path = Some(script_path); opts.script_path = Some(script_path);