├── .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 {x + y}", 858 | "fn foo(x: T, y: T) -> T where T: std::ops::Add {y + x}", 859 | r#"fn r1(p0: T, p1: T) where T: std::ops::Add { 860 | replace!(p0 + p1 => p1 + p0); 861 | }"#, 862 | ); 863 | } 864 | 865 | #[test] 866 | fn changed_closure() { 867 | check_determine_rule( 868 | "fn r(f: F) -> T where F: FnOnce() -> T {f()}", 869 | "fn foo(x: T, y: T) -> T where T: std::ops::Add {r(|| x + y)}", 870 | "fn foo(x: T, y: T) -> T where T: std::ops::Add {r(|| y + x)}", 871 | r#"fn r1(p0: T, p1: T) where T: std::ops::Add { 872 | replace!(p0 + p1 => p1 + p0); 873 | }"#, 874 | ); 875 | } 876 | 877 | // This test used to pass. Started failing on 2019-08-23. Not sure why. But 878 | // I've never heard of anyone actually using change_to_rule, so am not sure 879 | // I want to spend the time to figure it out. 880 | #[test] 881 | #[ignore] 882 | fn changed_println_arg() { 883 | check_determine_rule( 884 | "", 885 | r#"fn b() {println!("{}", 10 + 1);}"#, 886 | r#"fn b() {println!("{}", 10 - 1);}"#, 887 | "fn r1(p0: i32, p1: i32) {replace!(p0 + p1 => p0 - p1);}", 888 | ); 889 | } 890 | 891 | // When a value (1 in this case) appears multiple times in both the left and right code, 892 | // placeholders should be determined by ordering. i.e. the first 1 in both should be p0. 893 | #[test] 894 | fn repeated_placeholder() { 895 | check_determine_rule( 896 | "", 897 | "fn b() -> i32 {1 + 1}", 898 | "fn b() -> i32 {1 - 1}", 899 | "fn r1(p0: i32, p1: i32) {replace!(p0 + p1 => p0 - p1);}", 900 | ); 901 | } 902 | 903 | // If the changed code references a path, we need to import it into the rule code. 904 | #[test] 905 | fn use_fn_in_submodule() { 906 | check_determine_rule( 907 | "mod m1 { pub mod m11 { pub fn foo(_: i32) {} pub fn bar() {} } }", 908 | "mod m2 { use m1::m11::*; fn b() {foo(1)} }", 909 | "mod m2 { use m1::m11::*; fn b() {bar()} }", 910 | "use ::m1::m11::bar; use ::m1::m11::foo; fn r1() {replace!(foo(1) => bar());}", 911 | ); 912 | } 913 | 914 | // If we didn't give precedence to larger placeholders, the first 2 would match the 2 in 1 + 2, 915 | // then 1 + 2 couldn't be a placeholder so we'd end up with (p1, p1 + p2) instead of (p0, p1). 916 | #[test] 917 | fn larger_placeholder_takes_precedence() { 918 | check_determine_rule( 919 | "", 920 | "fn b() -> (i32, i32) {(2, 1 + 2)}", 921 | "fn b() -> (i32, i32) {(1 + 2, 2)}", 922 | "fn r1(p0: i32, p1: i32) {replace!((p0, p1) => (p1, p0));}", 923 | ); 924 | } 925 | 926 | // Verify that we correctly handle the case where the changed code starts in one expression and 927 | // finishes in another. In this case, if we're not careful, we'll think that our replacement 928 | // pattern is just `2`. Also verify that we don't produce multiple identical use statements. 929 | #[test] 930 | fn changed_span_crosses_expressions_and_deduplication_of_use_statements() { 931 | check_determine_rule( 932 | "fn foo(x: u32) -> u32 {x}", 933 | "fn b() -> u32 {foo(1) + foo(1)}", 934 | "fn b() -> u32 {foo(2)}", 935 | "use ::foo; fn r1() {replace!(foo(1) + foo(1) => foo(2));}", 936 | ); 937 | } 938 | 939 | // Make sure we don't emit unnecessary use statements for references contained within 940 | // code that becomes placeholders. 941 | #[test] 942 | #[ignore] // Not sure if this is worth making pass. 943 | fn function_used_only_from_placeholder() { 944 | check_determine_rule( 945 | "fn foo() -> i32 {1}", 946 | "fn f1() -> i32 {foo() + 1}", 947 | "fn f1() -> i32 {foo() - 1}", 948 | "fn r1(p0: i32, p1: i32) {replace!(p0 + p1 => p0 - p1);}", 949 | ); 950 | } 951 | 952 | // Make sure we don't make a function reference that's part of a function call be a placeholder. 953 | #[test] 954 | fn function_changed_function_arguments() { 955 | check_determine_rule( 956 | "fn foo(_: i32, _: i32) {}", 957 | "fn f1() {foo(1, 2)}", 958 | "fn f1() {foo(2, 1)}", 959 | "use ::foo; fn r1(p0: i32, p1: i32) {replace!(foo(p0, p1) => foo(p1, p0));}", 960 | ); 961 | } 962 | 963 | // Not sure if we actually want to do this. In some ways it would be more consistent if we 964 | // allowed the whole expression to be a placeholder, but we're effectively then matching 965 | // anything of that type. 966 | #[test] 967 | #[ignore] 968 | fn placeholder_is_whole_expression() { 969 | check_determine_rule( 970 | "", 971 | "fn f1() -> bool {true}", 972 | "fn f1() -> bool {!true}", 973 | "fn r1(p0: bool) {replace!(p0 => !p0);}", 974 | ); 975 | } 976 | } 977 | -------------------------------------------------------------------------------- /src/chunked_diff.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 | use colored::Colorize; 16 | use diff; 17 | use std::collections::VecDeque; 18 | use std::fmt; 19 | use std::ops::Range; 20 | 21 | pub fn print_diff(filename: &str, left: &str, right: &str) { 22 | println!("{}", format!("--- {}", filename).red()); 23 | println!("{}", format!("+++ {}", filename).green()); 24 | for chunk in chunked_diff(left, right, 3) { 25 | println!("{}", chunk); 26 | } 27 | } 28 | 29 | struct Chunk<'a> { 30 | lines: Vec>, 31 | left_range: Range, 32 | right_range: Range, 33 | } 34 | 35 | impl<'a> Chunk<'a> { 36 | fn new() -> Chunk<'a> { 37 | Chunk { 38 | lines: Vec::new(), 39 | left_range: 0..0, 40 | right_range: 0..0, 41 | } 42 | } 43 | } 44 | 45 | impl<'a> fmt::Display for Chunk<'a> { 46 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | writeln!( 48 | f, 49 | "{}", 50 | format!( 51 | "@@ -{},{} +{},{} @@", 52 | self.left_range.start, 53 | self.left_range.len(), 54 | self.right_range.start, 55 | self.right_range.len() 56 | ) 57 | .cyan() 58 | )?; 59 | for line in &self.lines { 60 | match *line { 61 | diff::Result::Left(l) => writeln!(f, "{}", format!("-{}", l).red())?, 62 | diff::Result::Right(r) => writeln!(f, "{}", format!("+{}", r).green())?, 63 | diff::Result::Both(l, _) => writeln!(f, " {}", l)?, 64 | } 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | fn chunked_diff<'a>(left: &'a str, right: &'a str, context: usize) -> Vec> { 71 | let mut chunks = Vec::new(); 72 | let mut recent_common = VecDeque::new(); 73 | let mut after_context_remaining = 0; 74 | let mut chunk = Chunk::new(); 75 | let mut left_line_num = 1; 76 | let mut right_line_num = 1; 77 | for diff in diff::lines(left, right) { 78 | let line_delta = match diff { 79 | diff::Result::Left(_) => (1, 0), 80 | diff::Result::Right(_) => (0, 1), 81 | diff::Result::Both(_, _) => (1, 1), 82 | }; 83 | left_line_num += line_delta.0; 84 | right_line_num += line_delta.1; 85 | match diff { 86 | diff::Result::Left(_) | diff::Result::Right(_) => { 87 | if chunk.lines.is_empty() { 88 | chunk.left_range = 89 | left_line_num - recent_common.len() - line_delta.0..left_line_num; 90 | chunk.right_range = 91 | right_line_num - recent_common.len() - line_delta.1..right_line_num; 92 | } 93 | chunk.left_range.end = left_line_num; 94 | chunk.right_range.end = right_line_num; 95 | chunk.lines.extend(recent_common.drain(0..)); 96 | chunk.lines.push(diff); 97 | after_context_remaining = context; 98 | } 99 | diff::Result::Both(_, _) => { 100 | if after_context_remaining > 0 { 101 | chunk.lines.push(diff); 102 | chunk.left_range.end = left_line_num; 103 | chunk.right_range.end = right_line_num; 104 | after_context_remaining -= 1; 105 | } else { 106 | recent_common.push_back(diff); 107 | if recent_common.len() > context { 108 | if !chunk.lines.is_empty() { 109 | chunks.push(chunk); 110 | chunk = Chunk::new(); 111 | } 112 | recent_common.pop_front(); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | if !chunk.lines.is_empty() { 119 | chunks.push(chunk); 120 | } 121 | chunks 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | use colored; 128 | 129 | #[test] 130 | fn changed_at_start() { 131 | let chunks = chunked_diff("1\n2\n3\n", "3\n", 2); 132 | assert_eq!(chunks.len(), 1); 133 | colored::control::set_override(false); 134 | assert_eq!(format!("{}", chunks[0]), "@@ -1,4 +1,2 @@\n-1\n-2\n 3\n \n"); 135 | } 136 | 137 | #[test] 138 | fn changed_at_end() { 139 | let chunks = chunked_diff("1\n2\n3\n", "1\n2\n", 2); 140 | assert_eq!(chunks.len(), 1); 141 | colored::control::set_override(false); 142 | assert_eq!(format!("{}", chunks[0]), "@@ -1,4 +1,3 @@\n 1\n 2\n-3\n \n"); 143 | } 144 | 145 | #[test] 146 | fn two_chunks() { 147 | let chunks = chunked_diff("1\n2\n3\n4\n5\n", "2\n3\n4\n", 1); 148 | assert_eq!(chunks.len(), 2); 149 | colored::control::set_override(false); 150 | assert_eq!(format!("{}", chunks[0]), "@@ -1,2 +1,1 @@\n-1\n 2\n"); 151 | assert_eq!(format!("{}", chunks[1]), "@@ -4,3 +3,2 @@\n 4\n-5\n \n"); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/code_substitution.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 | use itertools::Itertools; 16 | use rustc_ast; 17 | use rustc_span::source_map::{FileLoader, SourceMap}; 18 | use rustc_span::{self, BytePos, Span}; 19 | use std::collections::{hash_map, HashMap}; 20 | use std::fmt::Debug; 21 | use std::io; 22 | use std::path::PathBuf; 23 | 24 | /// A span within a file. Also differs from rustc's Span in that it's not interned, which allows us 25 | /// to make use of it after the compiler has finished running. 26 | #[derive(Debug, Ord, PartialOrd, PartialEq, Eq)] 27 | struct LocalSpan { 28 | lo: BytePos, 29 | hi: BytePos, 30 | } 31 | 32 | pub(crate) trait SpanT { 33 | fn lo(&self) -> BytePos; 34 | fn hi(&self) -> BytePos; 35 | } 36 | 37 | impl SpanT for Span { 38 | fn lo(&self) -> BytePos { 39 | Span::lo(*self) 40 | } 41 | fn hi(&self) -> BytePos { 42 | Span::hi(*self) 43 | } 44 | } 45 | 46 | impl SpanT for LocalSpan { 47 | fn lo(&self) -> BytePos { 48 | self.lo 49 | } 50 | fn hi(&self) -> BytePos { 51 | self.hi 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub(crate) struct SourceChunk<'a> { 57 | source: &'a str, 58 | start_pos: BytePos, 59 | } 60 | 61 | impl<'a> SourceChunk<'a> { 62 | pub(crate) fn new(source: &'a str, start_pos: BytePos) -> SourceChunk<'a> { 63 | SourceChunk { source, start_pos } 64 | } 65 | 66 | fn get_snippet(&self, lo: BytePos, hi: BytePos) -> &'a str { 67 | use rustc_span::Pos; 68 | &self.source[(lo - self.start_pos).to_usize()..(hi - self.start_pos).to_usize()] 69 | } 70 | 71 | fn to_end_from(&self, from: BytePos) -> &'a str { 72 | use rustc_span::Pos; 73 | &self.source[(from - self.start_pos).to_usize()..] 74 | } 75 | } 76 | 77 | #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] 78 | pub(crate) struct CodeSubstitution { 79 | // The span to be replaced. 80 | pub(crate) span: S, 81 | // New code to replace what's at span. 82 | pub(crate) new_code: String, 83 | // Whether parenthesis are needed around the substitution. 84 | pub(crate) needs_parenthesis: bool, 85 | // Whether `new_code` is a single token tree. e.g. an already parenthesised expression. If it 86 | // is, then we needn't add parenthesis even if the precedence suggests that we do. This is 87 | // necessary because our precedence rules are based on HIR which has no way to represent 88 | // parenthesis. 89 | code_is_single_tree: bool, 90 | } 91 | 92 | impl CodeSubstitution { 93 | pub(crate) fn new(span: S, new_code: String) -> CodeSubstitution { 94 | let single_tree = code_is_single_tree(&new_code); 95 | CodeSubstitution { 96 | span, 97 | new_code, 98 | needs_parenthesis: false, 99 | code_is_single_tree: single_tree, 100 | } 101 | } 102 | 103 | pub(crate) fn needs_parenthesis(mut self, needs_parenthesis: bool) -> CodeSubstitution { 104 | self.needs_parenthesis = needs_parenthesis; 105 | self 106 | } 107 | } 108 | 109 | impl CodeSubstitution { 110 | pub(crate) fn apply_with_source_map( 111 | substitutions: &[CodeSubstitution], 112 | source_map: &SourceMap, 113 | base_span: Span, 114 | ) -> String { 115 | let base_source = source_map.span_to_snippet(base_span).unwrap(); 116 | apply_substitutions( 117 | substitutions, 118 | SourceChunk::new(&base_source, base_span.lo()), 119 | ) 120 | } 121 | 122 | fn into_file_local_substitution(self, file_start: BytePos) -> CodeSubstitution { 123 | CodeSubstitution { 124 | span: LocalSpan { 125 | lo: self.span.lo() - file_start, 126 | hi: self.span.hi() - file_start, 127 | }, 128 | new_code: self.new_code, 129 | needs_parenthesis: self.needs_parenthesis, 130 | code_is_single_tree: self.code_is_single_tree, 131 | } 132 | } 133 | } 134 | 135 | // Take the code represented by base_span and apply all the substitutions, returning the 136 | // resulting code. All spans for the supplied substitutions must be within base_span and must be 137 | // non-overlapping. 138 | pub(crate) fn apply_substitutions<'a, S: SpanT + Sized + Debug>( 139 | substitutions: &[CodeSubstitution], 140 | source_chunk: SourceChunk<'a>, 141 | ) -> String { 142 | let mut output = String::new(); 143 | let mut span_lo = source_chunk.start_pos; 144 | for substitution in substitutions { 145 | if substitution.span.lo() < span_lo { 146 | panic!( 147 | "Bad substitutions: {:#?}\nFor source: {:?}", 148 | substitutions, source_chunk 149 | ); 150 | } 151 | output.push_str(source_chunk.get_snippet(span_lo, substitution.span.lo())); 152 | let should_add_parenthesis = 153 | substitution.needs_parenthesis && !substitution.code_is_single_tree; 154 | if should_add_parenthesis { 155 | output.push('('); 156 | } 157 | output.push_str(&substitution.new_code); 158 | if should_add_parenthesis { 159 | output.push(')'); 160 | } 161 | span_lo = substitution.span.hi(); 162 | // Macro invocations consume a ; that follows them. Check if the code we're replacing 163 | // ends with a ;. If it does and the new code doesn't then insert one. This may need to 164 | // be smarter, but hopefully this will do. 165 | let code_being_replaced = 166 | source_chunk.get_snippet(substitution.span.lo(), substitution.span.hi()); 167 | if code_being_replaced.ends_with(';') && !substitution.new_code.ends_with(';') { 168 | output.push(';'); 169 | } 170 | } 171 | output.push_str(source_chunk.to_end_from(span_lo)); 172 | output 173 | } 174 | 175 | /// Returns whether the supplied code is a single tokentree - e.g. a parenthesised expression. 176 | fn code_is_single_tree(code: &str) -> bool { 177 | use rustc_ast::tokenstream::TokenTree; 178 | use rustc_session::parse::ParseSess; 179 | use rustc_span::source_map::FilePathMapping; 180 | 181 | let session = ParseSess::new(FilePathMapping::empty()); 182 | let ts = rustc_parse::parse_stream_from_source_str( 183 | rustc_span::FileName::anon_source_code(code), 184 | code.to_owned(), 185 | &session, 186 | None, 187 | ); 188 | let mut count = 0; 189 | for tree in ts.into_trees() { 190 | match tree { 191 | TokenTree::Delimited(..) => { 192 | count += 1; 193 | } 194 | _ => return false, 195 | } 196 | } 197 | count <= 1 198 | } 199 | 200 | // TODO: We may want to warn if we somehow end up with overlapping matches that aren't duplicates. 201 | fn remove_duplicate_or_overlapping_matches(matches: &mut Vec>) { 202 | let mut max_hi = BytePos(0); 203 | matches.retain(|subst| { 204 | let retain = max_hi <= subst.span.lo; 205 | max_hi = ::std::cmp::max(max_hi, subst.span.hi); 206 | retain 207 | }); 208 | } 209 | 210 | #[derive(Debug, Default)] 211 | pub struct FileRelativeSubstitutions { 212 | // Substitutions keyed by filename. Each vector of substitutions must be sorted. 213 | substitutions_by_filename: HashMap>>, 214 | } 215 | 216 | impl FileRelativeSubstitutions { 217 | pub(crate) fn new( 218 | substitutions: Vec>, 219 | source_map: &SourceMap, 220 | ) -> FileRelativeSubstitutions { 221 | let mut by_file: HashMap>> = HashMap::new(); 222 | let substitutions_grouped_by_file = substitutions 223 | .into_iter() 224 | .group_by(|subst| source_map.span_to_filename(subst.span)); 225 | for (filename, file_substitutions) in &substitutions_grouped_by_file { 226 | if let rustc_span::FileName::Real(ref path) = filename { 227 | let file_relative_for_file = by_file 228 | .entry(path.local_path().to_path_buf()) 229 | .or_insert_with(Default::default); 230 | let source_file = source_map.get_source_file(&filename).unwrap(); 231 | for subst in file_substitutions { 232 | file_relative_for_file 233 | .push(subst.into_file_local_substitution(source_file.start_pos)); 234 | } 235 | file_relative_for_file.sort(); 236 | remove_duplicate_or_overlapping_matches(file_relative_for_file); 237 | } 238 | } 239 | FileRelativeSubstitutions { 240 | substitutions_by_filename: by_file, 241 | } 242 | } 243 | 244 | pub fn merge(&mut self, other: FileRelativeSubstitutions) { 245 | for (filename, substitutions) in other.substitutions_by_filename { 246 | match self.substitutions_by_filename.entry(filename) { 247 | hash_map::Entry::Vacant(entry) => { 248 | entry.insert(substitutions); 249 | } 250 | hash_map::Entry::Occupied(mut entry) => { 251 | let merged = entry.get_mut(); 252 | merged.extend(substitutions); 253 | merged.sort(); 254 | remove_duplicate_or_overlapping_matches(merged); 255 | } 256 | } 257 | } 258 | } 259 | 260 | pub fn updated_files( 261 | &self, 262 | file_loader: &dyn FileLoader, 263 | ) -> io::Result> { 264 | let mut updated_files = HashMap::new(); 265 | for (filename, substitutions) in &self.substitutions_by_filename { 266 | let source = file_loader.read_file(&filename)?; 267 | let output = apply_substitutions( 268 | substitutions, 269 | SourceChunk::new(&source, rustc_span::BytePos(0)), 270 | ); 271 | updated_files.insert(filename.clone(), output); 272 | } 273 | Ok(updated_files) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/definitions.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 | use rustc_hir; 16 | use rustc_hir::intravisit; 17 | use rustc_middle::ty::{self, TyCtxt}; 18 | use rustc_span::symbol::Symbol; 19 | 20 | #[derive(Copy, Clone)] 21 | pub(crate) struct RerastDefinitions<'tcx> { 22 | pub(crate) statements: ty::Ty<'tcx>, 23 | pub(crate) expr_rule_marker: ty::Ty<'tcx>, 24 | pub(crate) pattern_rule_marker: ty::Ty<'tcx>, 25 | pub(crate) type_rule_marker: ty::Ty<'tcx>, 26 | pub(crate) trait_ref_rule_marker: ty::Ty<'tcx>, 27 | pub(crate) search_symbol: Symbol, 28 | pub(crate) replace_symbol: Symbol, 29 | } 30 | 31 | // Visits the code in the rerast module, finding definitions we care about for later use. 32 | pub(crate) struct RerastDefinitionsFinder<'tcx> { 33 | tcx: TyCtxt<'tcx>, 34 | rerast_mod_symbol: Symbol, 35 | rerast_types_symbol: Symbol, 36 | inside_rerast_mod: bool, 37 | definitions: Option>, 38 | } 39 | 40 | impl<'tcx> RerastDefinitionsFinder<'tcx> { 41 | /// Finds rerast's internal definitions. Returns none if they cannot be found. This happens for 42 | /// example if the root source file has a #![cfg(feature = "something")] where the "something" 43 | /// feature is not enabled. 44 | pub(crate) fn find_definitions( 45 | tcx: TyCtxt<'tcx>, 46 | krate: &'tcx rustc_hir::Crate, 47 | ) -> Option> { 48 | let mut finder = RerastDefinitionsFinder { 49 | tcx, 50 | rerast_mod_symbol: Symbol::intern(super::RERAST_INTERNAL_MOD_NAME), 51 | rerast_types_symbol: Symbol::intern("rerast_types"), 52 | inside_rerast_mod: false, 53 | definitions: None, 54 | }; 55 | intravisit::walk_crate(&mut finder, krate); 56 | finder.definitions 57 | } 58 | } 59 | 60 | // This would be a little easier if there were a way to find functions by name. There's probably 61 | // something I've missed, but so far I haven't found one. 62 | impl<'tcx> intravisit::Visitor<'tcx> for RerastDefinitionsFinder<'tcx> { 63 | type Map = rustc_middle::hir::map::Map<'tcx>; 64 | 65 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { 66 | intravisit::NestedVisitorMap::All(self.tcx.hir()) 67 | } 68 | 69 | fn visit_item(&mut self, item: &'tcx rustc_hir::Item) { 70 | if self.inside_rerast_mod { 71 | intravisit::walk_item(self, item); 72 | } else if let rustc_hir::ItemKind::Mod(_) = item.kind { 73 | if item.ident.name == self.rerast_mod_symbol { 74 | self.inside_rerast_mod = true; 75 | intravisit::walk_item(self, item); 76 | self.inside_rerast_mod = false; 77 | } 78 | } 79 | } 80 | 81 | fn visit_body(&mut self, body: &'tcx rustc_hir::Body) { 82 | let fn_id = self.tcx.hir().body_owner_def_id(body.id()); 83 | if self.tcx.item_name(fn_id.to_def_id()) == self.rerast_types_symbol { 84 | let tables = self.tcx.typeck_tables_of(fn_id); 85 | let mut types = body.params.iter().map(|arg| tables.node_type(arg.hir_id)); 86 | self.definitions = Some(RerastDefinitions { 87 | statements: types 88 | .next() 89 | .expect("Internal error - missing type: statements"), 90 | expr_rule_marker: types 91 | .next() 92 | .expect("Internal error - missing type: expr_rule_marker"), 93 | pattern_rule_marker: types 94 | .next() 95 | .expect("Internal error - missing type: pattern_rule_marker"), 96 | type_rule_marker: types 97 | .next() 98 | .expect("Internal error - missing type: type_rule_marker"), 99 | trait_ref_rule_marker: types 100 | .next() 101 | .expect("Internal error - missing type: trait_ref_rule_marker"), 102 | search_symbol: Symbol::intern("Search"), 103 | replace_symbol: Symbol::intern("Replace"), 104 | }) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/errors.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 | use rustc_middle::ty::TyCtxt; 16 | use rustc_span::{FileLinesResult, Span, SpanLinesError}; 17 | use std; 18 | use std::fmt; 19 | use std::io; 20 | use std::io::Write; 21 | use std::sync::{Arc, Mutex}; 22 | 23 | #[derive(Debug, PartialEq, Eq)] 24 | pub(crate) struct ErrorWithSpan { 25 | message: String, 26 | span: Span, 27 | } 28 | 29 | impl ErrorWithSpan { 30 | pub(crate) fn new>(message: T, span: Span) -> ErrorWithSpan { 31 | ErrorWithSpan { 32 | message: message.into(), 33 | span, 34 | } 35 | } 36 | 37 | pub(crate) fn with_snippet<'a, 'tcx>(self, tcx: TyCtxt<'tcx>) -> RerastError { 38 | RerastError { 39 | message: self.message, 40 | file_lines: Some(FileLines::from_lines_result( 41 | tcx.sess.source_map().span_to_lines(self.span), 42 | )), 43 | } 44 | } 45 | } 46 | 47 | impl From for Vec { 48 | fn from(error: ErrorWithSpan) -> Vec { 49 | vec![error] 50 | } 51 | } 52 | 53 | pub struct RerastError { 54 | pub(crate) message: String, 55 | pub(crate) file_lines: Option>, 56 | } 57 | 58 | pub struct FileLines { 59 | pub(crate) file_name: rustc_span::FileName, 60 | pub(crate) lines: Vec, 61 | } 62 | 63 | pub struct Line { 64 | pub(crate) code: Option, 65 | pub(crate) line_index: usize, 66 | pub(crate) start_col: usize, 67 | pub(crate) end_col: usize, 68 | } 69 | 70 | pub struct FileLinesError { 71 | message: String, 72 | } 73 | 74 | impl RerastError { 75 | fn with_message>(message: T) -> RerastError { 76 | RerastError { 77 | message: message.into(), 78 | file_lines: None, 79 | } 80 | } 81 | } 82 | 83 | impl FileLines { 84 | fn from_lines_result(file_lines_result: FileLinesResult) -> Result { 85 | match file_lines_result { 86 | Ok(file_lines) => Ok(FileLines { 87 | file_name: file_lines.file.name.clone(), 88 | lines: file_lines 89 | .lines 90 | .iter() 91 | .map(|line_info| Line { 92 | code: file_lines 93 | .file 94 | .get_line(line_info.line_index) 95 | .and_then(|code| Some(code.into_owned())), 96 | line_index: line_info.line_index, 97 | start_col: line_info.start_col.0, 98 | end_col: line_info.end_col.0, 99 | }) 100 | .collect(), 101 | }), 102 | Err(span_lines_error) => Err(FileLinesError { 103 | message: match span_lines_error { 104 | SpanLinesError::DistinctSources(_) => { 105 | format!("Unable to report location. Spans distinct sources") 106 | } 107 | }, 108 | }), 109 | } 110 | } 111 | } 112 | 113 | impl fmt::Display for FileLines { 114 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 115 | if let Some(first_line) = self.lines.get(0) { 116 | writeln!( 117 | f, 118 | " --> {}:{}:{}", 119 | self.file_name, first_line.line_index, first_line.start_col 120 | )?; 121 | } 122 | for line_info in &self.lines { 123 | if let Some(line) = &line_info.code { 124 | writeln!(f, "{}", line)?; 125 | writeln!( 126 | f, 127 | "{}{}", 128 | " ".repeat(line_info.start_col), 129 | "^".repeat(line_info.end_col - line_info.start_col) 130 | )?; 131 | } else { 132 | writeln!( 133 | f, 134 | "Error occurred on non-existent line {}", 135 | line_info.line_index 136 | )?; 137 | } 138 | } 139 | Ok(()) 140 | } 141 | } 142 | 143 | impl fmt::Display for RerastError { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | writeln!(f, "error: {}", self.message)?; 146 | match &self.file_lines { 147 | Some(Ok(file_lines)) => writeln!(f, "{}", file_lines)?, 148 | Some(Err(error)) => writeln!(f, "{}", error.message)?, 149 | None => {} 150 | } 151 | Ok(()) 152 | } 153 | } 154 | 155 | impl fmt::Display for FileLinesError { 156 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 157 | writeln!(f, "{}", self.message) 158 | } 159 | } 160 | 161 | pub struct RerastErrors(Vec); 162 | 163 | impl RerastErrors { 164 | pub(crate) fn new(errors: Vec) -> RerastErrors { 165 | RerastErrors(errors) 166 | } 167 | pub fn with_message>(message: T) -> RerastErrors { 168 | RerastErrors(vec![RerastError::with_message(message)]) 169 | } 170 | 171 | pub fn iter(&self) -> impl Iterator { 172 | self.0.iter() 173 | } 174 | } 175 | 176 | impl std::ops::Index for RerastErrors { 177 | type Output = RerastError; 178 | 179 | fn index(&self, index: usize) -> &RerastError { 180 | &self.0[index] 181 | } 182 | } 183 | 184 | impl fmt::Debug for RerastErrors { 185 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 186 | fmt::Display::fmt(self, f) 187 | } 188 | } 189 | 190 | impl fmt::Display for RerastErrors { 191 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 192 | for error in &self.0 { 193 | writeln!(f, "{}", error)?; 194 | } 195 | Ok(()) 196 | } 197 | } 198 | 199 | impl From for RerastErrors { 200 | fn from(err: io::Error) -> RerastErrors { 201 | RerastErrors::with_message(err.to_string()) 202 | } 203 | } 204 | 205 | #[derive(Clone)] 206 | pub(crate) struct DiagnosticOutput { 207 | storage: Arc>>, 208 | } 209 | 210 | impl Write for DiagnosticOutput { 211 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 212 | let mut storage = self.storage.lock().unwrap(); 213 | storage.write(buf) 214 | } 215 | 216 | fn flush(&mut self) -> std::io::Result<()> { 217 | Ok(()) 218 | } 219 | } 220 | 221 | impl DiagnosticOutput { 222 | pub(crate) fn new() -> DiagnosticOutput { 223 | DiagnosticOutput { 224 | storage: Arc::new(Mutex::new(Vec::new())), 225 | } 226 | } 227 | 228 | pub(crate) fn errors(&self) -> RerastErrors { 229 | let storage = self.storage.lock().unwrap(); 230 | if let Ok(output) = std::str::from_utf8(&storage) { 231 | RerastErrors( 232 | output 233 | .lines() 234 | .filter_map(|line| { 235 | if let Ok(json) = json::parse(line) { 236 | if let Some(rendered) = json["rendered"].as_str() { 237 | return Some(RerastError::with_message(rendered)); 238 | } 239 | } 240 | None 241 | }) 242 | .collect(), 243 | ) 244 | } else { 245 | RerastErrors::with_message("Compiler emitted invalid UTF8") 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/file_loader.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 | use rustc_span::source_map::{FileLoader, RealFileLoader}; 16 | use std::collections::HashMap; 17 | use std::io; 18 | use std::path::{Path, PathBuf}; 19 | 20 | #[derive(Clone)] 21 | pub(crate) struct InMemoryFileLoader { 22 | files: HashMap, 23 | inner_file_loader: T, 24 | } 25 | 26 | impl InMemoryFileLoader { 27 | pub(crate) fn new(inner: T) -> InMemoryFileLoader { 28 | InMemoryFileLoader { 29 | files: HashMap::new(), 30 | inner_file_loader: inner, 31 | } 32 | } 33 | 34 | pub(crate) fn add_file>(&mut self, file_name: P, contents: String) { 35 | self.files.insert(file_name.into(), contents); 36 | } 37 | } 38 | 39 | impl FileLoader for InMemoryFileLoader { 40 | fn file_exists(&self, path: &Path) -> bool { 41 | self.files.contains_key(path) || self.inner_file_loader.file_exists(path) 42 | } 43 | 44 | fn read_file(&self, path: &Path) -> io::Result { 45 | if let Some(contents) = self.files.get(path) { 46 | return Ok(contents.to_string()); 47 | } 48 | self.inner_file_loader.read_file(path) 49 | } 50 | } 51 | 52 | /// We only need this because `RealFileLoader` doesn't derive `Clone` 53 | #[derive(Clone)] 54 | pub(crate) struct ClonableRealFileLoader; 55 | 56 | impl FileLoader for ClonableRealFileLoader { 57 | fn file_exists(&self, path: &Path) -> bool { 58 | RealFileLoader.file_exists(path) 59 | } 60 | 61 | fn read_file(&self, path: &Path) -> io::Result { 62 | RealFileLoader.read_file(path) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/rule_finder.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 | use super::DeclaredNamesFinder; 16 | use crate::definitions::RerastDefinitions; 17 | use crate::errors::ErrorWithSpan; 18 | use crate::rule_matcher::{Matchable, OperatorPrecedence}; 19 | use crate::rules::{Rule, Rules}; 20 | use rustc_hir::intravisit; 21 | use rustc_hir::{self, HirId}; 22 | use rustc_middle::ty::{self, TyCtxt}; 23 | use rustc_span::symbol::Symbol; 24 | use rustc_span::Span; 25 | use std::marker; 26 | use std::vec::Vec; 27 | 28 | // Finds rules. 29 | pub(crate) struct RuleFinder<'tcx> { 30 | tcx: TyCtxt<'tcx>, 31 | rerast_definitions: RerastDefinitions<'tcx>, 32 | rules_mod_symbol: Symbol, 33 | rules: Rules<'tcx>, 34 | body_ids: Vec, 35 | in_rules_module: bool, 36 | errors: Vec, 37 | } 38 | 39 | impl<'tcx> RuleFinder<'tcx> { 40 | pub(crate) fn find_rules( 41 | tcx: TyCtxt<'tcx>, 42 | rerast_definitions: RerastDefinitions<'tcx>, 43 | krate: &'tcx rustc_hir::Crate, 44 | ) -> Result, Vec> { 45 | let mut rule_finder = RuleFinder { 46 | tcx, 47 | rerast_definitions, 48 | rules_mod_symbol: Symbol::intern(super::RULES_MOD_NAME), 49 | rules: Rules::new(), 50 | body_ids: Vec::new(), 51 | in_rules_module: false, 52 | errors: Vec::new(), 53 | }; 54 | intravisit::walk_crate(&mut rule_finder, krate); 55 | if rule_finder.errors.is_empty() { 56 | Ok(rule_finder.rules) 57 | } else { 58 | Err(rule_finder.errors) 59 | } 60 | } 61 | 62 | // Possibly add a rule. 63 | fn maybe_add_rule( 64 | &mut self, 65 | arg_ty: ty::Ty<'tcx>, 66 | arms: &'tcx [rustc_hir::Arm], 67 | arg_ty_span: Span, 68 | ) -> Result<(), Vec> { 69 | if self.maybe_add_typed_rule::(arg_ty, arms)? 70 | || self.maybe_add_typed_rule::(arg_ty, arms)? 71 | || self.maybe_add_typed_rule::(arg_ty, arms)? 72 | || self.maybe_add_typed_rule::(arg_ty, arms)? 73 | { 74 | Ok(()) 75 | } else { 76 | // TODO: Report proper span. Perhaps report other code - this will only report an 77 | // unexpected match. 78 | Err(vec![ErrorWithSpan::new( 79 | "Unexpected code found in rule function", 80 | arg_ty_span, 81 | )]) 82 | } 83 | } 84 | 85 | fn maybe_add_typed_rule>( 86 | &mut self, 87 | arg_ty: ty::Ty<'tcx>, 88 | arms: &'tcx [rustc_hir::Arm], 89 | ) -> Result> { 90 | // Given some arms of a match statement, returns the block for arm_name if any. 91 | fn get_arm<'a>( 92 | arms: &'a [rustc_hir::Arm], 93 | arm_name: Symbol, 94 | ) -> Option<&'a rustc_hir::Block<'a>> { 95 | for arm in arms { 96 | if let rustc_hir::PatKind::Path(rustc_hir::QPath::Resolved(None, ref path)) = 97 | arm.pat.kind 98 | { 99 | if let Some(segment) = path.segments.last() { 100 | if segment.ident.name == arm_name { 101 | if let rustc_hir::ExprKind::Block(ref block, _) = arm.body.kind { 102 | return Some(block); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | None 109 | } 110 | 111 | if arg_ty != T::replace_marker_type(&self.rerast_definitions) { 112 | // This is a rule of a different type 113 | return Ok(false); 114 | } 115 | let mut placeholder_ids = Vec::new(); 116 | for body_id in &self.body_ids { 117 | let body = self.tcx.hir().body(*body_id); 118 | for param in body.params { 119 | placeholder_ids.push(param.pat.hir_id); 120 | } 121 | // Allow any variable declarations at the start or the rule block to 122 | // serve as placeholders in addition to the funciton arguments. This 123 | // is necssary since async functions transform the supplied code 124 | // into this form. e.g. if the async function has an argument r, 125 | // then the function will contain a block with the first statement 126 | // being let r = r; 127 | if let rustc_hir::ExprKind::Block(block, ..) = &body.value.kind { 128 | for stmt in block.stmts.iter() { 129 | if let rustc_hir::StmtKind::Local(local) = &stmt.kind { 130 | if let rustc_hir::PatKind::Binding(_, hir_id, ..) = &local.pat.kind { 131 | placeholder_ids.push(*hir_id); 132 | } 133 | } else { 134 | break; 135 | } 136 | } 137 | } 138 | } 139 | let body_id = match self.body_ids.last() { 140 | Some(x) => *x, 141 | None => return Ok(false), 142 | }; 143 | if let (Some(search_block), Some(replace_block)) = ( 144 | get_arm(arms, self.rerast_definitions.search_symbol), 145 | get_arm(arms, self.rerast_definitions.replace_symbol), 146 | ) { 147 | let search = T::extract_root(search_block)?; 148 | let replace = T::extract_root(replace_block)?; 149 | let rule = Rule { 150 | search, 151 | replace, 152 | body_id, 153 | placeholder_ids, 154 | declared_name_hir_ids: DeclaredNamesFinder::find(self.tcx, search), 155 | }; 156 | rule.validate(self.tcx)?; 157 | T::add_rule(rule, &mut self.rules); 158 | } else { 159 | // Pretty sure this shouldn't be possible unless rerast internals are directly used. 160 | panic!("Missing search/replace pattern"); 161 | } 162 | Ok(true) 163 | } 164 | } 165 | 166 | impl<'tcx> intravisit::Visitor<'tcx> for RuleFinder<'tcx> { 167 | type Map = rustc_middle::hir::map::Map<'tcx>; 168 | 169 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { 170 | intravisit::NestedVisitorMap::All(self.tcx.hir()) 171 | } 172 | 173 | fn visit_item(&mut self, item: &'tcx rustc_hir::Item) { 174 | if let rustc_hir::ItemKind::Mod(_) = item.kind { 175 | if item.ident.name == self.rules_mod_symbol { 176 | self.in_rules_module = true; 177 | intravisit::walk_item(self, item); 178 | self.in_rules_module = false; 179 | return; 180 | } else if !self.in_rules_module { 181 | // Not the module we're interested in 182 | return; 183 | } 184 | } 185 | intravisit::walk_item(self, item); 186 | } 187 | 188 | fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr) { 189 | if !self.in_rules_module { 190 | return; 191 | } 192 | use crate::rustc_hir::ExprKind; 193 | if let ExprKind::Match(ref match_expr, ref arms, _) = expr.kind { 194 | if let ExprKind::MethodCall(ref _name, ref _tys, ref args) = match_expr.kind { 195 | if let Some(&body_id) = self.body_ids.last() { 196 | let type_tables = self 197 | .tcx 198 | .typeck_tables_of(self.tcx.hir().body_owner_def_id(body_id)); 199 | let arg0 = &args[0]; 200 | let arg_ty = type_tables.node_type(arg0.hir_id); 201 | if let Err(errors) = self.maybe_add_rule(arg_ty, arms, arg0.span) { 202 | self.errors.extend(errors); 203 | } 204 | // Don't walk deeper into this expression. 205 | return; 206 | } 207 | } 208 | } 209 | intravisit::walk_expr(self, expr) 210 | } 211 | 212 | fn visit_body(&mut self, body: &'tcx rustc_hir::Body) { 213 | if !self.in_rules_module { 214 | return; 215 | } 216 | self.body_ids.push(body.id()); 217 | intravisit::walk_body(self, body); 218 | self.body_ids.pop(); 219 | } 220 | } 221 | 222 | // Trait implemented by types that we can match (as opposed to be part of a match). 223 | pub(crate) trait StartMatch<'tcx>: Matchable { 224 | fn span(&self) -> Span; 225 | fn walk>(visitor: &mut V, node: &'tcx Self); 226 | fn needs_parenthesis(_parent: Option<&Self>, _child: &Self) -> bool { 227 | false 228 | } 229 | // Extract the root search/replace node from the supplied block. 230 | fn extract_root(block: &'tcx rustc_hir::Block<'tcx>) -> Result<&'tcx Self, ErrorWithSpan>; 231 | // Adds the supplied rule to the appropriate typed collection in rules. 232 | fn add_rule(rule: Rule<'tcx, Self>, rules: &mut Rules<'tcx>) 233 | where 234 | Self: marker::Sized; 235 | // Get the type marker used to identify this type of search/replace. 236 | fn replace_marker_type(rerast_definitions: &RerastDefinitions<'tcx>) -> ty::Ty<'tcx>; 237 | // See comment on field of the same name on MatchState. 238 | fn bindings_can_match_patterns() -> bool { 239 | false 240 | } 241 | fn hir_id(&self) -> HirId; 242 | } 243 | 244 | impl<'tcx> StartMatch<'tcx> for rustc_hir::Expr<'tcx> { 245 | fn span(&self) -> Span { 246 | self.span 247 | } 248 | fn walk>(visitor: &mut V, node: &'tcx Self) { 249 | visitor.visit_expr(node); 250 | } 251 | fn needs_parenthesis(parent: Option<&Self>, child: &Self) -> bool { 252 | OperatorPrecedence::needs_parenthesis(parent, child) 253 | } 254 | fn extract_root(block: &'tcx rustc_hir::Block<'tcx>) -> Result<&'tcx Self, ErrorWithSpan> { 255 | if block.stmts.len() == 1 && block.expr.is_none() { 256 | if let rustc_hir::StmtKind::Semi(ref addr_expr) = block.stmts[0].kind { 257 | if let rustc_hir::ExprKind::AddrOf(_, _, ref expr) = addr_expr.kind { 258 | return Ok(&**expr); 259 | } 260 | } 261 | } 262 | Err(ErrorWithSpan::new( 263 | "replace! macro didn't produce expected structure", 264 | block.span, 265 | )) 266 | } 267 | fn add_rule(rule: Rule<'tcx, Self>, rules: &mut Rules<'tcx>) { 268 | rules.expr_rules.push(rule); 269 | } 270 | fn replace_marker_type(rerast_definitions: &RerastDefinitions<'tcx>) -> ty::Ty<'tcx> { 271 | rerast_definitions.expr_rule_marker 272 | } 273 | fn hir_id(&self) -> HirId { 274 | self.hir_id 275 | } 276 | } 277 | 278 | impl<'tcx> StartMatch<'tcx> for rustc_hir::Ty<'tcx> { 279 | fn span(&self) -> Span { 280 | self.span 281 | } 282 | fn walk>(visitor: &mut V, node: &'tcx Self) { 283 | visitor.visit_ty(node); 284 | } 285 | fn extract_root(block: &'tcx rustc_hir::Block<'tcx>) -> Result<&'tcx Self, ErrorWithSpan> { 286 | if block.stmts.len() == 1 && block.expr.is_none() { 287 | if let rustc_hir::StmtKind::Local(ref local) = block.stmts[0].kind { 288 | if let Some(ref ref_ty) = local.ty { 289 | if let rustc_hir::TyKind::Rptr(_, ref mut_ty) = ref_ty.kind { 290 | return Ok(&*mut_ty.ty); 291 | } 292 | } 293 | } 294 | } 295 | Err(ErrorWithSpan::new( 296 | "replace_type! macro didn't produce expected structure", 297 | block.span, 298 | )) 299 | } 300 | fn add_rule(rule: Rule<'tcx, Self>, rules: &mut Rules<'tcx>) { 301 | rules.type_rules.push(rule); 302 | } 303 | fn replace_marker_type(rerast_definitions: &RerastDefinitions<'tcx>) -> ty::Ty<'tcx> { 304 | rerast_definitions.type_rule_marker 305 | } 306 | fn hir_id(&self) -> HirId { 307 | self.hir_id 308 | } 309 | } 310 | 311 | impl<'tcx> StartMatch<'tcx> for rustc_hir::TraitRef<'tcx> { 312 | fn span(&self) -> Span { 313 | self.path.span 314 | } 315 | fn walk>(visitor: &mut V, node: &'tcx Self) { 316 | visitor.visit_trait_ref(node); 317 | } 318 | fn extract_root(block: &'tcx rustc_hir::Block<'tcx>) -> Result<&'tcx Self, ErrorWithSpan> { 319 | let ty = ::extract_root(block)?; 320 | if let rustc_hir::TyKind::TraitObject(ref bounds, _) = ty.kind { 321 | if bounds.len() == 1 { 322 | return Ok(&bounds[0].trait_ref); 323 | } else { 324 | return Err(ErrorWithSpan::new( 325 | "replace_trait_ref! requires exactly one trait", 326 | ty.span, 327 | )); 328 | } 329 | } else { 330 | return Err(ErrorWithSpan::new( 331 | "replace_trait_ref! requires a trait", 332 | ty.span, 333 | )); 334 | } 335 | } 336 | fn add_rule(rule: Rule<'tcx, Self>, rules: &mut Rules<'tcx>) { 337 | rules.trait_ref_rules.push(rule); 338 | } 339 | fn replace_marker_type(rerast_definitions: &RerastDefinitions<'tcx>) -> ty::Ty<'tcx> { 340 | rerast_definitions.trait_ref_rule_marker 341 | } 342 | fn hir_id(&self) -> HirId { 343 | self.hir_ref_id 344 | } 345 | } 346 | 347 | impl<'tcx> StartMatch<'tcx> for rustc_hir::Pat<'tcx> { 348 | fn span(&self) -> Span { 349 | self.span 350 | } 351 | fn walk>(visitor: &mut V, node: &'tcx Self) { 352 | visitor.visit_pat(node); 353 | } 354 | fn extract_root( 355 | block: &'tcx rustc_hir::Block<'tcx>, 356 | ) -> Result<&'tcx rustc_hir::Pat<'tcx>, ErrorWithSpan> { 357 | if block.stmts.len() == 1 && block.expr.is_none() { 358 | if let rustc_hir::StmtKind::Semi(ref expr) = block.stmts[0].kind { 359 | if let rustc_hir::ExprKind::Match(_, ref arms, _) = expr.kind { 360 | // The user's pattern is wrapped in Some(x) in order to make all patterns 361 | // refutable. Otherwise we'd need the user to use a different macro for 362 | // refutable and irrefutable patterns which wouldn't be very ergonomic. 363 | if let rustc_hir::PatKind::TupleStruct(_, ref patterns, _) = arms[0].pat.kind { 364 | return Ok(&patterns[0]); 365 | } 366 | } 367 | } 368 | } 369 | Err(ErrorWithSpan::new( 370 | "replace_pattern! macro didn't produce expected structure", 371 | block.span, 372 | )) 373 | } 374 | fn add_rule(rule: Rule<'tcx, Self>, rules: &mut Rules<'tcx>) { 375 | rules.pattern_rules.push(rule); 376 | } 377 | fn replace_marker_type<'a>(rerast_definitions: &RerastDefinitions<'a>) -> ty::Ty<'a> { 378 | rerast_definitions.pattern_rule_marker 379 | } 380 | fn bindings_can_match_patterns() -> bool { 381 | true 382 | } 383 | fn hir_id(&self) -> HirId { 384 | self.hir_id 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/rules.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 | use crate::rule_finder::StartMatch; 16 | use rustc_hir::{self, HirId}; 17 | use rustc_span::symbol::Symbol; 18 | use std::collections::HashMap; 19 | use std::vec::Vec; 20 | 21 | #[derive(Debug)] 22 | pub(crate) struct Rule<'tcx, T: StartMatch<'tcx>> { 23 | pub(crate) search: &'tcx T, 24 | pub(crate) replace: &'tcx T, 25 | // The method in which the rule is defined. 26 | pub(crate) body_id: rustc_hir::BodyId, 27 | pub(crate) placeholder_ids: Vec, 28 | // Maps from the names of declared variables (which must be unique within the search pattern) to 29 | // their HirId. This is used to pair up variables in the search pattern with their counterparts 30 | // in the replacement pattern. This is necessary since as far as rustc is concerned, they're 31 | // completely unrelated definitions. It isn't needed for expression placeholders since they're 32 | // declared as arguments to the function, so the search and replace pattern can both reference 33 | // the same placeholder variable. 34 | pub(crate) declared_name_hir_ids: HashMap, 35 | } 36 | 37 | #[derive(Debug)] 38 | pub(crate) struct Rules<'tcx> { 39 | pub(crate) expr_rules: Vec>>, 40 | pub(crate) pattern_rules: Vec>>, 41 | pub(crate) type_rules: Vec>>, 42 | pub(crate) trait_ref_rules: Vec>>, 43 | } 44 | 45 | impl<'tcx> Rules<'tcx> { 46 | pub(crate) fn new() -> Rules<'tcx> { 47 | Rules { 48 | expr_rules: Vec::new(), 49 | pattern_rules: Vec::new(), 50 | type_rules: Vec::new(), 51 | trait_ref_rules: Vec::new(), 52 | } 53 | } 54 | 55 | pub(crate) fn len(&self) -> usize { 56 | self.expr_rules.len() 57 | + self.pattern_rules.len() 58 | + self.type_rules.len() 59 | + self.trait_ref_rules.len() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/validation.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 | use super::hir_id_from_path; 16 | use crate::errors::ErrorWithSpan; 17 | use crate::rule_finder::StartMatch; 18 | use crate::rules::Rule; 19 | use rustc_hir::intravisit; 20 | use rustc_hir::{self, HirId}; 21 | use rustc_middle::ty::TyCtxt; 22 | use rustc_span::Span; 23 | use std::collections::HashSet; 24 | 25 | struct ValidatorState<'tcx> { 26 | tcx: TyCtxt<'tcx>, 27 | errors: Vec, 28 | // Definitions that are defined as placeholders. 29 | placeholders: HashSet, 30 | // Placeholders that have been bound. 31 | bound_placeholders: HashSet, 32 | } 33 | 34 | impl<'tcx> ValidatorState<'tcx> { 35 | fn add_error>(&mut self, message: T, span: Span) { 36 | self.errors.push(ErrorWithSpan::new(message, span)); 37 | } 38 | } 39 | 40 | impl<'tcx, T: StartMatch<'tcx> + 'tcx> Rule<'tcx, T> { 41 | pub(crate) fn validate<'a>(&self, tcx: TyCtxt<'tcx>) -> Result<(), Vec> { 42 | let rule_body = tcx.hir().body(self.body_id); 43 | 44 | let mut search_validator = SearchValidator { 45 | state: ValidatorState { 46 | tcx, 47 | errors: Vec::new(), 48 | placeholders: rule_body.params.iter().map(|arg| arg.pat.hir_id).collect(), 49 | bound_placeholders: HashSet::new(), 50 | }, 51 | }; 52 | StartMatch::walk(&mut search_validator, self.search); 53 | let mut replacement_validator = ReplacementValidator { 54 | state: search_validator.state, 55 | }; 56 | StartMatch::walk(&mut replacement_validator, self.replace); 57 | if !replacement_validator.state.errors.is_empty() { 58 | return Err(replacement_validator.state.errors); 59 | } 60 | Ok(()) 61 | } 62 | } 63 | 64 | struct SearchValidator<'tcx> { 65 | state: ValidatorState<'tcx>, 66 | } 67 | 68 | impl<'tcx> intravisit::Visitor<'tcx> for SearchValidator<'tcx> { 69 | type Map = rustc_middle::hir::map::Map<'tcx>; 70 | 71 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { 72 | intravisit::NestedVisitorMap::All(self.state.tcx.hir()) 73 | } 74 | 75 | fn visit_qpath(&mut self, qpath: &'tcx rustc_hir::QPath, id: rustc_hir::HirId, span: Span) { 76 | if let Some(hir_id) = hir_id_from_path(qpath) { 77 | if self.state.placeholders.contains(&hir_id) 78 | && !self.state.bound_placeholders.insert(hir_id) 79 | { 80 | self.state.add_error( 81 | "Placeholder is bound multiple times. This is not currently permitted.", 82 | span, 83 | ); 84 | } 85 | } 86 | intravisit::walk_qpath(self, qpath, id, span); 87 | } 88 | } 89 | 90 | struct ReplacementValidator<'tcx> { 91 | state: ValidatorState<'tcx>, 92 | } 93 | 94 | impl<'tcx> intravisit::Visitor<'tcx> for ReplacementValidator<'tcx> { 95 | type Map = rustc_middle::hir::map::Map<'tcx>; 96 | 97 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { 98 | intravisit::NestedVisitorMap::All(self.state.tcx.hir()) 99 | } 100 | 101 | fn visit_qpath(&mut self, qpath: &'tcx rustc_hir::QPath, id: rustc_hir::HirId, span: Span) { 102 | if let Some(hir_id) = hir_id_from_path(qpath) { 103 | if self.state.placeholders.contains(&hir_id) 104 | && !self.state.bound_placeholders.contains(&hir_id) 105 | { 106 | self.state.add_error( 107 | "Placeholder used in replacement pattern, but never bound.", 108 | span, 109 | ); 110 | } 111 | } 112 | intravisit::walk_qpath(self, qpath, id, span); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/cargo_rerast_tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | 5 | fn cargo_rerast(crate_root: &str) -> Command { 6 | // We can't use Assert.current_dir, because then Assert::cargo_binary doesn't work, instead we 7 | // pass the crate root as an argument and get our binary to change directories once it's 8 | // running. 9 | let mut cmd = Command::cargo_bin("cargo-rerast").unwrap(); 10 | cmd.arg("rerast").arg("--crate_root").arg(crate_root); 11 | cmd 12 | } 13 | 14 | #[test] 15 | fn test_help() { 16 | cargo_rerast(".") 17 | .arg("--help") 18 | .assert() 19 | .success() 20 | .stdout(predicate::str::contains("cargo rerast")); 21 | } 22 | 23 | #[test] 24 | fn test_simple_diff() { 25 | cargo_rerast("tests/crates/simple") 26 | // TODO: Remove once #10 is fixed. 27 | .env("RERAST_FULL_CARGO_CLEAN", "1") 28 | .arg("-p") 29 | .arg("p0: i32, p1: i32") 30 | .arg("-s") 31 | .arg("p0 > p1") 32 | .arg("-r") 33 | .arg("p1 < p0") 34 | .arg("--diff") 35 | .arg("--color=never") 36 | .assert() 37 | .stdout(predicate::eq( 38 | r#"--- src/lib.rs 39 | +++ src/lib.rs 40 | @@ -8,7 +8,7 @@ 41 | mod tests2 { 42 | #[test] 43 | fn x() { 44 | - if 1 > 42 { 45 | + if 42 < 1 { 46 | assert!(false); 47 | } 48 | } 49 | 50 | @@ -16,7 +16,7 @@ 51 | 52 | /// A well documented function. 53 | pub fn foo(a: i32, b: i32) -> i32 { 54 | - if a > b { 55 | + if b < a { 56 | 42 57 | } else { 58 | b 59 | 60 | @@ -26,7 +26,7 @@ 61 | #[cfg(test)] 62 | mod tests { 63 | fn bar(a: i32, b: i32) -> i32 { 64 | - if a > b { 65 | + if b < a { 66 | 42 67 | } else { 68 | b 69 | 70 | "#, 71 | )); 72 | } 73 | 74 | #[test] 75 | fn test_invalid_cargo_toml() { 76 | cargo_rerast("tests/crates/invalid_cargo_toml") 77 | .args(&["-s", "file!()", "-r", "\"foo\""]) 78 | .args(&["--diff", "--color=never"]) 79 | .assert() 80 | .failure() 81 | .stderr( 82 | predicate::str::contains("cargo metadata failed") 83 | .and(predicate::str::contains("could not parse input as TOML")), 84 | ); 85 | } 86 | 87 | #[test] 88 | fn test_compilation_error() { 89 | cargo_rerast("tests/crates/compilation_error") 90 | .args(&["-s", "file!()", "-r", "\"foo\""]) 91 | .args(&["--diff", "--color=never"]) 92 | .assert() 93 | .failure() 94 | .stderr(predicate::str::contains("this is not an i32")); 95 | } 96 | -------------------------------------------------------------------------------- /tests/crates/compilation_error/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compilation_error" 3 | version = "0.1.0" 4 | authors = ["David Lattimore "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /tests/crates/compilation_error/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn foo() -> i32 { 2 | "this is not an i32" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /tests/crates/invalid_cargo_toml/Cargo.toml: -------------------------------------------------------------------------------- 1 | ]]] 2 | 3 | -------------------------------------------------------------------------------- /tests/crates/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | authors = ["David Lattimore "] 5 | 6 | [dependencies] 7 | 8 | [features] 9 | some_feature = [] 10 | -------------------------------------------------------------------------------- /tests/crates/simple/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Sample input file. 2 | 3 | #![deny(missing_docs, warnings)] 4 | 5 | // We have two cfg(test) modules to make sure that matches in both get merged correctly with matches 6 | // in the main module. 7 | #[cfg(test)] 8 | mod tests2 { 9 | #[test] 10 | fn x() { 11 | if 1 > 42 { 12 | assert!(false); 13 | } 14 | } 15 | } 16 | 17 | /// A well documented function. 18 | pub fn foo(a: i32, b: i32) -> i32 { 19 | if a > b { 20 | 42 21 | } else { 22 | b 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | fn bar(a: i32, b: i32) -> i32 { 29 | if a > b { 30 | 42 31 | } else { 32 | b 33 | } 34 | } 35 | 36 | #[test] 37 | fn x() { 38 | assert_eq!(super::foo(1, 2), bar(1, 2)); 39 | } 40 | 41 | #[test] 42 | fn env() { 43 | // Make sure some environment variables are set. See issue #9. 44 | env!("CARGO_PKG_VERSION"); 45 | env!("CARGO_PKG_VERSION"); 46 | env!("CARGO_PKG_VERSION_MAJOR"); 47 | env!("CARGO_PKG_VERSION_MINOR"); 48 | env!("CARGO_PKG_VERSION_PATCH"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/crates/simple/tests/disabled_integration_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "some_feature")] 2 | --------------------------------------------------------------------------------