Skip to content

Commit 3040199

Browse files
committed
Initial version.
1 parent d7afd44 commit 3040199

File tree

8 files changed

+295
-1
lines changed

8 files changed

+295
-1
lines changed

.buildbot.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#! /bin/sh
2+
3+
set -e
4+
5+
export CARGO_HOME="`pwd`/.cargo"
6+
export RUSTUP_HOME="`pwd`/.rustup"
7+
8+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
9+
sh rustup.sh --default-host x86_64-unknown-linux-gnu --default-toolchain stable -y --no-modify-path
10+
11+
export PATH=`pwd`/.cargo/bin/:$PATH
12+
13+
cargo fmt --all -- --check
14+
cargo test
15+
16+
which cargo-deny | cargo install cargo-deny || true
17+
if [ "X`which cargo-deny`" != "X"]; then
18+
cargo-deny check license
19+
else
20+
echo "Warning: couldn't run cargo-deny" > /dev/stderr
21+
fi

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
Cargo.lock

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "depub"
3+
version = "0.1.0"
4+
authors = ["Laurence Tratt <[email protected]>"]
5+
edition = "2018"
6+
readme = "README.md"
7+
license = "Apache-2.0 OR MIT"
8+
9+
[dependencies]
10+
getopts = "0.2"
11+
regex = "1.4"

LICENSE-APACHE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
2+
this file except in compliance with the License. You may obtain a copy of the
3+
License at
4+
5+
http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed
8+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
9+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
10+
specific language governing permissions and limitations under the License.

LICENSE-MIT

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Permission is hereby granted, free of charge, to any person obtaining a copy of
2+
this software and associated documentation files (the "Software"), to deal in
3+
the Software without restriction, including without limitation the rights to
4+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
5+
of the Software, and to permit persons to whom the Software is furnished to do
6+
so, subject to the following conditions:
7+
8+
The above copyright notice and this permission notice shall be included in all
9+
copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17+
SOFTWARE.

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,79 @@
1-
# depub
1+
# depub: minimise visibility
2+
3+
## Overview
4+
5+
When working on medium or large sized Rust code bases, it can be hard to know
6+
whether the visibility of functions, structs, and so on are still at the
7+
minimum required. For example, sometimes functions that once needed to be `pub`
8+
now only need to be `pub(crate)`, `pub(super)`, or simply private.
9+
10+
`depub` minimises the visibility of such items in files passed to it, using a
11+
user-specified command (e.g. `cargo check`) as an oracle to tell if its
12+
reduction of an item's visibility is valid or not. Note that `depub` is
13+
entirely guided by the oracle command: if the code it compiles happens not to
14+
use part of an intentionally public interface, then `depub` is likely to
15+
suggest reducing its visibility even though that's not what you want. The
16+
broader the coverage of your oracle, the less this is an issue.
17+
18+
In essence, `depub` does a string search for `pub`, replaces it with `pub
19+
crate` and sees if a test command still succeeds. If it does, it keeps that
20+
visibility, otherwise it replaces with the original and tries the next item.
21+
Note that `depub` is inherently destructive: it overwrites files as it
22+
operates, so do not run it on source code that you do not want altered!
23+
24+
The list of visibilities that `depub` considers is, in order: `pub`,
25+
`pub(crate)`, `pub(super)`, and private (i.e. no `pub` keyword at all). `depub`
26+
searches for `pub`/`pub(crate)`/`pub(super)` instances, reduces their
27+
visibility by one level, and tries the oracle command. If it succeeds, it tries
28+
the next lower level until private visibility has been reached.
29+
30+
Since reducing the visibility of one item can enable other items' visibility to
31+
be reduced, `depub` keeps running "rounds" until a fixed point has been
32+
reached. The maximum number of rounds is equal to the number of visible items
33+
in the code base, though in practise 2 or 3 rounds are likely to be all that is
34+
needed.
35+
36+
37+
## Usage
38+
39+
`depub`'s usage is as follows:
40+
41+
```
42+
depub -c <command> file_1 [... file_n]
43+
```
44+
45+
where `<command>` is a string to be passed to `/bin/sh -c` for execution to
46+
determine whether the altered source code is still valid.
47+
48+
To reduce the visibility of a normal Rust project, `cd` to your Rust code base
49+
and execute:
50+
51+
```
52+
$ find . -name "*.rs" | \
53+
xargs /path/to/depub -c "cargo check && cargo check --test"
54+
```
55+
56+
`depub` informs you of its progress. After it is finished, `diff` your code
57+
base, and accept those of its recommendations you think appropriate. Note that
58+
`depub` currently uses string search and replace, so it will merrily change the
59+
string `pub` in a command into `pub(crate)` -- you should not expect to accept
60+
its recommendations without at least a cursory check.
61+
62+
63+
## Using with libraries
64+
65+
Running `depub` on a library will tend to reduce all its intentionally `pub`
66+
functions to private visibility. You can weed these out manually after `depub`
67+
has run, but this can be tedious, and may also have reduced the visibility of a
68+
cascade of other items.
69+
70+
To avoid this, use one or more users of the library in the oracle command as part
71+
of your oracle. Temporarily alter their `Cargo.toml` to point to the local
72+
version of your libary and use a command such as:
73+
74+
```
75+
$ find . -name "*.rs" | \
76+
xargs /path/to/depub -c " \
77+
cargo check && cargo check --test && \
78+
cd /path/to/lib && cargo check && cargo check --test"
79+
```

bors.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
status = ["buildbot/buildbot-build-script"]
2+
3+
timeout_sec = 300 # 10 minutes
4+
5+
# Have bors delete auto-merged branches
6+
delete_merged_branches = true

src/main.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use getopts::Options;
2+
use regex::RegexBuilder;
3+
use std::{
4+
env,
5+
fs::{read_to_string, write},
6+
io::{stdout, Write},
7+
path::Path,
8+
process::{self, Command, Stdio},
9+
};
10+
11+
enum PubKind {
12+
Pub,
13+
Crate,
14+
Super,
15+
Private,
16+
}
17+
18+
fn process(oracle_cmd: &str, p: &Path) -> u64 {
19+
let pub_regex = RegexBuilder::new("pub(?:\\s*\\(\\s*(.*?)\\s*\\))?")
20+
.multi_line(true)
21+
.build()
22+
.unwrap();
23+
let mut cur_txt = read_to_string(p).unwrap();
24+
let mut i = 0;
25+
let mut cl = pub_regex.capture_locations();
26+
let mut num_changed = 0;
27+
while i < cur_txt.len() {
28+
print!(".");
29+
stdout().flush().ok();
30+
let old_txt = cur_txt.clone();
31+
let m = match pub_regex.captures_read_at(&mut cl, &old_txt, i) {
32+
Some(m) => m,
33+
None => break,
34+
};
35+
let mut kind = if let Some((start, end)) = cl.get(1) {
36+
match &cur_txt[start..end] {
37+
"crate" => PubKind::Crate,
38+
"super" => PubKind::Super,
39+
_ => {
40+
// FIXME: this captures things we don't need to deal with (e.g. `pub(self)`),
41+
// things we could deal with (e.g. `pub(in ...)`) and random strings we've
42+
// accidentally picked up (e.g. `a pub(The Frog and Cow)`). We should probably
43+
// do something better with the middle class of thing than simply ignoring it.
44+
i = m.end();
45+
continue;
46+
}
47+
}
48+
} else {
49+
PubKind::Pub
50+
};
51+
let mut next_txt = cur_txt.clone();
52+
let mut depubed = false;
53+
loop {
54+
let next_kind = match kind {
55+
PubKind::Pub => PubKind::Crate,
56+
PubKind::Crate => PubKind::Super,
57+
PubKind::Super => PubKind::Private,
58+
PubKind::Private => break,
59+
};
60+
let mut try_txt = cur_txt[..m.start()].to_string();
61+
let pub_txt = match next_kind {
62+
PubKind::Crate => "pub(crate) ",
63+
PubKind::Super => "pub(super) ",
64+
PubKind::Private => "",
65+
_ => unreachable!(),
66+
};
67+
try_txt.push_str(&pub_txt);
68+
try_txt.push_str(&cur_txt[m.end()..]);
69+
write(p, &try_txt).unwrap();
70+
match Command::new("sh")
71+
.arg("-c")
72+
.arg(oracle_cmd)
73+
.stderr(Stdio::null())
74+
.stdout(Stdio::null())
75+
.status()
76+
{
77+
Ok(s) if s.success() => {
78+
if !depubed {
79+
num_changed += 1;
80+
depubed = true;
81+
}
82+
next_txt = try_txt;
83+
}
84+
_ => break,
85+
}
86+
kind = next_kind;
87+
}
88+
cur_txt = next_txt;
89+
if cur_txt.len() >= old_txt.len() {
90+
i = m.end() + (cur_txt.len() - old_txt.len());
91+
} else {
92+
i = m.end() - (old_txt.len() - cur_txt.len());
93+
}
94+
}
95+
write(p, cur_txt).unwrap();
96+
num_changed
97+
}
98+
99+
fn progname() -> String {
100+
match env::current_exe() {
101+
Ok(p) => p
102+
.file_name()
103+
.map(|x| x.to_str().unwrap_or("depub"))
104+
.unwrap_or("depub")
105+
.to_owned(),
106+
Err(_) => "depub".to_owned(),
107+
}
108+
}
109+
110+
/// Print out program usage then exit. This function must not be called after daemonisation.
111+
fn usage() -> ! {
112+
eprintln!(
113+
"Usage: {} -c <command> file_1 [... file_n]",
114+
progname = progname()
115+
);
116+
process::exit(1)
117+
}
118+
119+
fn main() {
120+
let matches = Options::new()
121+
.reqopt("c", "command", "Command to execute.", "string")
122+
.optflag("h", "help", "")
123+
.parse(env::args().skip(1))
124+
.unwrap_or_else(|_| usage());
125+
if matches.opt_present("h") || matches.free.is_empty() {
126+
usage();
127+
}
128+
129+
let oracle_cmd = matches.opt_str("c").unwrap();
130+
let mut round = 1;
131+
loop {
132+
println!("===> Round {}", round);
133+
let mut changed = false;
134+
for p in &matches.free {
135+
print!("{}: ", p);
136+
stdout().flush().ok();
137+
let num_changed = process(oracle_cmd.as_str(), &Path::new(&p));
138+
if num_changed > 0 {
139+
print!(" ({} items depub'ed)", num_changed);
140+
changed = true;
141+
}
142+
println!("");
143+
}
144+
if !changed {
145+
break;
146+
}
147+
round += 1;
148+
}
149+
}

0 commit comments

Comments
 (0)