├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── COOKBOOK.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── rerast_macros
├── Cargo.toml
└── src
│ └── lib.rs
├── scripts
└── release.sh
├── src
├── bin
│ └── cargo-rerast.rs
├── change_to_rule.rs
├── chunked_diff.rs
├── code_substitution.rs
├── definitions.rs
├── errors.rs
├── file_loader.rs
├── lib.rs
├── rule_finder.rs
├── rule_matcher.rs
├── rules.rs
└── validation.rs
└── tests
├── cargo_rerast_tests.rs
└── crates
├── compilation_error
├── Cargo.toml
└── src
│ └── lib.rs
├── invalid_cargo_toml
└── Cargo.toml
└── simple
├── Cargo.toml
├── src
└── lib.rs
└── tests
└── disabled_integration_test.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | **/*.rs.bk
3 | Cargo.lock
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 | rust:
3 | - nightly
4 | os:
5 | - linux
6 | - osx
7 | install:
8 | - rustup component add --toolchain nightly rustc-dev
9 | notifications:
10 | email:
11 | - rerast-dev@googlegroups.com
12 | - dvdlttmr@gmail.com
13 | branches:
14 | except:
15 | - rerast-stable
16 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Licensing
26 |
27 | Although this project is just licensed under the Apache License v2.0, the CLA is
28 | sufficient to allow Google the option to relicense in the future under a dual
29 | Apache2/MIT license. e.g. if this project were merged into another project that
30 | was dual licensed.
31 |
32 | ## Questions
33 |
34 | Please feel free to email David Lattimore (email address in Cargo.toml) if you'd
35 | like you contribute but aren't sure how, or if you have some ideas and want
36 | guidance, etc.
37 |
--------------------------------------------------------------------------------
/COOKBOOK.md:
--------------------------------------------------------------------------------
1 | # Rerast cookbook
2 | Here we've got some examples of things you can do with rerast. If you've got any
3 | more examples, please feel free to send pull requests.
4 |
5 | ### Replace ```try!(something)``` with ```something?```
6 |
7 | ```rust
8 | fn rule1>(r: Result) -> Result {
9 | replace!(r#try!(r) => r?);
10 | unreachable!()
11 | }
12 | ```
13 | This will change:
14 | ```rust
15 | pub fn get_file_contents(filename: &std::path::Path) -> std::io::Result {
16 | let mut result = String::new();
17 | use std::io::Read;
18 | try!(try!(std::fs::File::open(filename)).read_to_string(&mut result));
19 | Ok(result)
20 | }
21 | ```
22 | Into this:
23 | ```rust
24 | pub fn get_file_contents(filename: &std::path::Path) -> std::io::Result {
25 | let mut result = String::new();
26 | use std::io::Read;
27 | std::fs::File::open(filename)?.read_to_string(&mut result)?;
28 | Ok(result)
29 | }
30 | ```
31 |
32 | This rule also shows how to handle rules that have return statements in
33 | them. i.e. specify a return type for your rule function. ```unreachable!()```
34 | can then be used to avoid having to construct an actual value.
35 |
36 | ### Use accessors instead of direct field access
37 |
38 | ```rust
39 | fn r(mut p: Point, x: i32, y: i32) {
40 | replace!(Point{ x: x, y: y } => Point::new(x, y));
41 | replace!(p.x = x => p.set_x(x));
42 | replace!(&mut p.x => p.get_mut_x());
43 | replace!(p.x => p.get_x());
44 | }
45 | ```
46 | This will change:
47 |
48 | ```rust
49 | fn f1(point: Point, point_ref: &Point, mut_point_ref: &mut Point) {
50 | let p2 = Point { x: 1, y: 2 };
51 | process_i32(point.x);
52 | mut_point_ref.x = 1;
53 | let x = &mut mut_point_ref.x;
54 | *x = 42;
55 | process_i32(point_ref.x);
56 | }
57 | ```
58 | Into:
59 |
60 | ```rust
61 | fn f1(point: Point, point_ref: &Point, mut_point_ref: &mut Point) {
62 | let p2 = Point::new(1, 2);
63 | process_i32(point.get_x());
64 | mut_point_ref.set_x(1);
65 | let x = mut_point_ref.get_mut_x();
66 | *x = 42;
67 | process_i32(point_ref.get_x());
68 | }
69 | ```
70 |
71 | ### Remove the argument from a function
72 |
73 | [cargo-issue]: https://github.com/rust-lang/cargo/issues/5746
74 | [cargo-pr]: https://github.com/rust-lang/cargo/pull/5752
75 |
76 | This is a real world example from the Cargo project ([issue][cargo-issue], [PR][cargo-pr]).
77 |
78 | ```rust
79 | use cargotest::support::{ project, project_foo };
80 |
81 | fn rule1(a: &'static str) {
82 | replace!(project("foo") => project_foo());
83 | replace!(project(a) => project_foo().at(a));
84 | }
85 | ```
86 |
87 | Changes:
88 |
89 | ```rust
90 | let p = project("foo")
91 | .file("Cargo.toml", &basic_bin_manifest("foo"))
92 | .file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
93 | .build();
94 | let _p2 = project("bar")
95 | .file("Cargo.toml", &basic_bin_manifest("bar"))
96 | .file("src/bar.rs", &main_file(r#""i am bar""#, &[]))
97 | .build();
98 | ```
99 |
100 | Into:
101 |
102 | ```rust
103 | let p = project_foo()
104 | .file("Cargo.toml", &basic_bin_manifest("foo"))
105 | .file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
106 | .build();
107 | let _p2 = project_foo().at("bar")
108 | .file("Cargo.toml", &basic_bin_manifest("bar"))
109 | .file("src/bar.rs", &main_file(r#""i am bar""#, &[]))
110 | .build();
111 | ```
112 |
113 | When run like this:
114 |
115 | ```terminal
116 | cargo +nightly rerast --rules_file=rewrite.rs --force --targets tests --file tests/testsuite/main.rs
117 | ```
118 |
119 | Afterwhich a simple search and replace can rename `project_foo` back to `project` and the argument
120 | can be dropped.
121 |
122 | ### Replace await! macro with .await
123 | Since await can only be used from within an async function, you'll need to
124 | delcare your rule function async. This cannot be done from the command line, so
125 | you'll need to create a rule file with the following contents:
126 | ```
127 | async fn rule1(r: T) {
128 | replace!(await!(r) => r.await)
129 | }
130 | ```
131 |
132 | ### Replace yield
133 | Since yield can only be used from within a generator, you'll need to delcare
134 | your replace rule inside a generator. For example:
135 | ```
136 | fn r1(x: i32) {
137 | let _ = || {
138 | replace!(yield x => yield x + 1);
139 | };
140 | }
141 | ```
142 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.12.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "aho-corasick"
16 | version = "0.7.18"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
19 | dependencies = [
20 | "memchr",
21 | ]
22 |
23 | [[package]]
24 | name = "ansi_term"
25 | version = "0.11.0"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
28 | dependencies = [
29 | "winapi",
30 | ]
31 |
32 | [[package]]
33 | name = "assert_cmd"
34 | version = "1.0.1"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "c88b9ca26f9c16ec830350d309397e74ee9abdfd8eb1f71cb6ecc71a3fc818da"
37 | dependencies = [
38 | "doc-comment",
39 | "predicates",
40 | "predicates-core",
41 | "predicates-tree",
42 | "wait-timeout",
43 | ]
44 |
45 | [[package]]
46 | name = "atty"
47 | version = "0.2.14"
48 | source = "registry+https://github.com/rust-lang/crates.io-index"
49 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
50 | dependencies = [
51 | "hermit-abi",
52 | "libc",
53 | "winapi",
54 | ]
55 |
56 | [[package]]
57 | name = "autocfg"
58 | version = "1.0.0"
59 | source = "registry+https://github.com/rust-lang/crates.io-index"
60 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
61 |
62 | [[package]]
63 | name = "backtrace"
64 | version = "0.3.48"
65 | source = "registry+https://github.com/rust-lang/crates.io-index"
66 | checksum = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130"
67 | dependencies = [
68 | "addr2line",
69 | "cfg-if",
70 | "libc",
71 | "object",
72 | "rustc-demangle",
73 | ]
74 |
75 | [[package]]
76 | name = "bitflags"
77 | version = "1.2.1"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
80 |
81 | [[package]]
82 | name = "cfg-if"
83 | version = "0.1.10"
84 | source = "registry+https://github.com/rust-lang/crates.io-index"
85 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
86 |
87 | [[package]]
88 | name = "clap"
89 | version = "2.33.1"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
92 | dependencies = [
93 | "ansi_term",
94 | "atty",
95 | "bitflags",
96 | "strsim",
97 | "textwrap",
98 | "unicode-width",
99 | "vec_map",
100 | ]
101 |
102 | [[package]]
103 | name = "colored"
104 | version = "1.9.3"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
107 | dependencies = [
108 | "atty",
109 | "lazy_static",
110 | "winapi",
111 | ]
112 |
113 | [[package]]
114 | name = "diff"
115 | version = "0.1.12"
116 | source = "registry+https://github.com/rust-lang/crates.io-index"
117 | checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
118 |
119 | [[package]]
120 | name = "difference"
121 | version = "2.0.0"
122 | source = "registry+https://github.com/rust-lang/crates.io-index"
123 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
124 |
125 | [[package]]
126 | name = "doc-comment"
127 | version = "0.3.3"
128 | source = "registry+https://github.com/rust-lang/crates.io-index"
129 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
130 |
131 | [[package]]
132 | name = "either"
133 | version = "1.5.3"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
136 |
137 | [[package]]
138 | name = "failure"
139 | version = "0.1.8"
140 | source = "registry+https://github.com/rust-lang/crates.io-index"
141 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
142 | dependencies = [
143 | "backtrace",
144 | "failure_derive",
145 | ]
146 |
147 | [[package]]
148 | name = "failure_derive"
149 | version = "0.1.8"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
152 | dependencies = [
153 | "proc-macro2",
154 | "quote",
155 | "syn",
156 | "synstructure",
157 | ]
158 |
159 | [[package]]
160 | name = "float-cmp"
161 | version = "0.6.0"
162 | source = "registry+https://github.com/rust-lang/crates.io-index"
163 | checksum = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d"
164 | dependencies = [
165 | "num-traits",
166 | ]
167 |
168 | [[package]]
169 | name = "gimli"
170 | version = "0.21.0"
171 | source = "registry+https://github.com/rust-lang/crates.io-index"
172 | checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
173 |
174 | [[package]]
175 | name = "hermit-abi"
176 | version = "0.1.13"
177 | source = "registry+https://github.com/rust-lang/crates.io-index"
178 | checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71"
179 | dependencies = [
180 | "libc",
181 | ]
182 |
183 | [[package]]
184 | name = "itertools"
185 | version = "0.9.0"
186 | source = "registry+https://github.com/rust-lang/crates.io-index"
187 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
188 | dependencies = [
189 | "either",
190 | ]
191 |
192 | [[package]]
193 | name = "json"
194 | version = "0.12.4"
195 | source = "registry+https://github.com/rust-lang/crates.io-index"
196 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
197 |
198 | [[package]]
199 | name = "lazy_static"
200 | version = "1.4.0"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
203 |
204 | [[package]]
205 | name = "libc"
206 | version = "0.2.71"
207 | source = "registry+https://github.com/rust-lang/crates.io-index"
208 | checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
209 |
210 | [[package]]
211 | name = "memchr"
212 | version = "2.5.0"
213 | source = "registry+https://github.com/rust-lang/crates.io-index"
214 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
215 |
216 | [[package]]
217 | name = "normalize-line-endings"
218 | version = "0.3.0"
219 | source = "registry+https://github.com/rust-lang/crates.io-index"
220 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
221 |
222 | [[package]]
223 | name = "num-traits"
224 | version = "0.2.11"
225 | source = "registry+https://github.com/rust-lang/crates.io-index"
226 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
227 | dependencies = [
228 | "autocfg",
229 | ]
230 |
231 | [[package]]
232 | name = "object"
233 | version = "0.19.0"
234 | source = "registry+https://github.com/rust-lang/crates.io-index"
235 | checksum = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2"
236 |
237 | [[package]]
238 | name = "predicates"
239 | version = "1.0.4"
240 | source = "registry+https://github.com/rust-lang/crates.io-index"
241 | checksum = "347a1b6f0b21e636bc9872fb60b83b8e185f6f5516298b8238699f7f9a531030"
242 | dependencies = [
243 | "difference",
244 | "float-cmp",
245 | "normalize-line-endings",
246 | "predicates-core",
247 | "regex",
248 | ]
249 |
250 | [[package]]
251 | name = "predicates-core"
252 | version = "1.0.0"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178"
255 |
256 | [[package]]
257 | name = "predicates-tree"
258 | version = "1.0.0"
259 | source = "registry+https://github.com/rust-lang/crates.io-index"
260 | checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124"
261 | dependencies = [
262 | "predicates-core",
263 | "treeline",
264 | ]
265 |
266 | [[package]]
267 | name = "proc-macro2"
268 | version = "1.0.17"
269 | source = "registry+https://github.com/rust-lang/crates.io-index"
270 | checksum = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101"
271 | dependencies = [
272 | "unicode-xid",
273 | ]
274 |
275 | [[package]]
276 | name = "quote"
277 | version = "1.0.6"
278 | source = "registry+https://github.com/rust-lang/crates.io-index"
279 | checksum = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea"
280 | dependencies = [
281 | "proc-macro2",
282 | ]
283 |
284 | [[package]]
285 | name = "regex"
286 | version = "1.5.6"
287 | source = "registry+https://github.com/rust-lang/crates.io-index"
288 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
289 | dependencies = [
290 | "aho-corasick",
291 | "memchr",
292 | "regex-syntax",
293 | ]
294 |
295 | [[package]]
296 | name = "regex-syntax"
297 | version = "0.6.26"
298 | source = "registry+https://github.com/rust-lang/crates.io-index"
299 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
300 |
301 | [[package]]
302 | name = "rerast"
303 | version = "0.1.95"
304 | dependencies = [
305 | "assert_cmd",
306 | "clap",
307 | "colored",
308 | "diff",
309 | "failure",
310 | "itertools",
311 | "json",
312 | "predicates",
313 | "rerast_macros",
314 | ]
315 |
316 | [[package]]
317 | name = "rerast_macros"
318 | version = "0.1.12"
319 |
320 | [[package]]
321 | name = "rustc-demangle"
322 | version = "0.1.16"
323 | source = "registry+https://github.com/rust-lang/crates.io-index"
324 | checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
325 |
326 | [[package]]
327 | name = "strsim"
328 | version = "0.8.0"
329 | source = "registry+https://github.com/rust-lang/crates.io-index"
330 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
331 |
332 | [[package]]
333 | name = "syn"
334 | version = "1.0.27"
335 | source = "registry+https://github.com/rust-lang/crates.io-index"
336 | checksum = "ef781e621ee763a2a40721a8861ec519cb76966aee03bb5d00adb6a31dc1c1de"
337 | dependencies = [
338 | "proc-macro2",
339 | "quote",
340 | "unicode-xid",
341 | ]
342 |
343 | [[package]]
344 | name = "synstructure"
345 | version = "0.12.3"
346 | source = "registry+https://github.com/rust-lang/crates.io-index"
347 | checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
348 | dependencies = [
349 | "proc-macro2",
350 | "quote",
351 | "syn",
352 | "unicode-xid",
353 | ]
354 |
355 | [[package]]
356 | name = "textwrap"
357 | version = "0.11.0"
358 | source = "registry+https://github.com/rust-lang/crates.io-index"
359 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
360 | dependencies = [
361 | "unicode-width",
362 | ]
363 |
364 | [[package]]
365 | name = "treeline"
366 | version = "0.1.0"
367 | source = "registry+https://github.com/rust-lang/crates.io-index"
368 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
369 |
370 | [[package]]
371 | name = "unicode-width"
372 | version = "0.1.7"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
375 |
376 | [[package]]
377 | name = "unicode-xid"
378 | version = "0.2.0"
379 | source = "registry+https://github.com/rust-lang/crates.io-index"
380 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
381 |
382 | [[package]]
383 | name = "vec_map"
384 | version = "0.8.2"
385 | source = "registry+https://github.com/rust-lang/crates.io-index"
386 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
387 |
388 | [[package]]
389 | name = "wait-timeout"
390 | version = "0.2.0"
391 | source = "registry+https://github.com/rust-lang/crates.io-index"
392 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
393 | dependencies = [
394 | "libc",
395 | ]
396 |
397 | [[package]]
398 | name = "winapi"
399 | version = "0.3.8"
400 | source = "registry+https://github.com/rust-lang/crates.io-index"
401 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
402 | dependencies = [
403 | "winapi-i686-pc-windows-gnu",
404 | "winapi-x86_64-pc-windows-gnu",
405 | ]
406 |
407 | [[package]]
408 | name = "winapi-i686-pc-windows-gnu"
409 | version = "0.4.0"
410 | source = "registry+https://github.com/rust-lang/crates.io-index"
411 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
412 |
413 | [[package]]
414 | name = "winapi-x86_64-pc-windows-gnu"
415 | version = "0.4.0"
416 | source = "registry+https://github.com/rust-lang/crates.io-index"
417 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
418 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rerast"
3 | version = "0.1.95"
4 | license = "Apache-2.0"
5 | description = "An AST (abstract syntax tree) based search replace tool for code written in Rust"
6 | repository = "https://github.com/google/rerast"
7 | readme = "README.md"
8 | categories = ["development-tools", "development-tools::cargo-plugins"]
9 | edition = "2018"
10 | authors = [
11 | "David Lattimore ",
12 | "Dale Wijnand",
13 | "Kornel",
14 | ]
15 |
16 | [dependencies]
17 | rerast_macros = { version = "0.1.12", path = "rerast_macros" }
18 | itertools = "0.9.0"
19 | clap = "2.33.0"
20 | json = "0.12.4"
21 | diff = "0.1.12"
22 | colored = "1.9.3"
23 | failure = "0.1.8"
24 |
25 | [dev-dependencies]
26 | predicates = "1.0.4"
27 | assert_cmd = "1.0.1"
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rerast
2 |
3 | Rerast is a search/replace tool for Rust code using rules. A rule consists of a
4 | search pattern, a replacement and possibly some placeholders that can appear in
5 | both the search pattern and the replacement. Matching is done on syntax, not on
6 | text, so formatting doesn't matter. Placeholders are typed and must match the
7 | type found in the code for the rule to apply.
8 |
9 | Rerast is deprecated. We suggest using the [Structured Search
10 | Replace](https://rust-analyzer.github.io/manual.html#structural-seach-and-replace) feature available
11 | in rust-analyzer. It is available either in vscode or from the command line (and possibly also vim).
12 | If you are missing any particular feature that Rerast supported (or didn't), please comment on [this
13 | issue](https://github.com/rust-analyzer/rust-analyzer/issues/3186).
14 |
15 | If you'd like to still use Rerast, we suggest using Rerast version 0.1.88 with Rust version
16 | nightly-2020-02-27. There are a few newer version of Rerast, but there are some broken features in
17 | the newer versions.
18 |
19 | ## Installation
20 |
21 | ```sh
22 | rustup toolchain add nightly-2020-02-27
23 | rustup component add --toolchain nightly-2020-02-27 rustc-dev
24 | cargo +nightly-2020-02-27 install --version 0.1.88 rerast
25 | ```
26 |
27 | ## Usage
28 |
29 | Basic operations can be performed entirely from the command line
30 | ```sh
31 | cargo +nightly-2020-02-27 rerast --placeholders 'a: i32' --search 'a + 1' --replace_with 'a - 1' --diff
32 | ```
33 |
34 | Alternatively you can put your rule in a Rust file
35 | ```rust
36 | fn rule1(a: i32) {
37 | replace!(a + 1 => a - 1);
38 | }
39 | ```
40 | then use
41 |
42 | ```sh
43 | cargo +nightly-2020-02-27 rerast --rules_file=my_rules.rs
44 | ```
45 | Putting your rules in a file is required if you want to apply multiple rules at once.
46 |
47 | If you'd like to actually update your files, that can be done as follows:
48 |
49 | ```sh
50 | cargo +nightly-2020-02-27 rerast --placeholders 'a: i32' --search 'a + 1' --replace_with 'a - 1' --force --backup
51 | ```
52 |
53 | You can control which compilation roots rerast will inject the rule into using the `--file` argument, e.g.:
54 |
55 | ```sh
56 | cargo +nightly-2020-02-27 rerast --rules_file=my_rules.rs --targets tests --file tests/testsuite/main.rs --diff
57 | ```
58 |
59 | Here's a more complex example
60 |
61 | ```rust
62 | use std::rc::Rc;
63 | fn rule1(r: Rc) {
64 | replace!(r.clone() => Rc::clone(&r))
65 | }
66 | ```
67 |
68 | Here we're replacing calls to the clone() method on an Rc with the more explicit way of cloning
69 | an Rc - via Rc::clone.
70 |
71 | "r" is a placeholder which will match any expression of the type specified. The name of the function
72 | "rule1" is not currently used for anything. In future it may be possible to selectively
73 | enable/disable rules by specifying their name, so it's probably a good idea to put a slightly
74 | descriptive name here. Similarly, comments placed before the function may in the future be displayed
75 | to users when the rule matches. This is not yet implemented.
76 |
77 | A function can contain multiple invocations of the replace! macro, with earlier rules taking precedence.
78 | This is useful if you want to do several replacements that make use of the same placeholders or if you want
79 | to handle certain special patterns first, ahead of a more general match.
80 |
81 | Besides replace! there are several other replacement macros that can be used:
82 |
83 | * replace\_pattern! - this replaces patterns. e.g. &Some(a). Such a pattern might appear in a match
84 | arm or if let. Irrefutable patterns (those that are guaranteed to always match) can also be
85 | matched within let statements and function arguments.
86 | * replace\_type! - this replaces types. It's currently a bit limited in that it doesn't support
87 | placeholders. Also note, if your type is just a trait you should consider using
88 | replace\_trait\_ref! instead, since trait references can appear in contexts where types cannot -
89 | specifically generic bounds and where clauses.
90 | * replace\_trait\_ref! - this replaces references to the named trait
91 |
92 | Replacing statements is currently disabled pending a good use-case.
93 |
94 | ## Matching macro invocations
95 |
96 | Macro invocations can be matched so long as they expand to code that can be matched. Note however
97 | that a macro invocation will not match against the equivalent code, nor the invocation of a
98 | different, but identical macro. This is intentional. When verifying a match, we check that the same
99 | sequence of expansions was followed. Also note, that if a macro expands to something different every
100 | time it is invoked, it will never match. println! is an example of such a macro, since it generates
101 | a constant that is referenced from the expanded code and every invocation references a different
102 | constant.
103 |
104 | ## Order of operations
105 |
106 | Suppose you're replacing foo(a, b) with a && !b. Depending on what the placeholders end up matching
107 | and what context the entire expression is in, there may be need for extra parenthesis. For example
108 | if the matched code was !foo(x == 1, y == 2), if we didn't add any parenthesis, we'd end up with !x
109 | == 1 && !y == 2 which clearly isn't correct. Rerast detects this and adds parenthesis as needed in
110 | order to preserve the order or precedence found in the replacement. This would give !(x == 1 && !(y
111 | == 2)).
112 |
113 | ## Formatting of code
114 |
115 | No reformatting of code is currently done. Unmatched code will not be affected. Replacement code is
116 | produced by copying the replacement code from the rule and splicing in any matched patterns. In
117 | future, we may adjust identation for multi-line replacements. Running rustfmt afterwards is probably
118 | a good idea since some identation and line lengths may not be ideal.
119 |
120 | ## Recursive and overlapping matches
121 |
122 | The first matched rule wins. When some code is matched, no later rules will be applied to that
123 | code. However, code matched to placeholders will be searched for further matches to all rules.
124 |
125 | ## Automatically determining a rule from a source code change
126 |
127 | If you're about to make a change multiple times throughout your source code and you're using git,
128 | you can commit (or stage) your changes, make one edit then run:
129 |
130 | ```sh
131 | cargo +nightly-2020-02-27 rerast --replay_git --diff
132 | ```
133 |
134 | This will locate the changed expression in your project (of which there should be only one) then try
135 | to determine a rule that would have produced this change. It will print the rule, then apply it to
136 | your project. If you are happy with the changes, you can run again with --force to apply them, or
137 | you could copy the printed rule into a .rs file and apply it with --rules_file.
138 |
139 | * The rule produced will use placeholders to the maximum extent possible. i.e. wherever a
140 | subexpression is found in both the old and the new code, it will be replaced with a placeholder.
141 | * This only works for changed expressions at the moment, not for statements, types, patterns etc.
142 | * Your code must be able to compile both with and without the change.
143 |
144 | ## Limitations
145 |
146 | * Use statements are not yet updated, so depending on your rule, may need to be updated after the
147 | rule is applied. This should eventually be fixed, there just wasn't time before release and it's
148 | kind of tricky.
149 | * Your code must be able to compile for this to work.
150 | * The replacement code must also compile. This means rerast is better at replacing a deprecated API
151 | usage with its non-deprecated equivalent than dealing with breaking changes. Often the best
152 | workaround is to create a new API temporarily.
153 | * Code within rustdoc is not yet processed and matched.
154 | * Conditional code that disabled with a cfg attribute isn't matched. It's suggested to enable all
155 | features if possible when running so that as much code can be checked as possible.
156 | * replace_type! doesn't yet support placeholders.
157 | * Probably many bugs and missing features. Please feel free to file bugs / feature requests.
158 |
159 | ## Known issues
160 |
161 | * If you have integration tests (a "tests" directory) in your project, you might
162 | be no matches. Not sure why. This started from nightly-2020-04-10. You might
163 | be able to work around this issue by passing `--targets ''` to cargo rerast.
164 | Unfortunately then you won't get matches in non-integration tests (i.e.
165 | cfg(test)). Alternatively you could install an older version of rust and the
166 | corresponding rerast version.
167 | * Using `?` in the replacement is currently broken. This broke I think in
168 | February 2020. Something changed with spans.
169 |
170 | ## More examples
171 | See the [Rerast Cookbook](COOKBOOK.md) for more examples.
172 |
173 | ## Groups
174 | * [Users group](https://groups.google.com/forum/#!forum/rerast-users)
175 | * [Developers group](https://groups.google.com/forum/#!forum/rerast-dev)
176 |
177 | ## Questions?
178 | Feel free to just file an issue on github.
179 |
180 | ## Authors
181 |
182 | See Cargo.toml
183 |
184 | ## Contributing
185 |
186 | See [CONTRIBUTING.md](CONTRIBUTING.md)
187 |
188 | ## Code of conduct
189 |
190 | This project defers to the [Rust code of conduct](https://www.rust-lang.org/en-US/conduct.html). If
191 | you feel someone is not adhering to the code of conduct in relation to this project, please contact
192 | David Lattimore. My email address is in Cargo.toml.
193 |
194 | ## Disclaimer
195 |
196 | This is not an official Google product. It's released by Google only because the (original) author
197 | happens to work there.
198 |
--------------------------------------------------------------------------------
/rerast_macros/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rerast_macros"
3 | version = "0.1.12"
4 | description = "Macros for use in Rerast rules"
5 | repository = "https://github.com/google/rerast"
6 | license = "Apache-2.0"
7 | authors = ["David Lattimore "]
8 |
9 | [dependencies]
10 |
--------------------------------------------------------------------------------
/rerast_macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 |
16 | // global `#[deny()]` in the processed crate affects this file as well,
17 | // and could cause compilation-breaking warnings.
18 | #![allow(missing_docs, warnings)]
19 |
20 | /// Invocations of this macro instruct Rerast to replace the first expression/statement with the one
21 | /// after the =>. For example replace!(5 => 6) says to replace all occurences of the literal 5 with
22 | /// the literal 6.
23 | ///
24 | /// The macro should be invoked inside a function definition. The arguments to the function
25 | /// represent placeholders within the search and replace expressions.
26 | #[macro_export]
27 | macro_rules! replace {
28 | ($a:expr => $b:expr) => {
29 | // Suppress warnings about unused values in case we're replacing something like a Result.
30 | #[allow(unused_must_use)]
31 | // If we have a search or replacement pattern that is just a placeholder then this macro
32 | // will turn that into a statement. Normally a reference to a variable is not permitted as a
33 | // statement, allow it.
34 | #[allow(path_statements)]
35 | #[allow(unreachable_code)]
36 | {
37 | if false {
38 | use $crate::GetMode;
39 | match $crate::_ExprRuleMarker.get_mode() {
40 | $crate::Mode::Search => {&$a;}
41 | $crate::Mode::Replace => {&$b;}
42 | }
43 | // Some of the placeholders used in this replace! invocation might have been
44 | // moved. To avoid use-after-move errors, we mark this as unreachable.
45 | unreachable!();
46 | }
47 | }
48 | };
49 | }
50 |
51 | /// Replaces one type with another. This will not match trait bounds (e.g. in where clauses), since
52 | /// they are not types. If you want to replace all references to a trait, use `replace_trait_ref!`
53 | /// instead.
54 | #[macro_export]
55 | macro_rules! replace_type {
56 | ($a:ty => $b:ty) => {
57 | {
58 | use $crate::GetMode;
59 | match $crate::_TypeRuleMarker.get_mode() {
60 | $crate::Mode::Search => {
61 | let _t: &$a;
62 | }
63 | $crate::Mode::Replace => {
64 | let _t: &$b;
65 | }
66 | }
67 | }
68 | };
69 | }
70 |
71 | /// Replaces references to a trait with references to a different trait.
72 | #[macro_export]
73 | macro_rules! replace_trait_ref {
74 | ($a:ty => $b:ty) => {
75 | {
76 | use $crate::GetMode;
77 | match $crate::_TraitRefRuleMarker.get_mode() {
78 | $crate::Mode::Search => {
79 | let _t: &$a;
80 | }
81 | $crate::Mode::Replace => {
82 | let _t: &$b;
83 | }
84 | }
85 | }
86 | };
87 | }
88 |
89 | /// Replaces a pattern with another pattern. A placeholder is required in order to specify the types
90 | /// of the search and replacement pattern. Although they can be the same if you're replacing a
91 | /// pattern with another of the same type.
92 | ///
93 | /// # Examples
94 | ///
95 | /// ```
96 | /// # #[macro_use] extern crate rerast;
97 | /// # struct Foo(i32);
98 | /// fn some_foo_to_result_foo(p1: Option, p2: Result) {
99 | /// replace_pattern!(Some(f1) = p1 => Result::Ok(f1) = p2);
100 | /// replace_pattern!(None = p1 => Result::Err(()) = p2);
101 | /// }
102 | /// # fn main () {}
103 | /// ```
104 | ///
105 | /// This will transform:
106 | ///
107 | /// ```
108 | /// # #[derive(Copy, Clone)] struct Foo(i32);
109 | /// # fn f() -> Option {Some(Foo(42))}
110 | /// match f() {
111 | /// Some(Foo(x)) => x,
112 | /// None => 0,
113 | /// }
114 | /// ```
115 | ///
116 | /// Into:
117 | ///
118 | /// ```
119 | /// # fn f() -> Foo {Foo(42)}
120 | /// match f() {
121 | /// Result::Ok(Foo(x)) => x,
122 | /// Result::Err(()) => 0,
123 | /// }
124 | /// ```
125 | #[macro_export]
126 | macro_rules! replace_pattern {
127 | ($a:pat = $at:ident => $b:pat = $bt:ident) => {
128 | if false {
129 | use $crate::GetMode;
130 | match $crate::_PatternRuleMarker.get_mode() {
131 | $crate::Mode::Search => {
132 | if let Some($a) = Some($at) {};
133 | }
134 | $crate::Mode::Replace => {
135 | if let Some($b) = Some($bt) {};
136 | }
137 | }
138 | // Some of the placeholders used in this replace_pattern! invocation might have been
139 | // moved. To avoid use-after-move errors, we mark this as unreachable.
140 | unreachable!();
141 | }
142 | }
143 | }
144 |
145 | /// A placeholder that matches zero or more statements
146 | /// IMPORTANT: This is currently broken and will most likely be replaced with a macro.
147 | pub type Statements = &'static Fn() -> _Statements;
148 |
149 | // These types are internal markers to help rerast find and identify things in rules. They're not
150 | // intended to be referenced directly.
151 | pub enum Mode {
152 | Search,
153 | Replace,
154 | }
155 | pub struct _Statements;
156 | pub trait GetMode {
157 | fn get_mode(&self) -> Mode {
158 | Mode::Search
159 | }
160 | }
161 | pub struct _TypeRuleMarker;
162 | impl GetMode for _TypeRuleMarker {}
163 | pub struct _ExprRuleMarker;
164 | impl GetMode for _ExprRuleMarker {}
165 | pub struct _PatternRuleMarker;
166 | impl GetMode for _PatternRuleMarker {}
167 | pub struct _TraitRefRuleMarker;
168 | impl GetMode for _TraitRefRuleMarker {}
169 |
170 | pub const _RERAST_MACROS_SRC: &'static str = include_str!("lib.rs");
171 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | if ! git diff-index --quiet HEAD --; then
4 | echo "Please commit all changes first" >&2
5 | exit 1
6 | fi
7 | VERSION=$(grep '^version' Cargo.toml | cut -d '"' -f2)
8 | TAG="v$VERSION"
9 | if git tag | grep "$TAG"; then
10 | echo "$VERSION appears to have already been released" >&2
11 | exit 1
12 | fi
13 | git pull --rebase
14 | cargo build
15 | cargo test --all
16 | cargo publish
17 | git tag "$TAG"
18 | git push
19 |
--------------------------------------------------------------------------------
/src/bin/cargo-rerast.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | #![feature(rustc_private)]
16 | #![cfg_attr(feature = "clippy", feature(plugin))]
17 | #![cfg_attr(feature = "clippy", plugin(clippy))]
18 |
19 | #[macro_use]
20 | extern crate clap;
21 | use colored;
22 | #[macro_use]
23 | extern crate failure;
24 |
25 | use json;
26 | use rerast;
27 | extern crate rustc_span;
28 |
29 | use clap::ArgMatches;
30 | use failure::Error;
31 | use json::JsonValue;
32 | use rerast::chunked_diff;
33 | use rerast::{CompilerInvocationInfo, Config, RerastCompilerDriver, RerastOutput};
34 | use rustc_span::source_map::RealFileLoader;
35 | use std::collections::HashMap;
36 | use std::fs::{self, File};
37 | use std::io::prelude::*;
38 | use std::io::Write;
39 | use std::path::Path;
40 |
41 | // Environment variables that we use to pass data from the outer invocation of cargo-rerast through
42 | // to the inner invocation which runs within cargo check.
43 | mod var_names {
44 | // Environment variable name, which if set, indicates that we should write our arguments out as
45 | // JSON before running the actual rust compiler. Hopefully eventually cargo will have a way for
46 | // us to query the compile commandlines without doing this sort of thing.
47 | pub const PRINT_ARGS_JSON: &str = "RERAST_PRINT_ARGS_JSON";
48 | }
49 |
50 | const RERAST_JSON_MARKER: &str = "RERAST_JSON_MARKER: ";
51 |
52 | /// Queries cargo to find the name of the current crate,
53 | /// NOTE: this is not cached, so calling it multiple times will run `cargo` again.
54 | fn metadata() -> Result {
55 | let output = std::process::Command::new("cargo")
56 | .args(vec!["metadata", "--no-deps", "--format-version=1"])
57 | .stdout(std::process::Stdio::piped())
58 | .output()?;
59 | ensure!(
60 | output.status.success(),
61 | "cargo metadata failed:\n{}",
62 | std::str::from_utf8(output.stderr.as_slice())?
63 | );
64 | let metadata_str = std::str::from_utf8(output.stdout.as_slice())?;
65 | let parsed = match json::parse(metadata_str) {
66 | Ok(v) => v,
67 | Err(e) => bail!("Error parsing metadata JSON: {:?}", e),
68 | };
69 | Ok(parsed)
70 | }
71 |
72 | // Queries cargo to find the name of the current crate, then runs cargo clean to
73 | // clean up artifacts for that package (but not dependencies). This is necessary
74 | // in order to ensure that all the files in the current crate actually get built
75 | // when we run cargo check. Hopefully eventually there'll be a nicer way to
76 | // integrate with cargo such that we won't need to do this.
77 | fn clean_local_targets() -> Result<(), Error> {
78 | for package in metadata()?["packages"].members() {
79 | if let Some(name) = package["name"].as_str() {
80 | // TODO: Remove once #10 is fixed.
81 | if std::env::var("RERAST_FULL_CARGO_CLEAN") == Ok("1".to_string()) {
82 | std::process::Command::new("cargo")
83 | .args(vec!["clean"])
84 | .status()?;
85 | } else {
86 | std::process::Command::new("cargo")
87 | .args(vec!["clean", "--package", name])
88 | .status()?;
89 | }
90 | }
91 | }
92 | Ok(())
93 | }
94 |
95 | fn read_file_as_string(path: &Path) -> Result {
96 | fn read_file_internal(path: &Path) -> std::io::Result {
97 | let mut contents = String::new();
98 | File::open(path)?.read_to_string(&mut contents)?;
99 | Ok(contents)
100 | }
101 | read_file_internal(path).map_err(|error| format_err!("Error opening {:?}: {}", path, error))
102 | }
103 |
104 | fn get_compiler_invocation_infos_for_local_package(
105 | matches: &ArgMatches<'_>,
106 | ) -> Result, Error> {
107 | use std::str;
108 | clean_local_targets()?;
109 | let current_exe = std::env::current_exe().expect("env::current_exe() failed");
110 | // The -j 1 flags are to prevent interleaving of stdout from corrupting our JSON. See issue #5.
111 | let check_args = vec!["check", "-j", "1"];
112 | let target_args: Vec<_> = match matches.values_of("targets") {
113 | Some(targets) => targets.map(|t| format!("--{}", t)).collect(),
114 | None => vec!["--tests", "--examples", "--benches"]
115 | .into_iter()
116 | .map(str::to_owned)
117 | .collect(),
118 | };
119 | let cargo_check_output = std::process::Command::new("cargo")
120 | .env(var_names::PRINT_ARGS_JSON, "yes")
121 | .env("RUSTC_WRAPPER", current_exe)
122 | .args(check_args.clone())
123 | .args(target_args.clone())
124 | .stdout(std::process::Stdio::piped())
125 | .stderr(std::process::Stdio::piped())
126 | .output()
127 | .expect("Failed to invoke cargo");
128 | let output_str = std::str::from_utf8(cargo_check_output.stdout.as_slice())?;
129 | ensure!(
130 | cargo_check_output.status.code() == Some(0),
131 | "cargo check failed (exit code = {}). Output follows:\n\
132 | {}\n\n\
133 | To reproduce outside of rerast, try running:\n\
134 | cargo {} {}",
135 | cargo_check_output
136 | .status
137 | .code()
138 | .map(|c| c.to_string())
139 | .unwrap_or_else(|| "signal".to_owned()),
140 | std::str::from_utf8(cargo_check_output.stderr.as_slice())?,
141 | check_args.join(" "),
142 | target_args.join(" ")
143 | );
144 | let mut result: Vec = Vec::new();
145 | for line in output_str.lines() {
146 | if line.starts_with(RERAST_JSON_MARKER) {
147 | let json = &line[RERAST_JSON_MARKER.len()..];
148 | let parsed = match json::parse(json) {
149 | Ok(v) => v,
150 | Err(e) => bail!("Error parsing internal response JSON: {:?}: {:?}", e, json),
151 | };
152 | if let JsonValue::Array(data) = parsed {
153 | if let (JsonValue::Array(arg_values), env_values) = (&data[0], &data[1]) {
154 | let args: Result, Error> = arg_values
155 | .iter()
156 | .map(|v| {
157 | if let Some(s) = v.as_str() {
158 | Ok(s.to_owned())
159 | } else {
160 | bail!("Expected JSON string, got: {:?}", v);
161 | }
162 | })
163 | // First value will be the path to cargo-rerast, skip it.
164 | .skip(1)
165 | // Filter out any unsupported command-line arguments.
166 | .filter(|v| {
167 | if let Ok(v) = v {
168 | !(v.starts_with("-Cembed-bitcode")
169 | || v.starts_with("-Cbitcode-in-rlib"))
170 | } else {
171 | true
172 | }
173 | })
174 | .collect();
175 | let mut env: HashMap = HashMap::new();
176 | for (k, v) in env_values.entries() {
177 | if let Some(v) = v.as_str() {
178 | env.insert(k.to_owned(), v.to_owned());
179 | }
180 | }
181 | result.push(CompilerInvocationInfo { args: args?, env });
182 | }
183 | }
184 | }
185 | }
186 | Ok(result)
187 | }
188 |
189 | enum Action {
190 | Diff,
191 | DiffCmd(String),
192 | ForceWrite { backup: bool },
193 | }
194 |
195 | impl Action {
196 | fn from_matches(matches: &ArgMatches<'_>) -> Result {
197 | let mut actions = Vec::new();
198 | if matches.is_present("diff") {
199 | actions.push(Action::Diff)
200 | }
201 | if let Some(diff_cmd) = matches.value_of("diff_cmd") {
202 | actions.push(Action::DiffCmd(diff_cmd.to_owned()));
203 | }
204 | if matches.is_present("force") {
205 | actions.push(Action::ForceWrite {
206 | backup: matches.is_present("backup"),
207 | })
208 | }
209 | if actions.len() > 1 {
210 | actions.clear();
211 | }
212 | actions.into_iter().next().ok_or_else(|| {
213 | format_err!("Exactly one of --diff, --diff_cmd or --force is currently required")
214 | })
215 | }
216 |
217 | fn process(&self, path: &Path, new_contents: &str) -> Result<(), Error> {
218 | let filename = path
219 | .to_str()
220 | .ok_or_else(|| format_err!("Path wasn't valid UTF-8"))?;
221 | match *self {
222 | Action::Diff => {
223 | let current_contents = read_file_as_string(path)?;
224 | chunked_diff::print_diff(filename, ¤t_contents, new_contents);
225 | }
226 | Action::DiffCmd(ref diff_cmd) => {
227 | let mut diff_args_iter = diff_cmd.split(' ');
228 | let command = diff_args_iter.next().unwrap_or("diff");
229 | let diff_args: Vec<_> = diff_args_iter.chain(vec![filename, "-"]).collect();
230 | let mut diff_process = std::process::Command::new(command)
231 | .args(diff_args)
232 | .stdin(std::process::Stdio::piped())
233 | .spawn()
234 | .map_err(|e| format_err!("Error running '{}': {}", diff_cmd, e))?;
235 | diff_process
236 | .stdin
237 | .as_mut()
238 | .unwrap()
239 | .write_all(new_contents.as_bytes())?;
240 | diff_process.wait()?;
241 | }
242 | Action::ForceWrite { backup } => {
243 | // Write to a temporary file so that we don't truncate the file if writing fails.
244 | let tmp_file = "rerast-tmp";
245 | fs::File::create(tmp_file)?.write_all(new_contents.as_bytes())?;
246 | if backup {
247 | fs::rename(filename, filename.to_owned() + ".bk")?;
248 | }
249 | fs::rename(tmp_file, filename)?;
250 | }
251 | }
252 | Ok(())
253 | }
254 | }
255 |
256 | fn get_replacement_kind_and_arg(matches: &ArgMatches<'_>) -> Result<(&'static str, String), Error> {
257 | let mut result = Vec::new();
258 | if let Some(s) = matches.value_of("search") {
259 | result.push(("replace", s.to_owned()));
260 | }
261 | if let Some(s) = matches.value_of("search_type") {
262 | result.push(("replace_type", s.to_owned()));
263 | }
264 | if let Some(s) = matches.value_of("search_pattern") {
265 | result.push(("replace_pattern", s.to_owned()));
266 | }
267 | if let Some(s) = matches.value_of("search_trait_ref") {
268 | result.push(("replace_trait_ref", s.to_owned()));
269 | }
270 | if result.len() > 1 {
271 | result.clear();
272 | }
273 | result.into_iter().next().ok_or_else(|| {
274 | format_err!("--replace_with requires exactly one kind of --search* argument is required.")
275 | })
276 | }
277 |
278 | fn cargo_rerast() -> Result<(), Error> {
279 | let mut args: Vec = std::env::args().collect();
280 | // We want the help message to say "cargo rerast" not "cargo-rerast rerast".
281 | args[0] = "cargo".to_owned();
282 | let matches = clap::App::new("Rerast")
283 | .subcommand(
284 | clap::SubCommand::with_name("rerast")
285 | .about("Replace Rust code based on typed, syntactic patterns")
286 | .args_from_usage(
287 | "--rules_file=[FILE] 'Path to a rule file'
288 | --use=[USE_STATEMENT]... 'Use statements required by rule'
289 | -p, --placeholders=[PLACEHOLDERS] 'e.g. (o: option) or just \
290 | o: Option'
291 | -s, --search=[CODE] 'Expression to search for'
292 | --search_type=[CODE] 'Type to search for'
293 | --search_pattern=[CODE] 'Pattern to search for'
294 | --search_trait_ref=[TRAIT] 'Trait to search for'
295 | -r, --replace_with=[CODE] 'Replacement code'
296 | --file=[FILE]... 'Only apply to these root files and their submodules'
297 | --diff_cmd=[COMMAND] 'Diff changes with the specified diff command'
298 | --color=[always/never] 'Force color on or off'
299 | --debug_snippet=[CODE_SNIPPET] 'A snippet of code that you think should \
300 | match or list_all to list all checked \
301 | snippets.'
302 | --targets=[ARGS]... 'Build targets to process. Defaults to: \
303 | tests examples benches.'
304 | --crate_root=[DIR] 'Root directory of crate. Defaults to current directory.'
305 | \
306 | --diff 'Diff changes'
307 | --force 'Overwrite files',
308 | --backup 'Rename old files with a .bk extension',
309 | --replay_git 'Detect and replay existing unstaged git change',
310 | --verbose 'Print additional information about what's happening'",
311 | ),
312 | )
313 | .get_matches_from(&args);
314 | let matches = matches.subcommand_matches("rerast").ok_or_else(|| {
315 | format_err!("This binary is intended to be run as `cargo rerast` not run directly.")
316 | })?;
317 | let config = Config {
318 | verbose: matches.is_present("verbose"),
319 | debug_snippet: matches.value_of("debug_snippet").unwrap_or("").to_owned(),
320 | files: values_t!(matches.values_of("file"), String).ok(),
321 | };
322 | match matches.value_of("color") {
323 | Some("always") => colored::control::set_override(true),
324 | Some("never") => colored::control::set_override(false),
325 | Some(v) => bail!("Invalid value for --color: {}", v),
326 | _ => {}
327 | }
328 | // The rules files is relative to the original working directory (or
329 | // absolute), so we need to remember where that is before changing
330 | // the current directory.
331 | let here = std::env::current_dir()?;
332 | if let Some(crate_root) = matches.value_of("crate_root") {
333 | std::env::set_current_dir(crate_root)?;
334 | } else if let Some(s) = &metadata()?["workspace_root"].as_str() {
335 | std::env::set_current_dir(s)?;
336 | }
337 | let mut maybe_compiler_invocation_infos = None;
338 | let rules = if let Some(replacement) = matches.value_of("replace_with") {
339 | let (replace_kind, search) = get_replacement_kind_and_arg(matches)?;
340 | let mut placeholders = matches.value_of("placeholders").unwrap_or("").to_owned();
341 | // --placholders can be either just what's inside the parenthesis, or if
342 | // generics are needed, then "<...>(...)". For consitency, we also allow
343 | // the form "(...)" even though it could be written without parenthesis.
344 | if !(placeholders.starts_with('<') || placeholders.starts_with("(")) {
345 | placeholders = "(".to_owned() + &placeholders + ")";
346 | }
347 | let mut rules = String::new();
348 | if let Some(deps) = matches.values_of("use") {
349 | for dependency in deps {
350 | rules.push_str("use ");
351 | rules.push_str(dependency);
352 | rules.push_str(";\n");
353 | }
354 | }
355 | rules.push_str("pub fn rule");
356 | rules.push_str(&placeholders);
357 | rules.push_str("{");
358 | rules.push_str(replace_kind);
359 | rules.push_str("!(");
360 | rules.push_str(&search);
361 | rules.push_str(" => ");
362 | rules.push_str(replacement);
363 | rules.push_str(");}");
364 | rules
365 | } else if matches.is_present("replay_git") {
366 | let compiler_invocation_infos = get_compiler_invocation_infos_for_local_package(&matches)?;
367 | let rule_from_change = derive_rule_from_git_change(&compiler_invocation_infos)?;
368 | maybe_compiler_invocation_infos = Some(compiler_invocation_infos);
369 | println!("Generated rule:\n{}\n", rule_from_change);
370 | rule_from_change
371 | } else if matches.is_present("search")
372 | || matches.is_present("search_type")
373 | || matches.is_present("search_pattern")
374 | || matches.is_present("search_trait_ref")
375 | {
376 | bail!("Searching without --replace_with is not yet implemented");
377 | } else if let Some(rules_file) = matches.value_of("rules_file") {
378 | read_file_as_string(&here.join(rules_file))?
379 | } else {
380 | bail!("Must specify either --rules_file or both of --search and --replacement");
381 | };
382 | let action = Action::from_matches(matches)?;
383 | if config.verbose {
384 | println!("Running cargo check in order to build dependencies and get rustc commands");
385 | }
386 | // Get rustc command lines if we haven't already gotten them.
387 | let compiler_invocation_infos = if let Some(existing_value) = maybe_compiler_invocation_infos {
388 | existing_value
389 | } else {
390 | get_compiler_invocation_infos_for_local_package(&matches)?
391 | };
392 |
393 | let mut updates_to_apply = RerastOutput::default();
394 | for rustc_invocation_info in &compiler_invocation_infos {
395 | if config.verbose {
396 | use itertools::Itertools;
397 | println!(
398 | "Running rustc internally as:\n{} {}",
399 | rustc_invocation_info
400 | .env
401 | .iter()
402 | .map(|(k, v)| format!("{}={:?}", k, v))
403 | .join(" "),
404 | rustc_invocation_info.args.join(" ")
405 | );
406 | }
407 | let driver = RerastCompilerDriver::new(rustc_invocation_info.clone());
408 | let code_filename = driver.code_filename().ok_or_else(|| {
409 | format_err!(
410 | "Failed to determine code filename from: {:?}",
411 | &rustc_invocation_info.args[2..]
412 | )
413 | })?;
414 | if config.verbose {
415 | println!("Processing {}", code_filename);
416 | }
417 |
418 | let output_or_error =
419 | driver.apply_rules_from_string(rules.clone(), config.clone(), RealFileLoader);
420 | match output_or_error {
421 | Ok(output) => {
422 | updates_to_apply.merge(output);
423 | }
424 | Err(errors) => {
425 | bail!("{}", errors);
426 | }
427 | }
428 | }
429 | let updated_files = updates_to_apply.updated_files(&RealFileLoader)?;
430 | if config.verbose && updated_files.is_empty() {
431 | println!("No matches found");
432 | }
433 | for (filename, new_contents) in updated_files {
434 | action.process(&filename, &new_contents)?;
435 | }
436 | Ok(())
437 | }
438 |
439 | fn derive_rule_from_git_change(
440 | invocation_infos: &[CompilerInvocationInfo],
441 | ) -> Result {
442 | let git_diff_output = std::process::Command::new("git")
443 | .arg("diff")
444 | .arg("--name-only")
445 | .arg("--relative")
446 | .arg(".")
447 | .stdout(std::process::Stdio::piped())
448 | .output()?;
449 |
450 | let changed_files: Vec<&str> = std::str::from_utf8(&git_diff_output.stdout)?
451 | .lines()
452 | .collect();
453 | ensure!(
454 | !changed_files.is_empty(),
455 | "According to git diff, no files have been changed"
456 | );
457 | ensure!(
458 | changed_files.len() == 1,
459 | "According to git diff, multiple have been changed"
460 | );
461 | let changed_filename = changed_files[0];
462 |
463 | let git_show_output = std::process::Command::new("git")
464 | .arg("show")
465 | .arg(format!(":{}", changed_filename))
466 | .stdout(std::process::Stdio::piped())
467 | .output()?;
468 | let original_file_contents = std::str::from_utf8(&git_show_output.stdout)?;
469 |
470 | match rerast::change_to_rule::determine_rule(
471 | invocation_infos,
472 | changed_filename,
473 | original_file_contents,
474 | ) {
475 | Ok(rule) => Ok(rule),
476 | Err(errors) => bail!("{}", errors),
477 | }
478 | }
479 |
480 | fn pass_through_to_actual_compiler() {
481 | let args: Vec<_> = std::env::args().skip(2).collect();
482 | std::process::Command::new("rustc")
483 | .args(args)
484 | .status()
485 | .expect("Failed to run rustc");
486 | }
487 |
488 | pub fn main() {
489 | // See issue #17. May be able to be removed in the future.
490 | std::env::set_var("CARGO_INCREMENTAL", "0");
491 | let driver =
492 | RerastCompilerDriver::new(CompilerInvocationInfo::from_args(std::env::args().skip(1)));
493 | if std::env::var(var_names::PRINT_ARGS_JSON).is_ok() {
494 | // If cargo is calling us to get compiler configuration or is compiling
495 | // a dependent crate, then just run the compiler normally.
496 | if driver.args().has_arg("--print=cfg") || driver.is_compiling_dependency() {
497 | pass_through_to_actual_compiler();
498 | } else {
499 | let json_args: Vec = std::env::args().map(JsonValue::String).collect();
500 | let mut env_vars = JsonValue::new_object();
501 | for (k, v) in std::env::vars() {
502 | if k.starts_with("CARGO") || k == "OUT_DIR" {
503 | env_vars[k] = v.into();
504 | }
505 | }
506 | println!(
507 | "{}{}",
508 | RERAST_JSON_MARKER,
509 | JsonValue::Array(vec![JsonValue::Array(json_args), env_vars]).dump()
510 | );
511 | pass_through_to_actual_compiler();
512 | }
513 | } else if let Err(error) = cargo_rerast() {
514 | eprintln!("{}", error);
515 | std::process::exit(-1);
516 | }
517 | }
518 |
--------------------------------------------------------------------------------
/src/change_to_rule.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2017 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Given two versions of a source file, finds what's changed and builds a Rerast rule to reproduce
16 | // the change.
17 |
18 | extern crate rustc_ast;
19 | extern crate rustc_driver;
20 | extern crate rustc_hir;
21 | extern crate rustc_middle;
22 | extern crate rustc_parse;
23 | extern crate rustc_session;
24 | extern crate rustc_span;
25 |
26 | use crate::errors;
27 | use crate::errors::RerastErrors;
28 | use crate::file_loader::{ClonableRealFileLoader, InMemoryFileLoader};
29 | use crate::CompilerInvocationInfo;
30 | use rustc_ast::tokenstream::{TokenStream, TokenTree};
31 | use rustc_hir::intravisit;
32 | use rustc_interface::interface;
33 | use rustc_middle::ty::subst::GenericArgKind;
34 | use rustc_middle::ty::{TyCtxt, TyKind};
35 | use rustc_session::parse::ParseSess;
36 | use rustc_span::source_map::{FileLoader, FilePathMapping, SourceMap};
37 | use rustc_span::{BytePos, Pos, Span, SyntaxContext};
38 | use std::collections::hash_map::{DefaultHasher, HashMap};
39 | use std::collections::hash_set::HashSet;
40 | use std::fmt::Write;
41 | use std::hash::{Hash, Hasher};
42 | use std::ops::Range;
43 | use std::path::{Path, PathBuf};
44 | use std::rc::Rc;
45 |
46 | struct PlaceholderCandidate {
47 | hash: u64,
48 | children: Vec>,
49 | data: T,
50 | }
51 |
52 | impl PlaceholderCandidate {
53 | fn new(data: T) -> PlaceholderCandidate {
54 | PlaceholderCandidate {
55 | hash: 0,
56 | data,
57 | children: Vec::new(),
58 | }
59 | }
60 | }
61 |
62 | impl Hash for PlaceholderCandidate {
63 | fn hash(&self, hasher: &mut H) {
64 | hasher.write_u64(self.hash);
65 | }
66 | }
67 |
68 | struct Placeholder<'tcx> {
69 | expr: &'tcx rustc_hir::Expr<'tcx>,
70 | uses: Vec,
71 | }
72 |
73 | fn default_hash(value: &T) -> u64 {
74 | let mut hasher = DefaultHasher::new();
75 | value.hash(&mut hasher);
76 | hasher.finish()
77 | }
78 |
79 | fn hash_token_stream(stream: &TokenStream, hasher: &mut DefaultHasher) {
80 | for tt in stream.trees() {
81 | match tt {
82 | TokenTree::Token(_token) => {
83 | // If hash collisions become enough of a problem that we get bad performance, we'll
84 | // probably need to look into the structure of the token and hash that. In the mean
85 | // time, lets just hash an arbitrary constant value. At least expressions with
86 | // different tree structures will likely get different hashes.
87 | 42.hash(hasher)
88 | }
89 | TokenTree::Delimited(_span, _delimited, tts) => hash_token_stream(&tts, hasher),
90 | }
91 | }
92 | }
93 |
94 | struct PlaceholderCandidateFinder<'tcx, T, F>
95 | where
96 | F: Fn(&'tcx rustc_hir::Expr<'tcx>) -> T,
97 | {
98 | tcx: TyCtxt<'tcx>,
99 | stack: Vec>,
100 | data_fn: F,
101 | }
102 |
103 | impl<'tcx, T, F> PlaceholderCandidateFinder<'tcx, T, F>
104 | where
105 | F: Fn(&'tcx rustc_hir::Expr<'tcx>) -> T,
106 | {
107 | fn find_placeholder_candidates(
108 | tcx: TyCtxt<'tcx>,
109 | node: &'tcx rustc_hir::Expr<'tcx>,
110 | data_fn: F,
111 | ) -> Vec> {
112 | let mut state = PlaceholderCandidateFinder {
113 | tcx,
114 | stack: vec![PlaceholderCandidate::new(data_fn(node))],
115 | data_fn,
116 | };
117 | state.walk_expr_children(node);
118 | state.stack.pop().unwrap().children
119 | }
120 |
121 | fn walk_expr_children(&mut self, expr: &'tcx rustc_hir::Expr<'tcx>) {
122 | if let rustc_hir::ExprKind::Call(ref _expr_fn, ref args) = expr.kind {
123 | // Ignore expr_fn as a candidate, just consider the args.
124 | for arg in *args {
125 | use rustc_hir::intravisit::Visitor;
126 | self.visit_expr(arg);
127 | }
128 | } else {
129 | intravisit::walk_expr(self, expr);
130 | }
131 | }
132 | }
133 |
134 | impl<'tcx, T, F> intravisit::Visitor<'tcx> for PlaceholderCandidateFinder<'tcx, T, F>
135 | where
136 | F: Fn(&'tcx rustc_hir::Expr<'tcx>) -> T,
137 | {
138 | type Map = rustc_middle::hir::map::Map<'tcx>;
139 |
140 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap {
141 | intravisit::NestedVisitorMap::All(self.tcx.hir())
142 | }
143 |
144 | fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'tcx>) {
145 | self.stack
146 | .push(PlaceholderCandidate::new((self.data_fn)(expr)));
147 | self.walk_expr_children(expr);
148 | // We pushed to the stack. So long as all pushes and pops are matched, we should be able to
149 | // safely pop.
150 | let mut candidate = self.stack.pop().unwrap();
151 | candidate.hash = if candidate.children.is_empty() {
152 | // Leaf node. Get a token stream and hash its tokens.
153 | let snippet = self
154 | .tcx
155 | .sess
156 | .source_map()
157 | .span_to_snippet(expr.span)
158 | .unwrap();
159 | let session = ParseSess::new(FilePathMapping::empty());
160 | let stream = rustc_parse::parse_stream_from_source_str(
161 | rustc_span::FileName::anon_source_code(&snippet),
162 | snippet,
163 | &session,
164 | None,
165 | );
166 | let mut hasher = DefaultHasher::new();
167 | hash_token_stream(&stream, &mut hasher);
168 | hasher.finish()
169 | } else {
170 | // Non-leaf node. Just combine the already computed hashes of our children.
171 | default_hash(&candidate.children)
172 | };
173 | // There should at least be the root still on the stack.
174 | self.stack.last_mut().unwrap().children.push(candidate);
175 | }
176 | }
177 |
178 | fn span_within_span(span: Span, target: Span) -> Span {
179 | if target.contains(span) {
180 | span
181 | } else {
182 | let expn_info = span.ctxt().outer_expn().expn_data();
183 | span_within_span(expn_info.call_site, target)
184 | }
185 | }
186 |
187 | struct RelativeSpan(Range);
188 |
189 | impl RelativeSpan {
190 | fn new(absolute_span: Span, filemap: &rustc_span::SourceFile) -> RelativeSpan {
191 | let absolute_span = span_within_span(
192 | absolute_span,
193 | Span::with_root_ctxt(filemap.start_pos, filemap.end_pos),
194 | );
195 | let start_pos = filemap.start_pos;
196 | assert!(absolute_span.lo() >= start_pos);
197 | assert!(absolute_span.hi() <= filemap.end_pos);
198 | RelativeSpan((absolute_span.lo() - start_pos)..(absolute_span.hi() - start_pos))
199 | }
200 |
201 | fn absolute(&self, filemap: &rustc_span::SourceFile) -> Span {
202 | let start_pos = filemap.start_pos;
203 | let result = Span::with_root_ctxt(self.0.start + start_pos, self.0.end + start_pos);
204 | assert!(result.lo() >= filemap.start_pos);
205 | assert!(result.hi() <= filemap.end_pos);
206 | result
207 | }
208 | }
209 |
210 | // The span of a file that has changed. Start and end are relative to the start and end of the file,
211 | // which makes the files the same in both the changed and the original version of the file.
212 | #[derive(Eq, PartialEq, Debug, Copy, Clone)]
213 | struct ChangedSpan {
214 | common_prefix: usize,
215 | common_suffix: usize,
216 | }
217 |
218 | impl ChangedSpan {
219 | fn new(common_prefix: usize, common_suffix: usize) -> ChangedSpan {
220 | ChangedSpan {
221 | common_prefix,
222 | common_suffix,
223 | }
224 | }
225 |
226 | fn from_span(span: Span, filemap: &rustc_span::SourceFile) -> ChangedSpan {
227 | ChangedSpan {
228 | common_prefix: (span.lo() - filemap.start_pos).to_usize(),
229 | common_suffix: (filemap.end_pos - span.hi()).to_usize(),
230 | }
231 | }
232 |
233 | fn to_span(&self, filemap: &rustc_span::SourceFile) -> Span {
234 | Span::new(
235 | filemap.start_pos + BytePos::from_usize(self.common_prefix),
236 | filemap.end_pos - BytePos::from_usize(self.common_suffix),
237 | SyntaxContext::root(),
238 | )
239 | }
240 | }
241 |
242 | struct ChangedSideState {
243 | candidate_placeholders: Vec>,
244 | required_paths: HashSet,
245 | }
246 |
247 | struct FindRulesState {
248 | modified_file_name: String,
249 | modified_source: String,
250 | changed_span: ChangedSpan,
251 | result: String,
252 | changed_side_state: Option,
253 | diagnostic_output: errors::DiagnosticOutput,
254 | }
255 |
256 | impl rustc_driver::Callbacks for FindRulesState {
257 | fn config(&mut self, config: &mut rustc_interface::interface::Config) {
258 | config.diagnostic_output =
259 | rustc_session::DiagnosticOutput::Raw(Box::new(self.diagnostic_output.clone()));
260 | }
261 |
262 | fn after_analysis<'tcx>(
263 | &mut self,
264 | compiler: &interface::Compiler,
265 | queries: &'tcx rustc_interface::Queries<'tcx>,
266 | ) -> rustc_driver::Compilation {
267 | compiler.session().abort_if_errors();
268 | queries.global_ctxt().unwrap().peek_mut().enter(|tcx| {
269 | let source_map = tcx.sess.source_map();
270 | let maybe_filemap = source_map.get_source_file(&rustc_span::FileName::Real(
271 | rustc_span::RealFileName::Named(PathBuf::from(&self.modified_file_name)),
272 | ));
273 | let filemap = if let Some(f) = maybe_filemap {
274 | f
275 | } else {
276 | return rustc_driver::Compilation::Stop;
277 | };
278 | let span = self.changed_span.to_span(&filemap);
279 | let mut rule_finder = RuleFinder {
280 | tcx,
281 | changed_span: span,
282 | candidate: Node::NotFound,
283 | body_id: None,
284 | current_item: None,
285 | };
286 | intravisit::walk_crate(&mut rule_finder, tcx.hir().krate());
287 | match rule_finder.candidate {
288 | Node::NotFound => {}
289 | Node::Expr(expr, body_id, item) => {
290 | let new_changed_span = ChangedSpan::from_span(expr.span, &filemap);
291 | if let Some(ref changed_side_state) = self.changed_side_state {
292 | // Presence of changed_side_state means that we've already run on the changed
293 | // version of the source. We're now running on the original source.
294 | if self.changed_span != new_changed_span {
295 | self.changed_span = new_changed_span;
296 | } else {
297 | self.result = analyse_original_source(
298 | tcx,
299 | changed_side_state,
300 | expr,
301 | &self.changed_span,
302 | self.modified_source.clone(),
303 | body_id,
304 | item,
305 | );
306 | }
307 | } else {
308 | // First rustc invocation. We're running on the changed version of the source. Find
309 | // which bit of the HIR tree has changed and select candidate placeholders.
310 | self.changed_span = new_changed_span;
311 | self.changed_side_state = Some(ChangedSideState {
312 | candidate_placeholders:
313 | PlaceholderCandidateFinder::find_placeholder_candidates(
314 | tcx,
315 | expr,
316 | |child_expr| RelativeSpan::new(child_expr.span, &filemap),
317 | ),
318 | required_paths: ReferencedPathsFinder::paths_in_expr(tcx, expr),
319 | })
320 | }
321 | }
322 | }
323 | rustc_driver::Compilation::Stop
324 | })
325 | }
326 | }
327 |
328 | fn analyse_original_source<'a, 'tcx: 'a>(
329 | tcx: TyCtxt<'tcx>,
330 | changed_side_state: &ChangedSideState,
331 | expr: &'tcx rustc_hir::Expr<'tcx>,
332 | changed_span: &ChangedSpan,
333 | modified_source: String,
334 | body_id: rustc_hir::BodyId,
335 | item: &'tcx rustc_hir::Item,
336 | ) -> String {
337 | let source_map = tcx.sess.source_map();
338 | let mut others_by_hash = HashMap::new();
339 | populate_placeholder_map(
340 | &changed_side_state.candidate_placeholders,
341 | &mut others_by_hash,
342 | );
343 | let mut candidates =
344 | PlaceholderCandidateFinder::find_placeholder_candidates(tcx, expr, |child_expr| child_expr);
345 | let other_filemap = source_map.new_source_file(
346 | rustc_span::FileName::Custom("__other_source".to_owned()),
347 | modified_source,
348 | );
349 | let replacement_span = changed_span.to_span(&*other_filemap);
350 | let mut matcher = PlaceholderMatcher {
351 | tcx,
352 | other_filemap,
353 | other_candidates: others_by_hash,
354 | placeholders_found: Vec::new(),
355 | used_placeholder_spans: Vec::new(),
356 | };
357 | matcher.find_placeholders(&mut candidates);
358 | // Sort placeholders by the order they appear in the search expression, since this seems a bit
359 | // more natural to read.
360 | matcher.placeholders_found.sort_by_key(|p| p.expr.span.lo());
361 | build_rule(
362 | tcx,
363 | &matcher.placeholders_found,
364 | expr,
365 | body_id,
366 | item,
367 | &changed_side_state.required_paths,
368 | replacement_span,
369 | )
370 | }
371 |
372 | fn build_rule<'a, 'tcx: 'a>(
373 | tcx: TyCtxt<'tcx>,
374 | placeholders: &[Placeholder<'tcx>],
375 | expr: &'tcx rustc_hir::Expr,
376 | body_id: rustc_hir::BodyId,
377 | item: &'tcx rustc_hir::Item,
378 | right_paths: &HashSet,
379 | replacement_span: Span,
380 | ) -> String {
381 | let source_map = tcx.sess.source_map();
382 | let type_tables = tcx.body_tables(body_id);
383 | let mut uses_type_params = false;
384 | for ph in placeholders {
385 | let ph_ty = type_tables.expr_ty(ph.expr);
386 | for generic in ph_ty.walk() {
387 | if let GenericArgKind::Type(subtype) = generic.unpack() {
388 | if let TyKind::Param(..) = subtype.kind {
389 | uses_type_params = true;
390 | }
391 | }
392 | }
393 | }
394 | let generics_string;
395 | let where_string;
396 | if uses_type_params {
397 | if let Some(generics) = item.kind.generics() {
398 | generics_string = source_map.span_to_snippet(generics.span).unwrap();
399 | let mut where_predicate_strings = Vec::new();
400 | for predicate in generics.where_clause.predicates {
401 | let span = match *predicate {
402 | rustc_hir::WherePredicate::BoundPredicate(ref p) => p.span,
403 | rustc_hir::WherePredicate::RegionPredicate(ref p) => p.span,
404 | rustc_hir::WherePredicate::EqPredicate(ref p) => p.span,
405 | };
406 | where_predicate_strings.push(source_map.span_to_snippet(span).unwrap());
407 | }
408 | where_string = if where_predicate_strings.is_empty() {
409 | String::new()
410 | } else {
411 | format!(" where {}", where_predicate_strings.join(", "))
412 | };
413 | } else {
414 | panic!("Item has no generics, but type appears to have type parameters");
415 | }
416 | } else {
417 | generics_string = String::new();
418 | where_string = String::new();
419 | }
420 | let arg_decls = placeholders
421 | .iter()
422 | .enumerate()
423 | .map(|(ph_num, ph)| format!("p{}: {}", ph_num, type_tables.expr_ty(ph.expr)))
424 | .collect::>()
425 | .join(", ");
426 | let mut use_statements = String::new();
427 | let left_paths = ReferencedPathsFinder::paths_in_expr(tcx, expr);
428 | let mut paths: Vec<_> = left_paths.union(right_paths).collect();
429 | paths.sort();
430 | for path in paths {
431 | use_statements += &format!("use {}; ", path);
432 | }
433 |
434 | let mut search_substitutions = Vec::new();
435 | let mut replacement_substitutions = Vec::new();
436 | for (ph_num, ph) in placeholders.iter().enumerate() {
437 | search_substitutions.push((ph.expr.span, format!("p{}", ph_num)));
438 | for usage in &ph.uses {
439 | replacement_substitutions.push((*usage, format!("p{}", ph_num)));
440 | }
441 | }
442 | let search = substitute_placeholders(source_map, expr.span, &mut search_substitutions);
443 | let replace =
444 | substitute_placeholders(source_map, replacement_span, &mut replacement_substitutions);
445 | format!(
446 | "{}fn r1{}({}){} {{replace!({} => {});}}",
447 | use_statements, generics_string, arg_decls, where_string, search, replace
448 | )
449 | }
450 |
451 | fn substitute_placeholders(
452 | source_map: &SourceMap,
453 | span: Span,
454 | substitutions: &mut [(Span, String)],
455 | ) -> String {
456 | substitutions.sort_by_key(|v| v.0.lo());
457 | let mut result = String::new();
458 | let mut start = span.lo();
459 | for &(subst_span, ref substitution) in substitutions.iter() {
460 | result += &source_map
461 | .span_to_snippet(Span::with_root_ctxt(start, subst_span.lo()))
462 | .unwrap();
463 | result += substitution;
464 | start = subst_span.hi();
465 | }
466 | result += &source_map
467 | .span_to_snippet(Span::with_root_ctxt(start, span.hi()))
468 | .unwrap();
469 | result
470 | }
471 |
472 | struct PlaceholderMatcher<'tcx, 'placeholders> {
473 | tcx: TyCtxt<'tcx>,
474 | other_filemap: Rc,
475 | other_candidates: HashMap>>,
476 | placeholders_found: Vec>,
477 | used_placeholder_spans: Vec,
478 | }
479 |
480 | impl<'tcx, 'placeholders> PlaceholderMatcher<'tcx, 'placeholders> {
481 | fn find_placeholders(
482 | &mut self,
483 | candidates: &mut [PlaceholderCandidate<&'tcx rustc_hir::Expr>],
484 | ) {
485 | // Sort candidates with longest first so that they take precedence.
486 | candidates.sort_by_key(|p| p.data.span.lo() - p.data.span.hi());
487 | for candidate in candidates {
488 | let mut got_match = false;
489 | if let Some(matching_others) = self.other_candidates.get(&candidate.hash) {
490 | let source_map = self.tcx.sess.source_map();
491 | let source = source_map.span_to_snippet(candidate.data.span).unwrap();
492 | let mut placeholder = Placeholder {
493 | expr: candidate.data,
494 | uses: Vec::new(),
495 | };
496 | for other in matching_others {
497 | let session = ParseSess::new(FilePathMapping::empty());
498 | let stream = rustc_parse::parse_stream_from_source_str(
499 | rustc_span::FileName::Custom("left".to_owned()),
500 | source.clone(),
501 | &session,
502 | None,
503 | );
504 | let other_span = other.data.absolute(&*self.other_filemap);
505 | let other_source = source_map.span_to_snippet(other_span).unwrap();
506 | let other_stream = rustc_parse::parse_stream_from_source_str(
507 | rustc_span::FileName::Custom("right".to_owned()),
508 | other_source,
509 | &session,
510 | None,
511 | );
512 | if stream.eq_unspanned(&other_stream)
513 | && !self
514 | .used_placeholder_spans
515 | .iter()
516 | .any(|s| s.contains(other_span) || other_span.contains(*s))
517 | {
518 | self.used_placeholder_spans.push(other_span);
519 | placeholder.uses.push(other_span);
520 | break;
521 | }
522 | }
523 | if !placeholder.uses.is_empty() {
524 | self.placeholders_found.push(placeholder);
525 | got_match = true;
526 | }
527 | }
528 | if !got_match {
529 | // Placeholders can't overlap - only look for matches in our children if we didn't
530 | // match.
531 | self.find_placeholders(&mut candidate.children);
532 | }
533 | }
534 | }
535 | }
536 |
537 | fn populate_placeholder_map<'a, T>(
538 | candidates: &'a [PlaceholderCandidate],
539 | map_out: &mut HashMap>>,
540 | ) {
541 | for candidate in candidates {
542 | map_out
543 | .entry(candidate.hash)
544 | .or_insert_with(Vec::new)
545 | .push(candidate);
546 | populate_placeholder_map(&candidate.children, map_out);
547 | }
548 | }
549 |
550 | // Finds referenced item paths and builds use statements that import those paths.
551 | struct ReferencedPathsFinder<'tcx> {
552 | tcx: TyCtxt<'tcx>,
553 | result: HashSet,
554 | }
555 |
556 | impl<'tcx> ReferencedPathsFinder<'tcx> {
557 | fn paths_in_expr(tcx: TyCtxt<'tcx>, expr: &'tcx rustc_hir::Expr) -> HashSet {
558 | let mut finder = ReferencedPathsFinder {
559 | tcx,
560 | result: HashSet::new(),
561 | };
562 | intravisit::walk_expr(&mut finder, expr);
563 | finder.result
564 | }
565 | }
566 |
567 | impl<'tcx> intravisit::Visitor<'tcx> for ReferencedPathsFinder<'tcx> {
568 | type Map = rustc_middle::hir::map::Map<'tcx>;
569 |
570 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap {
571 | intravisit::NestedVisitorMap::All(self.tcx.hir())
572 | }
573 |
574 | fn visit_path(&mut self, path: &'tcx rustc_hir::Path, _: rustc_hir::HirId) {
575 | if let rustc_hir::def::Res::Def(_, def_id) = path.res {
576 | let mut qualified_path = String::new();
577 | for component in self.tcx.def_path(def_id).data {
578 | write!(qualified_path, "::{}", component.data.as_symbol()).unwrap();
579 | }
580 | self.result.insert(qualified_path);
581 | }
582 | }
583 | }
584 |
585 | enum Node<'tcx> {
586 | NotFound,
587 | Expr(
588 | &'tcx rustc_hir::Expr<'tcx>,
589 | rustc_hir::BodyId,
590 | &'tcx rustc_hir::Item<'tcx>,
591 | ),
592 | }
593 |
594 | struct RuleFinder<'tcx> {
595 | tcx: TyCtxt<'tcx>,
596 | changed_span: Span,
597 | candidate: Node<'tcx>,
598 | body_id: Option,
599 | current_item: Option<&'tcx rustc_hir::Item<'tcx>>,
600 | }
601 |
602 | impl<'tcx> intravisit::Visitor<'tcx> for RuleFinder<'tcx> {
603 | type Map = rustc_middle::hir::map::Map<'tcx>;
604 |
605 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap {
606 | intravisit::NestedVisitorMap::All(self.tcx.hir())
607 | }
608 |
609 | fn visit_item(&mut self, item: &'tcx rustc_hir::Item) {
610 | // TODO: Avoid visiting items that we know don't contain the changed code. Just need to make
611 | // sure we still visit mod items where the module code is in another file.
612 | let old_item = self.current_item;
613 | self.current_item = Some(item);
614 | intravisit::walk_item(self, item);
615 | self.current_item = old_item;
616 | }
617 |
618 | fn visit_body(&mut self, body: &'tcx rustc_hir::Body) {
619 | let old_body_id = self.body_id;
620 | self.body_id = Some(body.id());
621 | intravisit::walk_body(self, body);
622 | self.body_id = old_body_id;
623 | }
624 |
625 | fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr) {
626 | if expr.span.from_expansion() {
627 | intravisit::walk_expr(self, expr);
628 | } else if expr.span.contains(self.changed_span) {
629 | if let (Some(body_id), Some(item)) = (self.body_id, self.current_item) {
630 | self.candidate = Node::Expr(expr, body_id, item);
631 | intravisit::walk_expr(self, expr);
632 | } else {
633 | // TODO: Consider better error handling - assuming it's actually possible to trip
634 | // this. Static initializer? Or does that have a body?
635 | panic!("Changing expressions outside of bodies isn't supported");
636 | }
637 | }
638 | }
639 | }
640 |
641 | pub fn determine_rule(
642 | compiler_invocations: &[CompilerInvocationInfo],
643 | modified_file_name: &str,
644 | original_file_contents: &str,
645 | ) -> Result {
646 | determine_rule_with_file_loader(
647 | &ClonableRealFileLoader,
648 | compiler_invocations,
649 | modified_file_name,
650 | original_file_contents,
651 | )
652 | }
653 |
654 | fn determine_rule_with_file_loader(
655 | file_loader: &T,
656 | compiler_invocations: &[CompilerInvocationInfo],
657 | modified_file_name: &str,
658 | original_file_contents: &str,
659 | ) -> Result {
660 | let right = file_loader.read_file(Path::new(modified_file_name))?;
661 | let changed_span = match common(original_file_contents, &right) {
662 | Some(c) => c,
663 | None => {
664 | return Err(RerastErrors::with_message(
665 | "Nothing appears to have changed",
666 | ));
667 | }
668 | };
669 | let mut find_rules_state = FindRulesState {
670 | modified_file_name: modified_file_name.to_owned(),
671 | modified_source: right.clone(),
672 | changed_span,
673 | changed_side_state: None,
674 | result: String::new(),
675 | diagnostic_output: errors::DiagnosticOutput::new(),
676 | };
677 |
678 | let mut args_index = 0;
679 | let sysroot = crate::rust_sysroot()?;
680 | loop {
681 | // Run rustc on modified source to find HIR node that encloses changed code as well as
682 | // subnodes that will be candidates for placeholders.
683 | let invocation_info = compiler_invocations[args_index]
684 | .clone()
685 | .arg("--sysroot")
686 | .arg(&sysroot)
687 | .arg("--allow")
688 | .arg("dead_code");
689 | if invocation_info
690 | .run_compiler(&mut find_rules_state, Some(box file_loader.clone()))
691 | .is_err()
692 | {
693 | return Err(find_rules_state.diagnostic_output.errors());
694 | }
695 | if find_rules_state.changed_side_state.is_none() {
696 | // Span was not found with these compiler args, try the next command line.
697 | args_index += 1;
698 | if args_index >= compiler_invocations.len() {
699 | return Err(RerastErrors::with_message(
700 | "Failed to find a modified expression",
701 | ));
702 | }
703 | continue;
704 | }
705 | let right_side_changed_span = find_rules_state.changed_span;
706 |
707 | // Run rustc on original source to confirm matching HIR node exists and to match
708 | // placeholders.
709 | let mut original_file_loader = box InMemoryFileLoader::new(file_loader.clone());
710 | original_file_loader.add_file(
711 | modified_file_name.to_owned(),
712 | original_file_contents.to_owned(),
713 | );
714 | if invocation_info
715 | .run_compiler(&mut find_rules_state, Some(original_file_loader))
716 | .is_err()
717 | {
718 | return Err(find_rules_state.diagnostic_output.errors());
719 | }
720 |
721 | if right_side_changed_span == find_rules_state.changed_span {
722 | // The changed span after examining the right side matched a full expression on the
723 | // left, so we're done.
724 | break;
725 | }
726 | // The changed span after examing the right side corresponded to only part of an expression
727 | // on the left. We need to go back and reprocess the right with the now widened span.
728 | }
729 |
730 | Ok(find_rules_state.result)
731 | }
732 |
733 | fn common_prefix(left: &str, right: &str) -> Option {
734 | for (i, (l, r)) in left.bytes().zip(right.bytes()).enumerate() {
735 | if l != r {
736 | return Some(i);
737 | }
738 | }
739 | None
740 | }
741 |
742 | fn common_suffix(left: &str, right: &str) -> Option {
743 | for (i, (l, r)) in left.bytes().rev().zip(right.bytes().rev()).enumerate() {
744 | if l != r {
745 | return Some(i);
746 | }
747 | }
748 | None
749 | }
750 |
751 | fn common(left: &str, right: &str) -> Option {
752 | match (common_prefix(left, right), common_suffix(left, right)) {
753 | (Some(prefix), Some(suffix)) => Some(ChangedSpan::new(prefix, suffix)),
754 | _ => None,
755 | }
756 | }
757 |
758 | #[cfg(test)]
759 | mod tests {
760 | use super::*;
761 | use crate::tests::NullFileLoader;
762 |
763 | fn check_determine_rule_with_file_loader(
764 | file_loader: &InMemoryFileLoader,
765 | changed_filename: &str,
766 | original_file_contents: &str,
767 | expected_rule: &str,
768 | ) {
769 | let expected_rule = expected_rule
770 | .lines()
771 | .map(str::trim)
772 | .collect::>()
773 | .join("");
774 | let invocation_info = CompilerInvocationInfo {
775 | args: vec![
776 | "rustc".to_owned(),
777 | "--crate-type".to_owned(),
778 | "lib".to_owned(),
779 | "lib.rs".to_owned(),
780 | ],
781 | env: HashMap::new(),
782 | };
783 | let rule = determine_rule_with_file_loader(
784 | file_loader,
785 | &[invocation_info],
786 | changed_filename,
787 | original_file_contents,
788 | )
789 | .unwrap();
790 | assert_eq!(rule, expected_rule);
791 | }
792 |
793 | fn check_determine_rule(common: &str, left: &str, right: &str, expected_rule: &str) {
794 | let left = common.to_owned() + "\n" + left;
795 | let right = common.to_owned() + "\n" + right;
796 | let mut file_loader = InMemoryFileLoader::new(NullFileLoader);
797 | file_loader.add_file("lib.rs", right);
798 | check_determine_rule_with_file_loader(&file_loader, "lib.rs", &left, expected_rule);
799 | }
800 |
801 | #[test]
802 | fn change_in_separate_file() {
803 | let mut file_loader = InMemoryFileLoader::new(NullFileLoader);
804 | file_loader.add_file("lib.rs", "mod foo;".to_owned());
805 | file_loader.add_file("foo.rs", "fn bar() -> bool {!true}".to_owned());
806 | check_determine_rule_with_file_loader(
807 | &file_loader,
808 | "foo.rs",
809 | "fn bar() -> bool {false}",
810 | "fn r1() {replace!(false => !true);}",
811 | );
812 | }
813 |
814 | #[test]
815 | fn find_changed_range() {
816 | assert_eq!(
817 | Some(ChangedSpan::new(5, 2)),
818 | common("ababcab", "ababcdefab")
819 | );
820 | }
821 |
822 | #[test]
823 | fn determine_rule_basic() {
824 | check_determine_rule(
825 | r#"pub fn bar(x: i32) -> i32 {x}
826 | pub fn baz(x: i32) -> i32 {x}"#,
827 | "fn foo(a: i32, b: i32) -> i32 {if a < b {bar(a * 2 + 1)} else {b}}",
828 | "fn foo(a: i32, b: i32) -> i32 {if a < b {baz(a * 2)} else {b}}",
829 | "use ::bar; use ::baz; fn r1(p0: i32) {replace!(bar(p0 + 1) => baz(p0));}",
830 | );
831 | }
832 |
833 | #[test]
834 | fn swap_order() {
835 | check_determine_rule(
836 | r#"pub fn bar() -> (i32, usize) {(1, 2)}"#,
837 | "fn foo(x: i32, y: i32) -> (i32, i32) {(x + 1, y + 1)}",
838 | "fn foo(x: i32, y: i32) -> (i32, i32) {(y + 1, x + 1)}",
839 | "fn r1(p0: i32, p1: i32) {replace!((p0, p1) => (p1, p0));}",
840 | );
841 | }
842 |
843 | #[test]
844 | fn generic_typed_placeholder() {
845 | check_determine_rule(
846 | "",
847 | "fn foo>(x: T, y: T) -> T {x + y}",
848 | "fn foo>(x: T, y: T) -> T {y + x}",
849 | "fn r1>(p0: T, p1: T) {replace!(p0 + p1 => p1 + p0);}",
850 | );
851 | }
852 |
853 | #[test]
854 | fn generic_typed_placeholder_where_clause() {
855 | check_determine_rule(
856 | "",
857 | "fn foo(x: T, y: T) -> T where T: std::ops::Add