mirror of
https://github.com/Noratrieb/cargo-minimize.git
synced 2026-01-17 09:45:01 +01:00
Compare commits
21 commits
cf39338b30
...
887aade421
| Author | SHA1 | Date | |
|---|---|---|---|
| 887aade421 | |||
| 45a5fda1a3 | |||
| b7019e1e43 | |||
| 2f1eaecad7 | |||
| 78460595e6 | |||
| 8d236a2e4a | |||
|
|
2efee491b5 | ||
|
|
d023307d8d | ||
|
|
ecf52e2d3b | ||
|
|
a87558adf7 | ||
|
|
3de6992d63 | ||
|
|
21a5f1733c | ||
|
|
46c26f7af9 | ||
|
|
2f9a0d45a1 | ||
|
|
5ebb428295 | ||
|
|
6d4331b16b | ||
|
|
0b7e1c2a82 | ||
|
|
2f885257e6 | ||
|
|
b44cd4e6eb | ||
|
|
92aec21748 | ||
|
|
fc25fcbfb5 |
25 changed files with 487 additions and 115 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -102,6 +102,7 @@ dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"genemichaels",
|
"genemichaels",
|
||||||
"libloading",
|
"libloading",
|
||||||
|
"markdown",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ exclude = ["test-cases/*", "full-tests/*"]
|
||||||
[package]
|
[package]
|
||||||
name = "cargo-minimize"
|
name = "cargo-minimize"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
categories = ["development-tools"]
|
categories = ["development-tools"]
|
||||||
description = "A tool for minimizing rustc ICEs"
|
description = "A tool for minimizing rustc ICEs"
|
||||||
keywords = ["minimization", "ICE", "rust-development"]
|
keywords = ["minimization", "ICE", "rust-development"]
|
||||||
|
|
@ -24,6 +24,7 @@ anyhow = "1.0.65"
|
||||||
clap = { version = "4.0.29", features = ["derive"] }
|
clap = { version = "4.0.29", features = ["derive"] }
|
||||||
ctrlc = "3.2.5"
|
ctrlc = "3.2.5"
|
||||||
genemichaels = "0.1.21"
|
genemichaels = "0.1.21"
|
||||||
|
markdown = { version = "=1.0.0-alpha.14" } # pinning the version to ensure genemichaels builds.
|
||||||
libloading = "0.8.0"
|
libloading = "0.8.0"
|
||||||
owo-colors = "3.5.0"
|
owo-colors = "3.5.0"
|
||||||
proc-macro2 = { version = "1.0.48", features = ["span-locations"] }
|
proc-macro2 = { version = "1.0.48", features = ["span-locations"] }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 Nilstrieb
|
Copyright (c) 2025 Noratrieb and contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -1,8 +1,8 @@
|
||||||
# cargo-minimize
|
# cargo-minimize
|
||||||
|
|
||||||
Install with `cargo install --git https://github.com/Nilstrieb/cargo-minimize cargo-minimize` and use with `cargo minimize`.
|
Install with `cargo install --git https://github.com/Noratrieb/cargo-minimize cargo-minimize` and use with `cargo minimize`.
|
||||||
|
|
||||||
For more info, see the [cookbook](https://github.com/Nilstrieb/cargo-minimize#cookbook).
|
For more info, see the [cookbook](https://github.com/Noratrieb/cargo-minimize#cookbook).
|
||||||
|
|
||||||
## Idea
|
## Idea
|
||||||
|
|
||||||
|
|
@ -37,12 +37,18 @@ Options:
|
||||||
Additional environment variables to pass to cargo/rustc. Example: `--env NAME=VALUE --env ANOTHER_NAME=VALUE`
|
Additional environment variables to pass to cargo/rustc. Example: `--env NAME=VALUE --env ANOTHER_NAME=VALUE`
|
||||||
--project-dir <PROJECT_DIR>
|
--project-dir <PROJECT_DIR>
|
||||||
The working directory where cargo/rustc are invoked in. By default, this is the current working directory
|
The working directory where cargo/rustc are invoked in. By default, this is the current working directory
|
||||||
|
--passes <PASSES>
|
||||||
|
A comma-seperated list of passes that should be enabled. By default, all passes are enabled. If a pass is prefixed with `no-`, it will be disabled
|
||||||
--script-path <SCRIPT_PATH>
|
--script-path <SCRIPT_PATH>
|
||||||
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. For lints, the `MINIMIZE_LINTS` environment variable will be set to `1`. The first line of the lint stdout or stderr can be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
|
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. For lints, the `MINIMIZE_LINTS` environment variable will be set to `1`. The first line of the lint stdout or stderr can be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
|
||||||
--script-path-lints <SCRIPT_PATH_LINTS>
|
--script-path-lints <SCRIPT_PATH_LINTS>
|
||||||
A path to a script that is run to get lints. The first line of stdout or stderr must be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
|
A path to a script that is run to get lints. The first line of stdout or stderr must be `minimize-fmt-rustc` or `minimize-fmt-cargo` to show whether the rustc or wrapper cargo lint format and which output stream is used. Defaults to cargo and stdout
|
||||||
|
--ignore-file <IGNORE_FILE>
|
||||||
|
Do not touch the following files
|
||||||
|
--bisect-delete-imports
|
||||||
|
Remove individual use statements manually, instead of relying on rustc lints output
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help information
|
Print help
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: You can safely press `Ctrl-C` when running cargo-minimize. It will rollback the current minimization attempt and give you the latest known-reproducing state.
|
Note: You can safely press `Ctrl-C` when running cargo-minimize. It will rollback the current minimization attempt and give you the latest known-reproducing state.
|
||||||
|
|
|
||||||
12
flake.lock
generated
12
flake.lock
generated
|
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1694529238,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1695145219,
|
"lastModified": 1744932701,
|
||||||
"narHash": "sha256-Eoe9IHbvmo5wEDeJXKFOpKUwxYJIOxKUesounVccNYk=",
|
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5ba549eafcf3e33405e5f66decd1a72356632b96",
|
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
8
full-tests/nested-items.rs
Normal file
8
full-tests/nested-items.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub mod foo {
|
||||||
|
/// ~MINIMIZE-ROOT good
|
||||||
|
pub fn good(){}
|
||||||
|
/// ~REQUIRE-DELETED bad
|
||||||
|
pub fn bad(){}
|
||||||
|
}
|
||||||
|
/// ~MINIMIZE-ROOT main
|
||||||
|
fn main(){}
|
||||||
24
full-tests/nested-items2.rs
Normal file
24
full-tests/nested-items2.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// this should all get deleted in a single swoop *and* not panic about it
|
||||||
|
/// ~REQUIRE-DELETED l1
|
||||||
|
mod l1 {
|
||||||
|
mod l2 {
|
||||||
|
mod l3 {
|
||||||
|
mod l4{
|
||||||
|
mod l5 {
|
||||||
|
fn foo(){}
|
||||||
|
fn bar(){}
|
||||||
|
mod l6 {
|
||||||
|
fn x1(){}
|
||||||
|
}
|
||||||
|
fn x2(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod l4_2 {
|
||||||
|
fn y(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn x8(){}
|
||||||
|
}
|
||||||
|
/// ~MINIMIZE-ROOT main
|
||||||
|
fn main(){}
|
||||||
12
full-tests/reexports.rs
Normal file
12
full-tests/reexports.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
use hello::{thingy, whatever};
|
||||||
|
mod hello{
|
||||||
|
pub fn thingy(){}
|
||||||
|
/// ~REQUIRE-DELETED whatever
|
||||||
|
pub fn whatever(){}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(){
|
||||||
|
"~MINIMIZE-ROOT let x = thingy";
|
||||||
|
let x = thingy();
|
||||||
|
}
|
||||||
15
full-tests/reexports2.rs
Normal file
15
full-tests/reexports2.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
mod A {
|
||||||
|
/// ~REQUIRE-DELETED S1
|
||||||
|
pub struct S1;
|
||||||
|
pub struct S2;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod B {
|
||||||
|
use crate::A::{self, S1};
|
||||||
|
pub use A::S2 as thingy;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
"~MINIMIZE-ROOT let x = B::thingy";
|
||||||
|
let x = B::thingy;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::{bail, ensure, Context, Result};
|
use anyhow::{Context, Result, bail, ensure};
|
||||||
use rustfix::diagnostics::Diagnostic;
|
use rustfix::diagnostics::Diagnostic;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -10,7 +10,7 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{dylib_flag::RustFunction, EnvVar, Options};
|
use crate::{EnvVar, Options, dylib_flag::RustFunction};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Build {
|
pub struct Build {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ impl FromStr for RustFunction {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Self::compile(s).context("compiling and loading rust function")
|
Self::compile(s)
|
||||||
|
.map_err(|e| anyhow::format_err!("compiling and loading rust function: {:?}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
// this code is pretty neat i guess but i dont have a use for it right now
|
// this code is pretty neat i guess but i dont have a use for it right now
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use cargo::{
|
use cargo::{
|
||||||
core::{
|
core::{
|
||||||
|
Workspace,
|
||||||
compiler::{BuildContext, Unit, UnitInterner},
|
compiler::{BuildContext, Unit, UnitInterner},
|
||||||
manifest::TargetSourcePath,
|
manifest::TargetSourcePath,
|
||||||
Workspace,
|
|
||||||
},
|
},
|
||||||
ops::{self, CompileOptions},
|
ops::{self, CompileOptions},
|
||||||
util::{command_prelude::CompileMode, Config},
|
util::{Config, command_prelude::CompileMode},
|
||||||
};
|
};
|
||||||
use std::{collections::BTreeSet, fmt::Debug, ops::Not, path::Path, process::Command};
|
use std::{collections::BTreeSet, fmt::Debug, ops::Not, path::Path, process::Command};
|
||||||
use syn::{visit_mut::VisitMut, File, Item, ItemExternCrate, ItemMod, ItemUse, Visibility};
|
use syn::{File, Item, ItemExternCrate, ItemMod, ItemUse, Visibility, visit_mut::VisitMut};
|
||||||
|
|
||||||
fn cargo_expand(cargo_dir: &TargetSourcePath) -> Result<syn::File> {
|
fn cargo_expand(cargo_dir: &TargetSourcePath) -> Result<syn::File> {
|
||||||
let cargo_dir = cargo_dir
|
let cargo_dir = cargo_dir
|
||||||
|
|
|
||||||
18
src/lib.rs
18
src/lib.rs
|
|
@ -4,7 +4,7 @@ extern crate tracing;
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{Arc, atomic::AtomicBool},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
|
|
@ -15,14 +15,15 @@ mod processor;
|
||||||
|
|
||||||
pub use build::rustup_which;
|
pub use build::rustup_which;
|
||||||
|
|
||||||
#[cfg(this_pulls_in_cargo_which_is_a_big_dep_i_dont_like_it)]
|
// this experimental and doesnt really work
|
||||||
|
#[cfg(any())]
|
||||||
mod expand;
|
mod expand;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use dylib_flag::RustFunction;
|
use dylib_flag::RustFunction;
|
||||||
use processor::Minimizer;
|
use processor::{Minimizer, PassSelection};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
|
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
use crate::processor::Pass;
|
use crate::processor::Pass;
|
||||||
|
|
||||||
|
|
@ -82,8 +83,9 @@ pub struct Options {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// A comma-seperated list of passes that should be enabled. By default, all passes are enabled.
|
/// A comma-seperated list of passes that should be enabled. By default, all passes are enabled.
|
||||||
|
/// If a pass is prefixed with `no-`, it will be disabled.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub passes: Option<String>,
|
pub passes: Option<PassSelection>,
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|
@ -105,6 +107,10 @@ pub struct Options {
|
||||||
|
|
||||||
#[arg(skip)]
|
#[arg(skip)]
|
||||||
pub no_delete_functions: bool,
|
pub no_delete_functions: bool,
|
||||||
|
|
||||||
|
/// Remove individual use statements manually, instead of relying on rustc lints output
|
||||||
|
#[arg(long)]
|
||||||
|
pub bisect_delete_imports: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -142,6 +148,7 @@ pub fn minimize(options: Options, stop: Arc<AtomicBool>) -> Result<()> {
|
||||||
|
|
||||||
minimizer.run_passes([
|
minimizer.run_passes([
|
||||||
passes::EverybodyLoops::default().boxed(),
|
passes::EverybodyLoops::default().boxed(),
|
||||||
|
passes::SplitUse::default().boxed(),
|
||||||
passes::FieldDeleter::default().boxed(),
|
passes::FieldDeleter::default().boxed(),
|
||||||
passes::Privatize::default().boxed(),
|
passes::Privatize::default().boxed(),
|
||||||
])?;
|
])?;
|
||||||
|
|
@ -186,6 +193,7 @@ impl Default for Options {
|
||||||
script_path_lints: None,
|
script_path_lints: None,
|
||||||
ignore_file: Vec::new(),
|
ignore_file: Vec::new(),
|
||||||
no_delete_functions: false,
|
no_delete_functions: false,
|
||||||
|
bisect_delete_imports: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
extern crate tracing;
|
extern crate tracing;
|
||||||
|
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use cargo_minimize::{Cargo, Parser};
|
use cargo_minimize::{Cargo, Parser};
|
||||||
use tracing::{error, Level};
|
use tracing::{Level, error};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let Cargo::Minimize(options) = Cargo::parse();
|
let Cargo::Minimize(options) = Cargo::parse();
|
||||||
|
|
|
||||||
|
|
@ -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::{tracking, Pass, PassController, ProcessState, SourceFile};
|
use crate::processor::{Pass, PassController, ProcessState, SourceFile, tracking};
|
||||||
|
|
||||||
struct Visitor<'a> {
|
struct Visitor<'a> {
|
||||||
current_path: Vec<String>,
|
current_path: Vec<String>,
|
||||||
|
|
@ -25,9 +25,11 @@ impl<'a> Visitor<'a> {
|
||||||
impl VisitMut for Visitor<'_> {
|
impl VisitMut for Visitor<'_> {
|
||||||
fn visit_block_mut(&mut self, block: &mut syn::Block) {
|
fn visit_block_mut(&mut self, block: &mut syn::Block) {
|
||||||
match block.stmts.as_slice() {
|
match block.stmts.as_slice() {
|
||||||
[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.
|
// 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) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{visit_mut::VisitMut, Fields};
|
use syn::{Fields, visit_mut::VisitMut};
|
||||||
|
|
||||||
use crate::processor::{tracking, Pass, PassController, ProcessState, SourceFile};
|
use crate::processor::{Pass, PassController, ProcessState, SourceFile, tracking};
|
||||||
|
|
||||||
struct Visitor<'a> {
|
struct Visitor<'a> {
|
||||||
current_path: Vec<String>,
|
current_path: Vec<String>,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{
|
use syn::{
|
||||||
visit_mut::VisitMut, Item, ItemConst, ItemEnum, ItemExternCrate, ItemFn, ItemMacro, ItemMacro2,
|
Item, ItemConst, ItemEnum, ItemExternCrate, ItemFn, ItemMacro, ItemMacro2, ItemMod, ItemStatic,
|
||||||
ItemMod, ItemStatic, ItemStruct, ItemTrait, ItemTraitAlias, ItemType, ItemUnion, Signature,
|
ItemStruct, ItemTrait, ItemTraitAlias, ItemType, ItemUnion, ItemUse, Signature,
|
||||||
|
visit_mut::VisitMut,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::processor::{tracking, Pass, PassController, ProcessState, SourceFile};
|
use crate::processor::{Pass, PassController, ProcessState, SourceFile, tracking};
|
||||||
|
|
||||||
struct Visitor<'a> {
|
struct Visitor<'a> {
|
||||||
current_path: Vec<String>,
|
current_path: Vec<String>,
|
||||||
|
|
@ -73,9 +74,17 @@ impl<'a> Visitor<'a> {
|
||||||
self.current_path.pop();
|
self.current_path.pop();
|
||||||
should_retain
|
should_retain
|
||||||
}
|
}
|
||||||
|
// We would hope for the unused imports pass to catch all of these
|
||||||
|
// but sadly that's not the case
|
||||||
|
Item::Use(ItemUse { tree, .. }) if self.checker.options.bisect_delete_imports => {
|
||||||
|
self.current_path.push(tree.to_token_stream().to_string());
|
||||||
|
|
||||||
|
let should_retain = self.should_retain_item();
|
||||||
|
|
||||||
|
self.current_path.pop();
|
||||||
|
should_retain
|
||||||
|
}
|
||||||
Item::ForeignMod(_) => true,
|
Item::ForeignMod(_) => true,
|
||||||
// We hope for the unused imports to show them all.
|
|
||||||
Item::Use(_) => true,
|
|
||||||
Item::Verbatim(_) => true,
|
Item::Verbatim(_) => true,
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ mod everybody_loops;
|
||||||
mod field_deleter;
|
mod field_deleter;
|
||||||
mod item_deleter;
|
mod item_deleter;
|
||||||
mod privatize;
|
mod privatize;
|
||||||
|
mod split_use;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
everybody_loops::EverybodyLoops, field_deleter::FieldDeleter, item_deleter::ItemDeleter,
|
everybody_loops::EverybodyLoops, field_deleter::FieldDeleter, item_deleter::ItemDeleter,
|
||||||
privatize::Privatize,
|
privatize::Privatize, split_use::SplitUse,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{parse_quote, visit_mut::VisitMut, Visibility};
|
use syn::{Visibility, parse_quote, visit_mut::VisitMut};
|
||||||
|
|
||||||
use crate::processor::{tracking, Pass, PassController, ProcessState, SourceFile};
|
use crate::processor::{Pass, PassController, ProcessState, SourceFile, tracking};
|
||||||
|
|
||||||
struct Visitor<'a> {
|
struct Visitor<'a> {
|
||||||
pub_crate: Visibility,
|
pub_crate: Visibility,
|
||||||
|
|
@ -24,12 +24,32 @@ impl<'a> Visitor<'a> {
|
||||||
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.current_path.push("{{vis}}".to_string());
|
||||||
if self.checker.can_process(&self.current_path) {
|
if self.checker.can_process(&self.current_path) {
|
||||||
self.process_state = ProcessState::Changed;
|
self.process_state = ProcessState::Changed;
|
||||||
*vis = self.pub_crate.clone();
|
*vis = self.pub_crate.clone();
|
||||||
}
|
}
|
||||||
|
self.current_path.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn visit_item_mut(&mut self, item: &mut syn::Item) {
|
||||||
|
match item {
|
||||||
|
syn::Item::Use(u) => {
|
||||||
|
if let Visibility::Public(_) = u.vis {
|
||||||
|
let mut path = self.current_path.clone();
|
||||||
|
path.push(u.to_token_stream().to_string());
|
||||||
|
if self.checker.can_process(&path) {
|
||||||
|
self.process_state = ProcessState::Changed;
|
||||||
|
u.vis = self.pub_crate.clone();
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
return; // early return; do not walk the child items
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
syn::visit_mut::visit_item_mut(self, item);
|
||||||
|
}
|
||||||
|
|
||||||
tracking!();
|
tracking!();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
181
src/passes/split_use.rs
Normal file
181
src/passes/split_use.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
|
use crate::processor::{Pass, PassController, ProcessState, SourceFile, tracking};
|
||||||
|
use quote::ToTokens;
|
||||||
|
|
||||||
|
use syn::{Item, ItemUse, UseName, UsePath, UseRename, UseTree, visit_mut::VisitMut};
|
||||||
|
|
||||||
|
struct Visitor<'a> {
|
||||||
|
process_state: ProcessState,
|
||||||
|
current_path: Vec<String>,
|
||||||
|
checker: &'a mut PassController,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> {
|
||||||
|
fn new(checker: &'a mut PassController) -> Self {
|
||||||
|
Self {
|
||||||
|
process_state: ProcessState::NoChange,
|
||||||
|
current_path: Vec::new(),
|
||||||
|
checker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a "some::group::{a, b::{c,d}, e}" tree, and assuming checker allows processing of (only) "some::group",
|
||||||
|
// returns a ["some::group::a", "some::group::b::{c,d}", "some::group::e"] list of trees.
|
||||||
|
fn expand_use_groups(&mut self, top: &syn::ItemUse, tree: &UseTree) -> Vec<UseTree> {
|
||||||
|
// It would probably be nice if instead of *expanding* the whole "some::group" group, we could instead
|
||||||
|
// *extract* individual items ("some::group::a"), but that makes code much more convoluted, sadly
|
||||||
|
match tree {
|
||||||
|
UseTree::Path(p) => {
|
||||||
|
self.current_path.push(p.ident.to_string());
|
||||||
|
|
||||||
|
let out = self
|
||||||
|
.expand_use_groups(top, &p.tree)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
let mut new = p.clone();
|
||||||
|
new.tree = Box::new(x);
|
||||||
|
UseTree::Path(new)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.current_path.pop();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
UseTree::Group(g) => {
|
||||||
|
let new_trees = g
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|subtree| self.expand_use_groups(top, subtree))
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.current_path.push("{{group}}".to_string());
|
||||||
|
let can_process = self.checker.can_process(&self.current_path);
|
||||||
|
self.current_path.pop();
|
||||||
|
|
||||||
|
if can_process {
|
||||||
|
self.process_state = ProcessState::Changed;
|
||||||
|
return new_trees;
|
||||||
|
} else {
|
||||||
|
// Do not expand the group.
|
||||||
|
// recreate the UseTree::Group item (but with new subtrees), and return a single-element list
|
||||||
|
let mut g = g.clone();
|
||||||
|
g.items.clear();
|
||||||
|
g.items.extend(new_trees);
|
||||||
|
return vec![syn::UseTree::Group(g)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return vec![tree.clone()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_item_list(&mut self, items: &mut Vec<syn::Item>) {
|
||||||
|
let mut pos = 0; // index into the `items` list
|
||||||
|
while pos < items.len() {
|
||||||
|
let item_use: ItemUse = {
|
||||||
|
match &items[pos] {
|
||||||
|
Item::Use(u) => u.clone(),
|
||||||
|
_ => {
|
||||||
|
pos += 1; // if it's not a `use`` - simply advance to the next item
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_use_trees = self.expand_use_groups(&item_use, &item_use.tree);
|
||||||
|
// decorate each of the UseTree with a `use` keyword (and any attributes inherited)
|
||||||
|
let new_uses = new_use_trees.into_iter().map(|x| {
|
||||||
|
let mut new = item_use.clone();
|
||||||
|
new.tree = x;
|
||||||
|
trim_trailing_self(&mut new.tree);
|
||||||
|
syn::Item::Use(new)
|
||||||
|
});
|
||||||
|
|
||||||
|
let step = new_uses.len();
|
||||||
|
// replace the old use with the new uses
|
||||||
|
items.splice(pos..pos + 1, new_uses);
|
||||||
|
pos += step; // do not process freshly inserted items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is legal to write "use module::{self};", but not "use module::self;".
|
||||||
|
// If we do end up with the latter on our hands, convert it to "use module;" instead.
|
||||||
|
fn trim_trailing_self(use_tree: &mut UseTree) {
|
||||||
|
match use_tree {
|
||||||
|
UseTree::Path(UsePath {
|
||||||
|
tree: subtree,
|
||||||
|
ident: base_ident,
|
||||||
|
..
|
||||||
|
}) => match subtree.deref_mut() {
|
||||||
|
UseTree::Name(UseName { ident: sub_ident }) => {
|
||||||
|
if sub_ident == "self" {
|
||||||
|
*use_tree = UseTree::Name(UseName {
|
||||||
|
ident: base_ident.clone(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UseTree::Rename(syn::UseRename {
|
||||||
|
ident: sub_ident,
|
||||||
|
rename,
|
||||||
|
as_token,
|
||||||
|
}) => {
|
||||||
|
if sub_ident == "self" {
|
||||||
|
*use_tree = UseTree::Rename(UseRename {
|
||||||
|
ident: base_ident.clone(),
|
||||||
|
rename: rename.clone(),
|
||||||
|
as_token: as_token.clone(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UseTree::Path(_) => trim_trailing_self(&mut *subtree),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisitMut for Visitor<'_> {
|
||||||
|
fn visit_item_mod_mut(&mut self, item_mod: &mut syn::ItemMod) {
|
||||||
|
self.current_path.push(item_mod.ident.to_string());
|
||||||
|
if let Some((_, items)) = &mut item_mod.content {
|
||||||
|
self.visit_item_list(items);
|
||||||
|
}
|
||||||
|
syn::visit_mut::visit_item_mod_mut(self, item_mod);
|
||||||
|
self.current_path.pop();
|
||||||
|
}
|
||||||
|
fn visit_file_mut(&mut self, file: &mut syn::File) {
|
||||||
|
self.visit_item_list(&mut file.items);
|
||||||
|
syn::visit_mut::visit_file_mut(self, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracking!(visit_item_fn_mut);
|
||||||
|
tracking!(visit_impl_item_method_mut);
|
||||||
|
tracking!(visit_item_impl_mut);
|
||||||
|
tracking!(visit_field_mut);
|
||||||
|
tracking!(visit_item_struct_mut);
|
||||||
|
tracking!(visit_item_trait_mut);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SplitUse {}
|
||||||
|
|
||||||
|
impl Pass for SplitUse {
|
||||||
|
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 {
|
||||||
|
"split-use"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,14 @@ use self::worklist::Worklist;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
struct AstPath(Vec<String>);
|
struct AstPath(Vec<String>);
|
||||||
|
impl AstPath {
|
||||||
|
fn has_prefix(&self, other: &AstPath) -> bool {
|
||||||
|
if self.0.len() < other.0.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::iter::zip(self.0.iter(), other.0.iter()).all(|(a, b)| a == b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Borrow<[String]> for AstPath {
|
impl Borrow<[String]> for AstPath {
|
||||||
fn borrow(&self) -> &[String] {
|
fn borrow(&self) -> &[String] {
|
||||||
|
|
@ -75,6 +83,20 @@ mod worklist {
|
||||||
pub(super) fn pop(&mut self) -> Option<Vec<AstPath>> {
|
pub(super) fn pop(&mut self) -> Option<Vec<AstPath>> {
|
||||||
self.0.pop()
|
self.0.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove all the worklist items that would have been covered by the
|
||||||
|
// given ones.
|
||||||
|
// I.e. if we have already deleted the entire module, there's no need
|
||||||
|
// trying to delete that module's individual items anymore
|
||||||
|
pub(super) fn prune(&mut self, things: &std::collections::BTreeSet<AstPath>) {
|
||||||
|
for wl in &mut self.0 {
|
||||||
|
wl.retain(|path| {
|
||||||
|
// retain only if none of the things are a prefix of this path
|
||||||
|
things.iter().all(|thing| !path.has_prefix(thing))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.0.retain(|wl| !wl.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,8 +119,9 @@ impl PassController {
|
||||||
committed,
|
committed,
|
||||||
failed: _,
|
failed: _,
|
||||||
current,
|
current,
|
||||||
worklist: _,
|
worklist,
|
||||||
} => {
|
} => {
|
||||||
|
worklist.prune(current);
|
||||||
committed.extend(mem::take(current));
|
committed.extend(mem::take(current));
|
||||||
|
|
||||||
self.next_in_worklist();
|
self.next_in_worklist();
|
||||||
|
|
@ -110,19 +133,10 @@ impl PassController {
|
||||||
/// The changes did not reproduce the regression. Bisect further.
|
/// The changes did not reproduce the regression. Bisect further.
|
||||||
pub fn does_not_reproduce(&mut self) {
|
pub fn does_not_reproduce(&mut self) {
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
PassControllerState::InitialCollection { candidates } => {
|
PassControllerState::InitialCollection { candidates: _ } => {
|
||||||
// Applying them all was too much, let's bisect!
|
unreachable!(
|
||||||
let (current, first_worklist_item) = split_owned(mem::take(candidates));
|
"we should have made no changes on initial collection, what do you mean it does not reproduce?!?"
|
||||||
|
)
|
||||||
let mut worklist = Worklist::new();
|
|
||||||
worklist.push(first_worklist_item);
|
|
||||||
|
|
||||||
self.state = PassControllerState::Bisecting {
|
|
||||||
committed: BTreeSet::new(),
|
|
||||||
failed: BTreeSet::new(),
|
|
||||||
current,
|
|
||||||
worklist,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
PassControllerState::Bisecting {
|
PassControllerState::Bisecting {
|
||||||
committed,
|
committed,
|
||||||
|
|
@ -155,18 +169,36 @@ impl PassController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pass did not apply any changes. We're done.
|
/// The pass did not apply any changes. We're either done or just starting
|
||||||
pub fn no_change(&mut self) {
|
pub fn no_change(&mut self) {
|
||||||
match &self.state {
|
match &mut self.state {
|
||||||
PassControllerState::InitialCollection { candidates } => {
|
PassControllerState::InitialCollection { candidates } => {
|
||||||
assert!(
|
if candidates.is_empty() {
|
||||||
candidates.is_empty(),
|
|
||||||
"No change but received candidates. The responsible pass does not seem to track the ProcessState correctly: {candidates:?}"
|
|
||||||
);
|
|
||||||
self.state = PassControllerState::Success;
|
self.state = PassControllerState::Success;
|
||||||
|
} else {
|
||||||
|
// We could just set `current=candidates; worklist=default()`,
|
||||||
|
// but we are doing a minor optimization to split overlapping items into separate tries,
|
||||||
|
// so as to reduce the number of bisection steps that yield literally no new info.
|
||||||
|
let layers = layer_candidates(mem::take(candidates));
|
||||||
|
let mut worklist = Worklist::new();
|
||||||
|
for layer in layers.into_iter().rev() {
|
||||||
|
// .rev() so that we add shorter paths last, and process them first
|
||||||
|
worklist.push(layer);
|
||||||
|
}
|
||||||
|
let current = worklist.pop().unwrap().into_iter().collect();
|
||||||
|
|
||||||
|
self.state = PassControllerState::Bisecting {
|
||||||
|
committed: BTreeSet::new(),
|
||||||
|
failed: BTreeSet::new(),
|
||||||
|
current,
|
||||||
|
worklist,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PassControllerState::Bisecting { current, .. } => {
|
PassControllerState::Bisecting { current, .. } => {
|
||||||
unreachable!("Pass said it didn't change anything in the bisection phase, nils forgot what this means: {current:?}");
|
unreachable!(
|
||||||
|
"Pass said it didn't change anything in the bisection phase, nora forgot what this means: {current:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
PassControllerState::Success { .. } => {}
|
PassControllerState::Success { .. } => {}
|
||||||
}
|
}
|
||||||
|
|
@ -184,9 +216,9 @@ impl PassController {
|
||||||
pub fn can_process(&mut self, path: &[String]) -> bool {
|
pub fn can_process(&mut self, path: &[String]) -> bool {
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
PassControllerState::InitialCollection { candidates } => {
|
PassControllerState::InitialCollection { candidates } => {
|
||||||
// For the initial collection, we collect the candidate and apply them all.
|
// For the initial collection, we collect the candidate but don't apply anything
|
||||||
candidates.push(AstPath(path.to_owned()));
|
candidates.push(AstPath(path.to_owned()));
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
PassControllerState::Bisecting { current, .. } => current.contains(path),
|
PassControllerState::Bisecting { current, .. } => current.contains(path),
|
||||||
PassControllerState::Success { .. } => {
|
PassControllerState::Success { .. } => {
|
||||||
|
|
@ -205,6 +237,7 @@ impl PassController {
|
||||||
match worklist.pop() {
|
match worklist.pop() {
|
||||||
Some(next) => {
|
Some(next) => {
|
||||||
*current = next.into_iter().collect();
|
*current = next.into_iter().collect();
|
||||||
|
trace!(?current, "current working set: ");
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.state = PassControllerState::Success;
|
self.state = PassControllerState::Success;
|
||||||
|
|
@ -218,11 +251,7 @@ impl PassController {
|
||||||
pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
|
pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
|
||||||
let d = lhs / rhs;
|
let d = lhs / rhs;
|
||||||
let r = lhs % rhs;
|
let r = lhs % rhs;
|
||||||
if r > 0 && rhs > 0 {
|
if r > 0 && rhs > 0 { d + 1 } else { d }
|
||||||
d + 1
|
|
||||||
} else {
|
|
||||||
d
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits an owned container in half.
|
/// Splits an owned container in half.
|
||||||
|
|
@ -239,3 +268,33 @@ fn split_owned<T, From: IntoIterator<Item = T>, A: FromIterator<T>, B: FromItera
|
||||||
|
|
||||||
(first_half, second_half)
|
(first_half, second_half)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split up a list of AstPath's into several sets such that none of those sets have
|
||||||
|
/// any of the AstPath's overlap.
|
||||||
|
/// I.e. so that we avoid having both ["foo"] and ["foo", "bar"] in the same set.
|
||||||
|
/// It is expected, but not guaranteed that the earlier sets would contain "less granular" items
|
||||||
|
/// (i.e. ["foo"] from the above example) and the latter sets would contain the "more granular" ones.
|
||||||
|
fn layer_candidates(mut candidates: Vec<AstPath>) -> Vec<Vec<AstPath>> {
|
||||||
|
candidates.sort(); // this *should* put less-granular/shorter-path items first
|
||||||
|
let mut layers: Vec<Vec<AstPath>> = vec![];
|
||||||
|
for candidate in candidates {
|
||||||
|
let mut appropriate_layer_no = None;
|
||||||
|
for (no, layer) in layers.iter().enumerate() {
|
||||||
|
if !layer
|
||||||
|
.iter()
|
||||||
|
.any(|known_path| candidate.has_prefix(known_path))
|
||||||
|
{
|
||||||
|
appropriate_layer_no = Some(no);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match appropriate_layer_no {
|
||||||
|
Some(no) => layers[no].push(candidate),
|
||||||
|
None => {
|
||||||
|
let new_layer = vec![candidate];
|
||||||
|
layers.push(new_layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layers
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ mod files;
|
||||||
mod reaper;
|
mod reaper;
|
||||||
|
|
||||||
pub(crate) use self::files::SourceFile;
|
pub(crate) use self::files::SourceFile;
|
||||||
use crate::{build::Build, processor::files::Changes, Options};
|
use crate::{Options, build::Build, processor::files::Changes};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::{collections::HashSet, ffi::OsStr, fmt::Debug, sync::atomic::AtomicBool};
|
use std::{collections::HashSet, ffi::OsStr, fmt::Debug, sync::atomic::AtomicBool};
|
||||||
|
|
||||||
pub(crate) use self::checker::PassController;
|
pub(crate) use self::checker::PassController;
|
||||||
|
|
@ -19,7 +19,7 @@ pub(crate) trait Pass {
|
||||||
|
|
||||||
/// Process a file. The state of the processor might get invalidated in the process as signaled with
|
/// 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`
|
/// `ProcessState::FileInvalidated`. When a file is invalidated, the minimizer will call `Processor::refersh_state`
|
||||||
/// before calling the this function on the same file again.
|
/// before calling this function on the same file again.
|
||||||
fn process_file(
|
fn process_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
krate: &mut syn::File,
|
krate: &mut syn::File,
|
||||||
|
|
@ -50,6 +50,33 @@ pub(crate) enum ProcessState {
|
||||||
FileInvalidated,
|
FileInvalidated,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PassSelection {
|
||||||
|
Enable(Vec<String>),
|
||||||
|
Disable(Vec<String>),
|
||||||
|
}
|
||||||
|
impl std::str::FromStr for PassSelection {
|
||||||
|
type Err = &'static str;
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
let values = s.split(',').collect::<Vec<_>>();
|
||||||
|
let have_negative = values.iter().any(|v| v.starts_with("no-"));
|
||||||
|
if have_negative && !values.iter().all(|v| v.starts_with("no-")) {
|
||||||
|
return Err(
|
||||||
|
"Pass exclusion is supported, by mixing positive pass selection with negative is not allowed (because it's pointless and confusing)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let actual_values = values
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.strip_prefix("no-").unwrap_or(v).to_string())
|
||||||
|
.collect();
|
||||||
|
if !have_negative {
|
||||||
|
Ok(PassSelection::Enable(actual_values))
|
||||||
|
} else {
|
||||||
|
Ok(PassSelection::Disable(actual_values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Minimizer {
|
pub(crate) struct Minimizer {
|
||||||
files: Vec<SourceFile>,
|
files: Vec<SourceFile>,
|
||||||
|
|
@ -59,14 +86,13 @@ pub(crate) struct Minimizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Minimizer {
|
impl Minimizer {
|
||||||
fn pass_disabled(&self, name: &str) -> bool {
|
fn pass_enabled(&self, name: &str) -> bool {
|
||||||
if let Some(passes) = &self.options.passes {
|
match &self.options.passes {
|
||||||
if !passes.split(",").any(|allowed| name == allowed) {
|
None => true,
|
||||||
return true;
|
Some(PassSelection::Enable(v)) => v.iter().any(|allowed| name == allowed),
|
||||||
|
Some(PassSelection::Disable(v)) => v.iter().all(|forbidden| name != forbidden),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new_glob_dir(
|
pub(crate) fn new_glob_dir(
|
||||||
options: Options,
|
options: Options,
|
||||||
|
|
@ -131,7 +157,7 @@ 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()) {
|
if !self.pass_enabled(pass.name()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.run_pass(&mut *pass)?;
|
self.run_pass(&mut *pass)?;
|
||||||
|
|
@ -187,6 +213,7 @@ impl Minimizer {
|
||||||
// The logic for bisecting down lives in PassController.
|
// The logic for bisecting down lives in PassController.
|
||||||
|
|
||||||
let mut checker = PassController::new(self.options.clone());
|
let mut checker = PassController::new(self.options.clone());
|
||||||
|
let mut initial_pass = true;
|
||||||
loop {
|
loop {
|
||||||
let mut change = file.try_change(changes)?;
|
let mut change = file.try_change(changes)?;
|
||||||
let (_, krate) = change.before_content();
|
let (_, krate) = change.before_content();
|
||||||
|
|
@ -203,24 +230,27 @@ impl Minimizer {
|
||||||
if after.reproduces_issue() {
|
if after.reproduces_issue() {
|
||||||
change.commit();
|
change.commit();
|
||||||
checker.reproduces();
|
checker.reproduces();
|
||||||
|
if has_made_change == ProcessState::FileInvalidated {
|
||||||
|
invalidated_files.insert(file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
change.rollback()?;
|
change.rollback()?;
|
||||||
checker.does_not_reproduce();
|
checker.does_not_reproduce();
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_made_change == ProcessState::FileInvalidated {
|
|
||||||
invalidated_files.insert(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ProcessState::NoChange => {
|
ProcessState::NoChange => {
|
||||||
|
if !initial_pass {
|
||||||
if self.options.no_color {
|
if self.options.no_color {
|
||||||
info!("{file:?}: After {}: no changes", pass.name());
|
info!("{file:?}: After {}: no changes", pass.name());
|
||||||
} else {
|
} else {
|
||||||
info!("{file:?}: After {}: {}", pass.name(), "no changes".yellow());
|
info!("{file:?}: After {}: {}", pass.name(), "no changes".yellow());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
checker.no_change();
|
checker.no_change();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
initial_pass = false;
|
||||||
|
|
||||||
if self.cancel.load(Ordering::SeqCst) {
|
if self.cancel.load(Ordering::SeqCst) {
|
||||||
info!("Exiting early.");
|
info!("Exiting early.");
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,13 @@
|
||||||
|
|
||||||
use crate::build::Build;
|
use crate::build::Build;
|
||||||
|
|
||||||
use super::{files::Changes, tracking, Minimizer, Pass, PassController, ProcessState, SourceFile};
|
use super::{Minimizer, Pass, PassController, ProcessState, SourceFile, files::Changes, tracking};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use rustfix::{diagnostics::Diagnostic, Suggestion};
|
use rustfix::{Suggestion, diagnostics::Diagnostic};
|
||||||
use std::{
|
use std::{collections::HashMap, ops::Range, path::Path};
|
||||||
collections::{HashMap, HashSet},
|
use syn::{ImplItem, Item, visit_mut::VisitMut};
|
||||||
ops::Range,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use syn::{visit_mut::VisitMut, ImplItem, Item};
|
|
||||||
|
|
||||||
fn file_for_suggestion(suggestion: &Suggestion) -> &Path {
|
fn file_for_suggestion(suggestion: &Suggestion) -> &Path {
|
||||||
Path::new(&suggestion.solutions[0].replacements[0].snippet.file_name)
|
Path::new(&suggestion.solutions[0].replacements[0].snippet.file_name)
|
||||||
|
|
@ -22,7 +18,7 @@ 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) {
|
if !self.pass_enabled(PASS_NAME) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,9 +75,17 @@ impl Minimizer {
|
||||||
.copied()
|
.copied()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
if desired_suggestions.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let result =
|
let result =
|
||||||
rustfix::apply_suggestions(change.before_content().0, &desired_suggestions)?;
|
rustfix::apply_suggestions(change.before_content().0, &desired_suggestions)?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
result != change.before_content().0,
|
||||||
|
"Suggestions 'applied' but no changes made??"
|
||||||
|
);
|
||||||
|
|
||||||
let result = syn::parse_file(&result).context("parsing file after rustfix")?;
|
let result = syn::parse_file(&result).context("parsing file after rustfix")?;
|
||||||
change.write(result)?;
|
change.write(result)?;
|
||||||
|
|
||||||
|
|
@ -103,16 +107,11 @@ impl Minimizer {
|
||||||
struct DeleteUnusedFunctions {
|
struct DeleteUnusedFunctions {
|
||||||
diags: Vec<Diagnostic>,
|
diags: Vec<Diagnostic>,
|
||||||
build: Build,
|
build: Build,
|
||||||
invalid: HashSet<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeleteUnusedFunctions {
|
impl DeleteUnusedFunctions {
|
||||||
fn new(build: Build, diags: Vec<Diagnostic>) -> Self {
|
fn new(build: Build, diags: Vec<Diagnostic>) -> Self {
|
||||||
DeleteUnusedFunctions {
|
DeleteUnusedFunctions { diags, build }
|
||||||
diags,
|
|
||||||
build,
|
|
||||||
invalid: HashSet::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +119,6 @@ impl Pass for DeleteUnusedFunctions {
|
||||||
fn refresh_state(&mut self) -> Result<()> {
|
fn refresh_state(&mut self) -> Result<()> {
|
||||||
let (diags, _) = self.build.get_diags().context("getting diagnostics")?;
|
let (diags, _) = self.build.get_diags().context("getting diagnostics")?;
|
||||||
self.diags = diags;
|
self.diags = diags;
|
||||||
self.invalid.clear();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,18 +128,9 @@ impl Pass for DeleteUnusedFunctions {
|
||||||
file: &SourceFile,
|
file: &SourceFile,
|
||||||
checker: &mut super::PassController,
|
checker: &mut super::PassController,
|
||||||
) -> ProcessState {
|
) -> ProcessState {
|
||||||
assert!(
|
|
||||||
!self.invalid.contains(file.path_no_fs_interact()),
|
|
||||||
"processing with invalid state"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut visitor = FindUnusedFunction::new(file, self.diags.iter(), checker);
|
let mut visitor = FindUnusedFunction::new(file, self.diags.iter(), checker);
|
||||||
visitor.visit_file_mut(krate);
|
visitor.visit_file_mut(krate);
|
||||||
|
|
||||||
if visitor.process_state == ProcessState::FileInvalidated {
|
|
||||||
self.invalid.insert(file.path_no_fs_interact().to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
visitor.process_state
|
visitor.process_state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,8 +224,12 @@ impl<'a> FindUnusedFunction<'a> {
|
||||||
match span_matches {
|
match span_matches {
|
||||||
0 => true,
|
0 => true,
|
||||||
1 => {
|
1 => {
|
||||||
|
if self.checker.can_process(&self.current_path) {
|
||||||
self.process_state = ProcessState::FileInvalidated;
|
self.process_state = ProcessState::FileInvalidated;
|
||||||
!self.checker.can_process(&self.current_path)
|
!self.checker.can_process(&self.current_path)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("multiple dead_code spans matched identifier: {span_matches}.");
|
panic!("multiple dead_code spans matched identifier: {span_matches}.");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::{ensure, Result};
|
use anyhow::{Result, ensure};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ fn build(cargo: &Path, path: &Path, regression_checker_path: &Path) -> Result<()
|
||||||
flag.push(regression_checker_path);
|
flag.push(regression_checker_path);
|
||||||
flag
|
flag
|
||||||
});
|
});
|
||||||
|
cmd.arg("--bisect-delete-imports");
|
||||||
|
|
||||||
let minimize_roots = start_roots.join(",");
|
let minimize_roots = start_roots.join(",");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue