├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples └── useless.rs └── src ├── cauterize.rs ├── diff_format.rs ├── error.rs ├── main.rs ├── resolver.rs ├── unused.rs └── vcs ├── check_vcs.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "camino" 13 | version = "1.1.6" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" 16 | dependencies = [ 17 | "serde", 18 | ] 19 | 20 | [[package]] 21 | name = "cargo-minify" 22 | version = "0.5.0" 23 | dependencies = [ 24 | "cargo_metadata", 25 | "diff", 26 | "git2", 27 | "glob-match", 28 | "gumdrop", 29 | "multimap", 30 | "nu-ansi-term", 31 | "proc-macro2", 32 | "syn 2.0.39", 33 | "thiserror", 34 | ] 35 | 36 | [[package]] 37 | name = "cargo-platform" 38 | version = "0.1.5" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" 41 | dependencies = [ 42 | "serde", 43 | ] 44 | 45 | [[package]] 46 | name = "cargo_metadata" 47 | version = "0.17.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "e7daec1a2a2129eeba1644b220b4647ec537b0b5d4bfd6876fcc5a540056b592" 50 | dependencies = [ 51 | "camino", 52 | "cargo-platform", 53 | "semver", 54 | "serde", 55 | "serde_json", 56 | "thiserror", 57 | ] 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.0.83" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 64 | dependencies = [ 65 | "jobserver", 66 | "libc", 67 | ] 68 | 69 | [[package]] 70 | name = "diff" 71 | version = "0.1.13" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 74 | 75 | [[package]] 76 | name = "form_urlencoded" 77 | version = "1.2.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 80 | dependencies = [ 81 | "percent-encoding", 82 | ] 83 | 84 | [[package]] 85 | name = "git2" 86 | version = "0.17.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" 89 | dependencies = [ 90 | "bitflags", 91 | "libc", 92 | "libgit2-sys", 93 | "log", 94 | "openssl-probe", 95 | "openssl-sys", 96 | "url", 97 | ] 98 | 99 | [[package]] 100 | name = "glob-match" 101 | version = "0.2.1" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" 104 | 105 | [[package]] 106 | name = "gumdrop" 107 | version = "0.8.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" 110 | dependencies = [ 111 | "gumdrop_derive", 112 | ] 113 | 114 | [[package]] 115 | name = "gumdrop_derive" 116 | version = "0.8.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" 119 | dependencies = [ 120 | "proc-macro2", 121 | "quote", 122 | "syn 1.0.109", 123 | ] 124 | 125 | [[package]] 126 | name = "idna" 127 | version = "0.5.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 130 | dependencies = [ 131 | "unicode-bidi", 132 | "unicode-normalization", 133 | ] 134 | 135 | [[package]] 136 | name = "itoa" 137 | version = "1.0.9" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 140 | 141 | [[package]] 142 | name = "jobserver" 143 | version = "0.1.27" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" 146 | dependencies = [ 147 | "libc", 148 | ] 149 | 150 | [[package]] 151 | name = "libc" 152 | version = "0.2.150" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 155 | 156 | [[package]] 157 | name = "libgit2-sys" 158 | version = "0.15.2+1.6.4" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" 161 | dependencies = [ 162 | "cc", 163 | "libc", 164 | "libssh2-sys", 165 | "libz-sys", 166 | "openssl-sys", 167 | "pkg-config", 168 | ] 169 | 170 | [[package]] 171 | name = "libssh2-sys" 172 | version = "0.3.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 175 | dependencies = [ 176 | "cc", 177 | "libc", 178 | "libz-sys", 179 | "openssl-sys", 180 | "pkg-config", 181 | "vcpkg", 182 | ] 183 | 184 | [[package]] 185 | name = "libz-sys" 186 | version = "1.1.12" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" 189 | dependencies = [ 190 | "cc", 191 | "libc", 192 | "pkg-config", 193 | "vcpkg", 194 | ] 195 | 196 | [[package]] 197 | name = "log" 198 | version = "0.4.20" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 201 | 202 | [[package]] 203 | name = "multimap" 204 | version = "0.9.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "e1a5d38b9b352dbd913288736af36af41c48d61b1a8cd34bcecd727561b7d511" 207 | dependencies = [ 208 | "serde", 209 | ] 210 | 211 | [[package]] 212 | name = "nu-ansi-term" 213 | version = "0.49.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" 216 | dependencies = [ 217 | "windows-sys", 218 | ] 219 | 220 | [[package]] 221 | name = "openssl-probe" 222 | version = "0.1.5" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 225 | 226 | [[package]] 227 | name = "openssl-sys" 228 | version = "0.9.96" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" 231 | dependencies = [ 232 | "cc", 233 | "libc", 234 | "pkg-config", 235 | "vcpkg", 236 | ] 237 | 238 | [[package]] 239 | name = "percent-encoding" 240 | version = "2.3.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 243 | 244 | [[package]] 245 | name = "pkg-config" 246 | version = "0.3.27" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 249 | 250 | [[package]] 251 | name = "proc-macro2" 252 | version = "1.0.70" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 255 | dependencies = [ 256 | "unicode-ident", 257 | ] 258 | 259 | [[package]] 260 | name = "quote" 261 | version = "1.0.33" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 264 | dependencies = [ 265 | "proc-macro2", 266 | ] 267 | 268 | [[package]] 269 | name = "ryu" 270 | version = "1.0.15" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 273 | 274 | [[package]] 275 | name = "semver" 276 | version = "1.0.20" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 279 | dependencies = [ 280 | "serde", 281 | ] 282 | 283 | [[package]] 284 | name = "serde" 285 | version = "1.0.193" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 288 | dependencies = [ 289 | "serde_derive", 290 | ] 291 | 292 | [[package]] 293 | name = "serde_derive" 294 | version = "1.0.193" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 297 | dependencies = [ 298 | "proc-macro2", 299 | "quote", 300 | "syn 2.0.39", 301 | ] 302 | 303 | [[package]] 304 | name = "serde_json" 305 | version = "1.0.108" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 308 | dependencies = [ 309 | "itoa", 310 | "ryu", 311 | "serde", 312 | ] 313 | 314 | [[package]] 315 | name = "syn" 316 | version = "1.0.109" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 319 | dependencies = [ 320 | "proc-macro2", 321 | "quote", 322 | "unicode-ident", 323 | ] 324 | 325 | [[package]] 326 | name = "syn" 327 | version = "2.0.39" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 330 | dependencies = [ 331 | "proc-macro2", 332 | "quote", 333 | "unicode-ident", 334 | ] 335 | 336 | [[package]] 337 | name = "thiserror" 338 | version = "1.0.50" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 341 | dependencies = [ 342 | "thiserror-impl", 343 | ] 344 | 345 | [[package]] 346 | name = "thiserror-impl" 347 | version = "1.0.50" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn 2.0.39", 354 | ] 355 | 356 | [[package]] 357 | name = "tinyvec" 358 | version = "1.6.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 361 | dependencies = [ 362 | "tinyvec_macros", 363 | ] 364 | 365 | [[package]] 366 | name = "tinyvec_macros" 367 | version = "0.1.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 370 | 371 | [[package]] 372 | name = "unicode-bidi" 373 | version = "0.3.13" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 376 | 377 | [[package]] 378 | name = "unicode-ident" 379 | version = "1.0.12" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 382 | 383 | [[package]] 384 | name = "unicode-normalization" 385 | version = "0.1.22" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 388 | dependencies = [ 389 | "tinyvec", 390 | ] 391 | 392 | [[package]] 393 | name = "url" 394 | version = "2.5.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 397 | dependencies = [ 398 | "form_urlencoded", 399 | "idna", 400 | "percent-encoding", 401 | ] 402 | 403 | [[package]] 404 | name = "vcpkg" 405 | version = "0.2.15" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 408 | 409 | [[package]] 410 | name = "windows-sys" 411 | version = "0.48.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 414 | dependencies = [ 415 | "windows-targets", 416 | ] 417 | 418 | [[package]] 419 | name = "windows-targets" 420 | version = "0.48.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 423 | dependencies = [ 424 | "windows_aarch64_gnullvm", 425 | "windows_aarch64_msvc", 426 | "windows_i686_gnu", 427 | "windows_i686_msvc", 428 | "windows_x86_64_gnu", 429 | "windows_x86_64_gnullvm", 430 | "windows_x86_64_msvc", 431 | ] 432 | 433 | [[package]] 434 | name = "windows_aarch64_gnullvm" 435 | version = "0.48.5" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 438 | 439 | [[package]] 440 | name = "windows_aarch64_msvc" 441 | version = "0.48.5" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 444 | 445 | [[package]] 446 | name = "windows_i686_gnu" 447 | version = "0.48.5" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 450 | 451 | [[package]] 452 | name = "windows_i686_msvc" 453 | version = "0.48.5" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 456 | 457 | [[package]] 458 | name = "windows_x86_64_gnu" 459 | version = "0.48.5" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 462 | 463 | [[package]] 464 | name = "windows_x86_64_gnullvm" 465 | version = "0.48.5" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 468 | 469 | [[package]] 470 | name = "windows_x86_64_msvc" 471 | version = "0.48.5" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 474 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-minify" 3 | description = "A cargo utility to automatically remove unused code from a Rust project." 4 | categories = [ "development-tools::cargo-plugins" ] 5 | version = "0.5.0" 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/tweedegolf/cargo-minify" 8 | homepage = "https://github.com/tweedegolf/cargo-minify" 9 | edition = "2021" 10 | publish = true 11 | 12 | [[example]] 13 | name = "useless" 14 | 15 | [dependencies] 16 | cargo_metadata = "0.17" 17 | diff = "0.1.13" 18 | git2 = "0.17" 19 | glob-match = "0.2.1" 20 | gumdrop = "0.8" 21 | multimap = "0.9" 22 | nu-ansi-term = "0.49.0" 23 | proc-macro2 = { version = "1.0.66", features = ["span-locations"] } 24 | syn = { version = "2.0.28", features = ["full"] } 25 | thiserror = "1.0.44" 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-minify 2 | 3 | Remove unused code from your Rust programs. Primarily aimed at generated code (prime example: bindings generated by 4 | bindgen), but also usable in more general context. 5 | 6 | ## Limitations 7 | 8 | Public functions and types in libraries (which makes sense) but also in binaries and examples are not considered unused, 9 | as well as code that is explicitly allowed to be unused (using `#[allow(unused)]`). 10 | 11 | ## Installation 12 | 13 | ```shell 14 | cargo install cargo-minify 15 | ``` 16 | 17 | ## Usage 18 | 19 | After installation, you can run this tool by simply typing `cargo minify` from your crate root. 20 | This runs it on your project and will print out any changes that will be made to your code. 21 | 22 | To actually apply these changes, you have to run `cargo minify --apply`. 23 | 24 | You can perform a more precise minifcation by using the `--ignore` option, followed by a 25 | wildcard specification. Unused code in the excluded files will not be touched. You can also you 26 | the `--kinds` flag to specify which types of unused code to remove. Supported are: 27 | 28 | * `FUNCTION`, which will remove unused function defintions 29 | * `ASSOCIATED_FUNCTION`, which will remove unused associated functions from `impl` blocks 30 | * `STRUCT`, `ENUM`, `UNION`, which will remove unused type definitions of said type 31 | * `TYPE_ALIAS`, which removes unused type aliases 32 | * `CONST`, which will remove unused constants 33 | * `STATIC`, which will remove unused static variables 34 | 35 | Without any `--kinds` specification, all of the above will be removed. 36 | 37 | `cargo minify --apply` expects your files to be under control of version control; if this is not 38 | the case a warning will be given and no changes will be made; this can be overridden using the 39 | `--allow-no-vcs`, `--allow-dirty`, and `--allow-staged` flags. 40 | 41 | Of course you can also view this information (and other options) by running `cargo minify --help`. 42 | 43 | ## Future work 44 | 45 | Still to add to `cargo minify`: 46 | 47 | * Remove unused `static` variables. 48 | * Detected and remove unused derived traits. 49 | 50 | ## License 51 | 52 | Licensed under either of 53 | 54 | * Apache License, Version 2.0 55 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 56 | * MIT license 57 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 58 | 59 | at your option. 60 | 61 | ## Contribution 62 | 63 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as 64 | defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 65 | -------------------------------------------------------------------------------- /examples/useless.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | 5 | /// This code *is* used, and useful to see if our formatting and diff filtering 6 | /// is working properly. 7 | mod used {} 8 | 9 | /// This one is also used, but the function inside is unused. 10 | mod function_unused { 11 | fn huk() {} 12 | 13 | /// A mod inside a mod... inside a mod? 14 | mod function_unused { 15 | fn bar() {} 16 | } 17 | } 18 | 19 | pub const BAR: &str = "Hello, world!"; 20 | const FOO: usize = 5; 21 | 22 | fn foo() {} 23 | 24 | pub fn bar() {} 25 | 26 | struct Foo {} 27 | 28 | impl Foo { 29 | fn new() {} 30 | } 31 | 32 | /// Another one of these super helpful modules that allow us to see whether our 33 | /// formatting and diff filtering work well. 34 | mod also_used {} 35 | 36 | /// And yet another one of these super helpful modules that allow us to see 37 | /// whether our formatting and diff filtering work well. 38 | mod also_also_used {} 39 | 40 | pub enum Bar {} 41 | 42 | union Baz { 43 | baz: bool, 44 | } 45 | 46 | type Qux = Bar; 47 | 48 | extern "C" { 49 | fn baz(); 50 | } 51 | 52 | macro_rules! foo { 53 | () => { 54 | fn foo() {} 55 | }; 56 | } 57 | 58 | macro_rules! huk { 59 | () => { 60 | fn huk() {} 61 | }; 62 | } 63 | 64 | /// The generated function is unused, but won't be removed by cargo-minify 65 | huk!(); 66 | 67 | /// Let's finish with yet another extremely useful module. 68 | mod used_as_well {} 69 | -------------------------------------------------------------------------------- /src/cauterize.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::Range, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use syn::{spanned::Spanned, File}; 7 | 8 | use crate::unused::{UnusedDiagnostic, UnusedDiagnosticKind}; 9 | 10 | const SPACE: u8 = b' '; 11 | const NEWLINE: u8 = b'\n'; 12 | 13 | pub struct Change { 14 | file_name: PathBuf, 15 | original_content: Vec, 16 | proposed_content: Vec, 17 | } 18 | 19 | impl Change { 20 | pub fn file_name(&self) -> &Path { 21 | &self.file_name 22 | } 23 | 24 | pub fn original_content(&self) -> &[u8] { 25 | &self.original_content 26 | } 27 | 28 | pub fn proposed_content(&self) -> &[u8] { 29 | &self.proposed_content 30 | } 31 | } 32 | 33 | /// Finds the position of the first whitespace that is considered belonging 34 | /// to the next definition/declaration (this is a kind of "heuristic") 35 | /// Current heuristic: 36 | /// - leave prefixing whitespace as is (if it doesn't contain a newline) 37 | /// - remove the rest of the line (if there is a newline) 38 | fn find_prefix_whitespace(src: &[u8]) -> usize { 39 | (0..src.len()) 40 | .rev() 41 | .take_while(|&k| src[k].is_ascii_whitespace()) 42 | .find(|&k| src[k] == NEWLINE) 43 | .map(|j| j + 1) 44 | .unwrap_or(src.len()) 45 | } 46 | 47 | /// Finds the position of the first whitespace that is not considered belonging 48 | /// to the previous definition/declaration (this is kind of "heuristic") 49 | /// Current heuristic: 50 | /// - if there is a newline, eat all space before it, and the newline 51 | /// - if there is no newline, eat all trailing whitespace until the next token 52 | fn find_suffix_whitespace(src: &[u8]) -> usize { 53 | src.iter() 54 | .position(|c| *c != SPACE) 55 | .map(|pos| if src[pos] == NEWLINE { pos + 1 } else { pos }) 56 | .unwrap_or(src.len()) 57 | } 58 | 59 | /// Turns a list of "locations of identifiers" into a list of "chunks" 60 | fn diagnostics_to_ranges<'a>( 61 | src: &'a [u8], 62 | idents: impl IntoIterator + 'a, 63 | ) -> Result> + 'a, syn::Error> { 64 | let s = String::from_utf8_lossy(src); 65 | let parsed = syn::parse_str::(&s)?; 66 | 67 | let cumulative_lengths = line_offsets(src); 68 | 69 | let ranges = idents 70 | .into_iter() 71 | .flat_map(move |(kind, ident)| { 72 | parsed.items.iter().find_map(|item| { 73 | use syn::{ForeignItem, ImplItem, Item}; 74 | use UnusedDiagnosticKind::*; 75 | let item_ident = match item { 76 | Item::Const(obj) if kind == Constant => &obj.ident, 77 | Item::Enum(obj) if kind == Enum => &obj.ident, 78 | Item::Fn(obj) if kind == Function => &obj.sig.ident, 79 | Item::Macro(syn::ItemMacro { 80 | ident: Some(name), .. 81 | }) if kind == MacroDefinition => name, 82 | Item::Static(obj) if kind == Static => &obj.ident, 83 | Item::Struct(obj) if kind == Struct => &obj.ident, 84 | Item::Type(obj) if kind == TypeAlias => &obj.ident, 85 | Item::Union(obj) if kind == Union => &obj.ident, 86 | Item::Mod(block) => return handle_mod_diagnostic(block, &kind, &ident), 87 | Item::ForeignMod(block) => { 88 | return block.items.iter().find_map(|item| { 89 | let item_ident = match item { 90 | ForeignItem::Fn(obj) if kind == Function => &obj.sig.ident, 91 | ForeignItem::Static(obj) if kind == Static => &obj.ident, 92 | ForeignItem::Type(obj) if kind == TypeAlias => &obj.ident, 93 | _ => return None, 94 | }; 95 | 96 | if *item_ident == ident { 97 | Some(item.span()) 98 | } else { 99 | None 100 | } 101 | }) 102 | } 103 | Item::Impl(block) => { 104 | return block.items.iter().find_map(|item| { 105 | let item_ident = match item { 106 | ImplItem::Const(obj) if kind == Constant => &obj.ident, 107 | ImplItem::Fn(obj) if kind == AssociatedFunction => &obj.sig.ident, 108 | ImplItem::Type(obj) if kind == TypeAlias => &obj.ident, 109 | _ => return None, 110 | }; 111 | 112 | if *item_ident == ident { 113 | Some(item.span()) 114 | } else { 115 | None 116 | } 117 | }) 118 | } 119 | _ => return None, 120 | }; 121 | 122 | if *item_ident == ident { 123 | Some(item.span()) 124 | } else { 125 | None 126 | } 127 | }) 128 | }) 129 | .map(move |span| to_range(&cumulative_lengths, span)); 130 | 131 | Ok(ranges) 132 | } 133 | 134 | /// Handles (inline) module content 135 | fn handle_mod_diagnostic(block: &syn::ItemMod, kind: &UnusedDiagnosticKind, ident: &str) -> Option { 136 | use syn::Item; 137 | use UnusedDiagnosticKind::*; 138 | 139 | block.content.iter().find_map(|(_b, items)| { 140 | items.iter().find_map(|item| { 141 | let item_ident = match item { 142 | Item::Fn(obj) if *kind == Function => &obj.sig.ident, 143 | Item::Static(obj) if *kind == Static => &obj.ident, 144 | Item::Type(obj) if *kind == TypeAlias => &obj.ident, 145 | Item::Mod(obj) => return handle_mod_diagnostic(obj, kind, ident), 146 | _ => return None, 147 | }; 148 | 149 | if item_ident == ident { 150 | Some(item.span()) 151 | } else { 152 | None 153 | } 154 | }) 155 | }) 156 | } 157 | 158 | fn expand_ranges_to_include_whitespace<'a>( 159 | src: &'a [u8], 160 | iter: impl Iterator> + 'a, 161 | ) -> impl Iterator> + 'a { 162 | iter.map(|range| { 163 | find_prefix_whitespace(&src[..range.start]) 164 | ..find_suffix_whitespace(&src[range.end..]) + range.end 165 | }) 166 | } 167 | 168 | /// Deletes a list-of-positions-of-identifiers from a bytearray that is valid 169 | /// rust code BUGS: if the position is in the body of a function, it will try to 170 | /// delete identifiers there ... probably? 171 | pub fn delete_chunks(src: &[u8], chunks_to_delete: &[Range]) -> Vec { 172 | src.iter() 173 | .enumerate() 174 | .filter_map(|(i, &byte)| { 175 | if chunks_to_delete.iter().any(|range| range.contains(&i)) { 176 | None 177 | } else { 178 | Some(byte) 179 | } 180 | }) 181 | .collect() 182 | } 183 | 184 | /// Deletes a list-of-positions-of-identifiers from a bytearray that is valid 185 | /// rust code BUGS: if the position is in the body of a function, it will try to 186 | /// delete identifiers there ... probably? 187 | pub fn rust_delete( 188 | src: &[u8], 189 | diagnostics: impl IntoIterator, 190 | ) -> Result, syn::Error> { 191 | let chunks_to_delete = 192 | expand_ranges_to_include_whitespace(src, diagnostics_to_ranges(src, diagnostics)?); 193 | 194 | Ok(delete_chunks(src, &chunks_to_delete.collect::>())) 195 | } 196 | 197 | /// Processes a list of file+list-of-edits into an iterator of 198 | /// filenames+proposed new contents 199 | fn process_files>( 200 | diagnostics: impl IntoIterator, 201 | ) -> impl Iterator { 202 | diagnostics 203 | .into_iter() 204 | .filter_map(|(file_name, diagnostic)| { 205 | let original_content = std::fs::read(&file_name).ok()?; 206 | let removed_unused = rust_delete( 207 | &original_content, 208 | diagnostic.into_iter().map(|warn| (warn.kind, warn.ident)), 209 | ) 210 | .expect("syntax error"); 211 | let proposed_content = remove_empty_blocks(&removed_unused).expect("syntax error"); 212 | 213 | let change = Change { 214 | file_name, 215 | original_content, 216 | proposed_content, 217 | }; 218 | 219 | Some(change) 220 | }) 221 | } 222 | 223 | /// Process a list of UnusedDiagnostics into an iterator of filenames+proposed contents 224 | pub fn process_diagnostics( 225 | diagnostics: impl IntoIterator, 226 | ) -> impl Iterator { 227 | process_files( 228 | diagnostics 229 | .into_iter() 230 | .map(|diagnostic| { 231 | let path = PathBuf::from(&diagnostic.span.file_name); 232 | (path, diagnostic) 233 | }) 234 | .collect::>(), 235 | ) 236 | } 237 | 238 | /// Create a table of byte locations of newline symbols, 239 | /// to translate LineColumn's into exact offsets 240 | fn line_offsets(bytes: &[u8]) -> Vec { 241 | let mut offsets: Vec = bytes 242 | .iter() 243 | .enumerate() 244 | .filter_map(|(pos, b)| match b { 245 | // TODO: Support \r\n 246 | b'\n' => Some(pos + 1), 247 | _ => None, 248 | }) 249 | .collect(); 250 | // First line has no offset 251 | offsets.insert(0, 0); 252 | 253 | offsets 254 | } 255 | 256 | fn to_range(offsets: &[usize], span: proc_macro2::Span) -> Range { 257 | let byte_offset = |pos: proc_macro2::LineColumn| offsets[pos.line - 1] + pos.column; 258 | 259 | byte_offset(span.start())..byte_offset(span.end()) 260 | } 261 | 262 | fn remove_empty_blocks(bytes: &[u8]) -> Result, syn::Error> { 263 | let s = String::from_utf8_lossy(bytes).to_string(); 264 | let ast: File = syn::parse_str(&s)?; 265 | 266 | let cumulative_lengths = line_offsets(bytes); 267 | 268 | let spans = ast 269 | .items 270 | .iter() 271 | .filter_map(|item| match item { 272 | syn::Item::ForeignMod(block) => { 273 | (block.items.is_empty() && block.attrs.is_empty()).then(|| block.span()) 274 | } 275 | syn::Item::Impl(block) => { 276 | (block.items.is_empty() && block.attrs.is_empty() && block.trait_.is_none()) 277 | .then(|| block.span()) 278 | } 279 | _ => None, 280 | }) 281 | .map(|span| to_range(&cumulative_lengths, span)); 282 | 283 | let expanded_spans: Vec> = 284 | expand_ranges_to_include_whitespace(bytes, spans).collect(); 285 | 286 | Ok(delete_chunks(bytes, &expanded_spans)) 287 | } 288 | 289 | /// This actually applies a collection of changes to your filesystem (use with care) 290 | pub fn commit_changes( 291 | changes: impl IntoIterator, 292 | ) -> Result<(), Vec> { 293 | let errors = changes 294 | .into_iter() 295 | .filter_map(|change| std::fs::write(change.file_name, change.proposed_content).err()) 296 | .collect::>(); 297 | 298 | if errors.is_empty() { 299 | Ok(()) 300 | } else { 301 | Err(errors) 302 | } 303 | } 304 | 305 | #[cfg(test)] 306 | mod test { 307 | use super::*; 308 | 309 | fn fun(name: &str) -> (UnusedDiagnosticKind, String) { 310 | (UnusedDiagnosticKind::Function, name.to_owned()) 311 | } 312 | 313 | fn constant(name: &str) -> (UnusedDiagnosticKind, String) { 314 | (UnusedDiagnosticKind::Constant, name.to_owned()) 315 | } 316 | 317 | #[test] 318 | fn identifier_to_span() { 319 | let src = b"fn foo() {} fn foa() -> i32 { barf; } const FOO: i32 = 42;"; 320 | // 01234567890123456789012345678901234567890123456789012345678 321 | // 1 2 3 4 5 322 | let pos = diagnostics_to_ranges(src, [fun("foo"), fun("foa"), constant("FOO")]) 323 | .unwrap() 324 | .collect::>(); 325 | assert_eq!(pos, vec![0..11, 13..38, 39..59]); 326 | } 327 | 328 | #[allow(clippy::single_range_in_vec_init)] 329 | #[test] 330 | fn chunk_deletion() { 331 | let src = b"fn foo() {} fn foa() -> i32 { barf; } const FOO: i32 = 42;"; 332 | // 012345678901234567890123456789012345678901234567890123456 333 | assert_eq!( 334 | delete_chunks(src, &[5..8]), 335 | b"fn fo {} fn foa() -> i32 { barf; } const FOO: i32 = 42;" 336 | ); 337 | } 338 | 339 | #[test] 340 | fn deletion() { 341 | let src = b"fn foo() { }fn foa() -> i32 { barf; }const FOO: i32 = 42;"; 342 | assert_eq!( 343 | rust_delete(src, [fun("foo")]).unwrap(), 344 | b"fn foa() -> i32 { barf; }const FOO: i32 = 42;" 345 | ); 346 | assert_eq!( 347 | rust_delete(src, [fun("foa")]).unwrap(), 348 | b"fn foo() { }const FOO: i32 = 42;" 349 | ); 350 | assert_eq!( 351 | rust_delete(src, [constant("FOO")]).unwrap(), 352 | b"fn foo() { }fn foa() -> i32 { barf; }" 353 | ); 354 | } 355 | 356 | #[test] 357 | fn type_check() { 358 | let src = b"fn foo() { }fn foa() -> i32 { barf; }const FOO: i32 = 42;"; 359 | assert_eq!( 360 | rust_delete(src, [constant("foo")]).unwrap(), 361 | b"fn foo() { }fn foa() -> i32 { barf; }const FOO: i32 = 42;" 362 | ); 363 | assert_eq!( 364 | rust_delete(src, [constant("foa")]).unwrap(), 365 | b"fn foo() { }fn foa() -> i32 { barf; }const FOO: i32 = 42;" 366 | ); 367 | assert_eq!( 368 | rust_delete(src, [fun("FOO")]).unwrap(), 369 | b"fn foo() { }fn foa() -> i32 { barf; }const FOO: i32 = 42;" 370 | ); 371 | } 372 | 373 | #[test] 374 | fn formatting_preserval() { 375 | let src = b" fn foo(){} fn foa() -> huk { barf; } const FOO: i32 = 42; fn bar(){ } "; 376 | assert_eq!( 377 | rust_delete(src, [fun("foo")]).unwrap(), 378 | b" fn foa() -> huk { barf; } const FOO: i32 = 42; fn bar(){ } " 379 | ); 380 | assert_eq!( 381 | rust_delete(src, [fun("foa")]).unwrap(), 382 | b" fn foo(){} const FOO: i32 = 42; fn bar(){ } " 383 | ); 384 | assert_eq!( 385 | rust_delete(src, [constant("FOO")]).unwrap(), 386 | b" fn foo(){} fn foa() -> huk { barf; } fn bar(){ } " 387 | ); 388 | assert_eq!( 389 | rust_delete(src, [fun("bar")]).unwrap(), 390 | b" fn foo(){} fn foa() -> huk { barf; } const FOO: i32 = 42; " 391 | ); 392 | 393 | assert_eq!( 394 | rust_delete(src, [fun("foa"), fun("foo")]).unwrap(), 395 | b" const FOO: i32 = 42; fn bar(){ } " 396 | ); 397 | assert_eq!( 398 | rust_delete(src, [fun("foa"), constant("FOO")]).unwrap(), 399 | b" fn foo(){} fn bar(){ } " 400 | ); 401 | } 402 | 403 | #[test] 404 | #[rustfmt::skip] 405 | fn whitespace_semi_preserval() { 406 | let src = b" fn foo() {} fn fixme() {} fn main() {}"; 407 | assert_eq!( 408 | rust_delete(src, [fun("fixme")]).unwrap(), 409 | b" fn foo() {} fn main() {}" 410 | ); 411 | let src = b" fn foo() {} fn fixme() {}fn main() {}"; 412 | assert_eq!( 413 | rust_delete(src, [fun("fixme")]).unwrap(), 414 | b" fn foo() {} fn main() {}" 415 | ); 416 | let src = b" fn foo() {}fn fixme() {} fn main() {}"; 417 | assert_eq!( 418 | rust_delete(src, [fun("fixme")]).unwrap(), 419 | b" fn foo() {}fn main() {}" 420 | ); 421 | let src = b" fn foo() {}\nfn fixme() {}\nfn main() {}"; 422 | assert_eq!( 423 | rust_delete(src, [fun("fixme")]).unwrap(), 424 | b" fn foo() {}\nfn main() {}" 425 | ); 426 | let src = b" fn foo() {}\n\nfn fixme() {}\nfn main() {}"; 427 | assert_eq!( 428 | rust_delete(src, [fun("fixme")]).unwrap(), 429 | b" fn foo() {}\n\nfn main() {}" 430 | ); 431 | let src = b" fn foo() {}\nfn fixme() {}\n\nfn main() {}"; 432 | assert_eq!( 433 | rust_delete(src, [fun("fixme")]).unwrap(), 434 | b" fn foo() {}\n\nfn main() {}" 435 | ); 436 | let src = b" fn foo() {}\n\nfn fixme() {}\n\nfn main() {}"; 437 | assert_eq!( 438 | rust_delete(src, [fun("fixme")]).unwrap(), 439 | b" fn foo() {}\n\n\nfn main() {}" 440 | ); 441 | 442 | let src = b"fn foo() {}\n fn fixme() {}\n fn main() {}"; 443 | assert_eq!( 444 | rust_delete(src, [fun("fixme")]).unwrap(), 445 | b"fn foo() {}\n fn main() {}" 446 | ); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/diff_format.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use nu_ansi_term::Color; 4 | use thiserror::Error; 5 | 6 | use crate::cauterize::Change; 7 | 8 | const BEFORE_CONTEXT: isize = 3; 9 | const AFTER_CONTEXT: isize = 3; 10 | 11 | pub fn println(change: &Change, color_mode: ColorMode) { 12 | let text = format!("#\n#\tshowing diff for {:?}:\n#", change.file_name()); 13 | if color_mode.enabled() { 14 | println!("{}", Color::DarkGray.paint(text)); 15 | } else { 16 | println!("{text}") 17 | } 18 | 19 | let left = String::from_utf8_lossy(change.original_content()); 20 | let right = String::from_utf8_lossy(change.proposed_content()); 21 | 22 | let diff = diff::lines(&left, &right); 23 | 24 | let mut included = Vec::new(); 25 | 26 | let mut last_change: isize = -AFTER_CONTEXT - 1; 27 | let mut last_insert: isize = -AFTER_CONTEXT - 1; 28 | for index in 0..diff.len() as isize { 29 | if has_changed(&diff[index as usize]) { 30 | if last_insert < index - BEFORE_CONTEXT - 1 { 31 | included.push(DiffLine::Ellipsis); 32 | } 33 | for index in (index - BEFORE_CONTEXT).max(last_insert + 1).max(0)..index { 34 | included.push(DiffLine::Context(get_line(&diff[index as usize]))); 35 | } 36 | included.push(DiffLine::Diff(diff[index as usize].clone())); 37 | last_insert = index; 38 | last_change = index; 39 | } else if index - last_change <= AFTER_CONTEXT { 40 | included.push(DiffLine::Context(get_line(&diff[index as usize]))); 41 | last_insert = index; 42 | } 43 | } 44 | if last_insert < diff.len() as isize - 1 { 45 | included.push(DiffLine::Ellipsis); 46 | } 47 | 48 | for line in included { 49 | let (symbol, color, line) = match line { 50 | DiffLine::Diff(diff::Result::Left(line)) => ('-', Color::LightRed, line), 51 | DiffLine::Diff(diff::Result::Right(line)) => ('+', Color::LightGreen, line), 52 | DiffLine::Diff(diff::Result::Both(_, _)) => unreachable!(), 53 | DiffLine::Context(line) => (' ', Color::Default, line), 54 | DiffLine::Ellipsis => ('#', Color::DarkGray, "..."), 55 | }; 56 | 57 | let format = format!("{symbol}\t{line}"); 58 | 59 | if color_mode.enabled() { 60 | println!("{}", color.paint(format)); 61 | } else { 62 | println!("{format}"); 63 | } 64 | } 65 | } 66 | 67 | fn has_changed(diff: &diff::Result<&str>) -> bool { 68 | match diff { 69 | diff::Result::Left(_) | diff::Result::Right(_) => true, 70 | diff::Result::Both(_, _) => false, 71 | } 72 | } 73 | 74 | fn get_line<'a>(diff: &diff::Result<&'a str>) -> &'a str { 75 | match diff { 76 | diff::Result::Left(line) | diff::Result::Right(line) => line, 77 | diff::Result::Both(line, _) => line, 78 | } 79 | } 80 | 81 | enum DiffLine { 82 | Diff(diff::Result), 83 | Context(T), 84 | Ellipsis, 85 | } 86 | 87 | #[derive(Copy, Clone, Debug, Default)] 88 | pub enum ColorMode { 89 | #[default] 90 | Auto, 91 | Always, 92 | Never, 93 | } 94 | 95 | impl ColorMode { 96 | pub fn enabled(&self) -> bool { 97 | match self { 98 | // TODO: Improve 99 | ColorMode::Auto => true, 100 | ColorMode::Always => true, 101 | ColorMode::Never => false, 102 | } 103 | } 104 | } 105 | 106 | impl FromStr for ColorMode { 107 | type Err = UnsupportedPrintColor; 108 | 109 | fn from_str(s: &str) -> Result { 110 | match s { 111 | "auto" => Ok(ColorMode::Auto), 112 | "always" => Ok(ColorMode::Always), 113 | "never" => Ok(ColorMode::Never), 114 | _ => Err(UnsupportedPrintColor), 115 | } 116 | } 117 | } 118 | 119 | #[derive(Debug, Error)] 120 | #[error("unsupported color mode, pick any of: auto, always, never")] 121 | pub struct UnsupportedPrintColor; 122 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("{0}")] 8 | Io(#[from] std::io::Error), 9 | #[error("{0}")] 10 | Utf8(#[from] std::string::FromUtf8Error), 11 | 12 | #[error("{0}")] 13 | CommandLine(#[from] gumdrop::Error), 14 | 15 | #[error("invalid command line arguments: {0}")] 16 | Args(&'static str), 17 | } 18 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io, io::Write, path::PathBuf}; 2 | 3 | use gumdrop::Options; 4 | 5 | use crate::{ 6 | diff_format::ColorMode, 7 | error::{Error, Result}, 8 | unused::UnusedDiagnosticKind, 9 | }; 10 | 11 | mod cauterize; 12 | mod diff_format; 13 | mod error; 14 | mod resolver; 15 | mod unused; 16 | mod vcs; 17 | 18 | const SUBCOMMAND_NAME: &str = "minify"; 19 | 20 | #[derive(Debug, Options)] 21 | struct MinifyOptions { 22 | #[options(help = "No output printed to stdout")] 23 | quiet: bool, 24 | 25 | #[options(help = "Package to minify", meta = "SPEC")] 26 | package: Vec, 27 | #[options(no_short, help = "Minify all packages in the workspace")] 28 | workspace: bool, 29 | #[options(no_short, help = "Exclude packages from the minify", meta = "SPEC")] 30 | exclude: Vec, 31 | 32 | #[options(help = "File to minify", meta = "SPEC")] 33 | file: Vec, 34 | #[options(help = "Ignore files from the minify", meta = "SPEC")] 35 | ignore: Vec, 36 | 37 | #[options( 38 | help = "specify which kinds of diagnostics to apply (all by default)", 39 | meta = "< FUNCTION | CONST | STATIC | STRUCT | ENUM | UNION | TYPE_ALIAS | \ 40 | ASSOCIATED_FUNCTION | MACRO_DEFINITION >" 41 | )] 42 | kinds: Vec, 43 | 44 | #[options(no_short, help = "Apply changes instead of outputting a diff")] 45 | apply: bool, 46 | 47 | #[options(help = "Print help message")] 48 | help: bool, 49 | 50 | #[options(no_short, help = "Coloring: auto, always, never", meta = "WHEN")] 51 | color: ColorMode, 52 | 53 | #[options(no_short, help = "Path to Cargo.toml", meta = "PATH")] 54 | manifest_path: Option, 55 | 56 | #[options(no_short, help = "Fix code even if the working directory is dirty")] 57 | allow_dirty: bool, 58 | 59 | #[options(no_short, help = "Fix code even if there are staged files in the VCS")] 60 | allow_staged: bool, 61 | 62 | #[options(no_short, help = "Also operate if no version control system was found")] 63 | allow_no_vcs: bool, 64 | } 65 | 66 | fn main() { 67 | // Drop the first actual argument if it is equal to our subcommand 68 | // (i.e. we are being called via 'cargo') 69 | let mut args = env::args().peekable(); 70 | args.next(); 71 | 72 | if args.peek().map(|s| s.as_str()) == Some(SUBCOMMAND_NAME) { 73 | args.next(); 74 | } 75 | 76 | let mini_help = || { 77 | eprintln!(); 78 | eprintln!("For more information, try '--help'"); 79 | }; 80 | 81 | let status_code = match execute(&args.collect::>()) { 82 | Err(Error::Io(err)) => { 83 | eprintln!("IO error: {}", err); 84 | 3 85 | } 86 | Err(Error::Utf8(err)) => { 87 | eprintln!("Encoding error: {}", err); 88 | 2 89 | } 90 | Err(Error::Args(err)) => { 91 | eprintln!("error: {}", err); 92 | mini_help(); 93 | 1 94 | } 95 | Err(Error::CommandLine(err)) => { 96 | eprintln!("error: {}", err); 97 | mini_help(); 98 | 1 99 | } 100 | _ => 0, 101 | }; 102 | 103 | io::stdout().flush().unwrap(); 104 | 105 | std::process::exit(status_code); 106 | } 107 | 108 | fn execute(args: &[String]) -> Result<()> { 109 | let opts = MinifyOptions::parse_args_default(args)?; 110 | let manifest_path = opts.manifest_path.as_ref().map(PathBuf::from); 111 | let crate_resolution = CrateResolutionOptions::from_options(&opts)?; 112 | let file_resolution = FileResolutionOptions::from_options(&opts)?; 113 | 114 | if opts.help { 115 | println!("{}", MinifyOptions::usage()); 116 | } else { 117 | let unused = unused::get_unused( 118 | manifest_path.as_deref(), 119 | &crate_resolution, 120 | &file_resolution, 121 | &opts.kinds, 122 | )?; 123 | let changes: Vec<_> = cauterize::process_diagnostics(unused).collect(); 124 | 125 | if !opts.quiet { 126 | if changes.is_empty() { 127 | eprintln!("no unused code that can be minified") 128 | } else { 129 | for change in &changes { 130 | diff_format::println(change, opts.color); 131 | } 132 | } 133 | } 134 | 135 | let cargo_root = resolver::get_cargo_metadata(manifest_path.as_deref())?.workspace_root; 136 | 137 | if opts.apply { 138 | use vcs::Status; 139 | match vcs::status(&cargo_root) { 140 | Status::Error(e) => { 141 | eprintln!("git problem: {}", e) 142 | } 143 | Status::NoVCS if !opts.allow_no_vcs => { 144 | eprintln!( 145 | "no VCS found for this package and `cargo minify` can potentially perform \ 146 | destructive changes; if you'd like to suppress this error pass \ 147 | `--allow-no-vcs`" 148 | ); 149 | } 150 | Status::Unclean { dirty, staged } 151 | if !(dirty.is_empty() || opts.allow_dirty) 152 | || !(staged.is_empty() || opts.allow_staged) => 153 | { 154 | eprintln!("working directory contains dirty/staged files:"); 155 | for file in dirty { 156 | eprintln!("\t{} (dirty)", file) 157 | } 158 | for file in staged { 159 | eprintln!("\t{} (staged)", file) 160 | } 161 | eprintln!( 162 | "please fix this or ignore this warning with --allow-dirty and/or \ 163 | --allow-staged" 164 | ); 165 | } 166 | _ => { 167 | // TODO: Remove unwrap 168 | cauterize::commit_changes(changes).unwrap(); 169 | } 170 | } 171 | } else if !changes.is_empty() { 172 | println!("run with --apply to apply these changes") 173 | } 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | pub enum CrateResolutionOptions<'a> { 180 | Root, 181 | Workspace { exclude: &'a [String] }, 182 | Package { packages: &'a [String] }, 183 | } 184 | 185 | impl<'a> CrateResolutionOptions<'a> { 186 | fn from_options(opts: &'a MinifyOptions) -> Result { 187 | match ( 188 | opts.workspace, 189 | !opts.package.is_empty(), 190 | !opts.exclude.is_empty(), 191 | ) { 192 | (true, false, true) | (true, false, false) => Ok(CrateResolutionOptions::Workspace { 193 | exclude: &opts.exclude, 194 | }), 195 | (false, true, false) => Ok(CrateResolutionOptions::Package { 196 | packages: &opts.package, 197 | }), 198 | (false, false, false) => Ok(CrateResolutionOptions::Root), 199 | (true, true, false) | (false, true, true) | (true, true, true) => Err(Error::Args( 200 | "either specify --workspace and optionally --exclude specific targets, or specify \ 201 | specific targets with --package", 202 | )), 203 | (false, false, true) => Err(Error::Args( 204 | "--exclude can only be used in conjunction with --workspace", 205 | )), 206 | } 207 | } 208 | } 209 | 210 | pub enum FileResolutionOptions<'a> { 211 | Only(&'a [String]), 212 | AllBut(&'a [String]), 213 | } 214 | 215 | impl<'a> FileResolutionOptions<'a> { 216 | fn from_options(opts: &'a MinifyOptions) -> Result { 217 | match (!opts.file.is_empty(), !opts.ignore.is_empty()) { 218 | (false, false) | (false, true) => Ok(FileResolutionOptions::AllBut(&opts.ignore)), 219 | (true, false) => Ok(FileResolutionOptions::Only(&opts.file)), 220 | (true, true) => Err(Error::Args( 221 | "either specify --ignore to minify all files except", 222 | )), 223 | } 224 | } 225 | 226 | pub fn is_included(&self, file_name: &str) -> bool { 227 | match self { 228 | FileResolutionOptions::Only(files) => files 229 | .iter() 230 | .any(|file| glob_match::glob_match(file, file_name)), 231 | FileResolutionOptions::AllBut(ignored) => ignored 232 | .iter() 233 | .all(|ignore| !glob_match::glob_match(ignore, file_name)), 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/resolver.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the functionality necessary to find which packages the 2 | //! user wants to minify. 3 | 4 | // Portions of the below code are inspired by/taken from Rustfmt, https://github.com/rust-lang/rustfmt 5 | // Copyright (c) 2016-2021 The Rust Project Developers 6 | 7 | use std::{ 8 | collections::{BTreeSet, HashSet}, 9 | env, io, 10 | path::{Path, PathBuf}, 11 | }; 12 | 13 | use cargo_metadata::Target; 14 | 15 | use crate::{error::Result, CrateResolutionOptions}; 16 | 17 | pub fn get_targets( 18 | manifest_path: Option<&Path>, 19 | crate_resolution: &CrateResolutionOptions, 20 | ) -> Result> { 21 | let mut targets = HashSet::new(); 22 | 23 | match crate_resolution { 24 | CrateResolutionOptions::Root => root_targets(manifest_path, &mut targets)?, 25 | CrateResolutionOptions::Workspace { exclude } => { 26 | workspace_targets(manifest_path, exclude, &mut targets, &mut BTreeSet::new())? 27 | } 28 | CrateResolutionOptions::Package { packages } => { 29 | package_targets(manifest_path, packages, &mut targets)? 30 | } 31 | } 32 | 33 | if targets.is_empty() { 34 | eprintln!("crate resolution found no targets"); 35 | } 36 | 37 | Ok(targets) 38 | } 39 | 40 | fn root_targets(manifest_path: Option<&Path>, targets: &mut HashSet) -> Result<()> { 41 | let metadata = get_cargo_metadata(manifest_path)?; 42 | let workspace_root_path = PathBuf::from(&metadata.workspace_root).canonicalize()?; 43 | let (in_workspace_root, current_dir_manifest) = if let Some(target_manifest) = manifest_path { 44 | ( 45 | workspace_root_path == target_manifest, 46 | target_manifest.canonicalize()?, 47 | ) 48 | } else { 49 | let current_dir = env::current_dir()?.canonicalize()?; 50 | ( 51 | workspace_root_path == current_dir, 52 | current_dir.join("Cargo.toml"), 53 | ) 54 | }; 55 | 56 | let package_targets = match metadata.packages.len() { 57 | 1 => metadata.packages.into_iter().next().unwrap().targets, 58 | _ => metadata 59 | .packages 60 | .into_iter() 61 | .filter(|p| { 62 | in_workspace_root 63 | || PathBuf::from(&p.manifest_path) 64 | .canonicalize() 65 | .unwrap_or_default() 66 | == current_dir_manifest 67 | }) 68 | .flat_map(|p| p.targets) 69 | .collect(), 70 | }; 71 | 72 | for target in package_targets { 73 | targets.insert(target); 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | fn workspace_targets( 80 | manifest_path: Option<&Path>, 81 | exclude: &[String], 82 | targets: &mut HashSet, 83 | visited: &mut BTreeSet, 84 | ) -> Result<()> { 85 | let metadata = get_cargo_metadata(manifest_path)?; 86 | for package in &metadata.packages { 87 | if !exclude 88 | .iter() 89 | .any(|name| glob_match::glob_match(name, &package.name)) 90 | { 91 | for target in &package.targets { 92 | targets.insert(target.clone()); 93 | } 94 | 95 | for dependency in &package.dependencies { 96 | if dependency.path.is_none() || visited.contains(&dependency.name) { 97 | continue; 98 | } 99 | 100 | let manifest_path = 101 | PathBuf::from(dependency.path.as_ref().unwrap()).join("Cargo.toml"); 102 | if manifest_path.exists() 103 | && !metadata 104 | .packages 105 | .iter() 106 | .any(|p| p.manifest_path.eq(&manifest_path)) 107 | { 108 | visited.insert(dependency.name.to_owned()); 109 | workspace_targets(Some(&manifest_path), exclude, targets, visited)?; 110 | } 111 | } 112 | } 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | fn package_targets( 119 | manifest_path: Option<&Path>, 120 | packages: &[String], 121 | targets: &mut HashSet, 122 | ) -> Result<()> { 123 | let metadata = get_cargo_metadata(manifest_path)?; 124 | let mut workspace_hitlist: BTreeSet<&String> = BTreeSet::from_iter(packages); 125 | 126 | for package in metadata.packages { 127 | if workspace_hitlist.remove(&package.name) { 128 | for target in package.targets { 129 | targets.insert(target); 130 | } 131 | } 132 | } 133 | 134 | if workspace_hitlist.is_empty() { 135 | Ok(()) 136 | } else { 137 | let package = workspace_hitlist.iter().next().unwrap(); 138 | Err(io::Error::new( 139 | io::ErrorKind::InvalidInput, 140 | format!("package `{}` is not a member of the workspace", package), 141 | ) 142 | .into()) 143 | } 144 | } 145 | 146 | pub fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result { 147 | let mut cmd = cargo_metadata::MetadataCommand::new(); 148 | cmd.no_deps(); 149 | if let Some(manifest_path) = manifest_path { 150 | cmd.manifest_path(manifest_path); 151 | } 152 | cmd.other_options(vec![String::from("--offline")]); 153 | 154 | match cmd.exec() { 155 | Ok(metadata) => Ok(metadata), 156 | Err(_) => { 157 | cmd.other_options(vec![]); 158 | match cmd.exec() { 159 | Ok(metadata) => Ok(metadata), 160 | Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string()).into()), 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/unused.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Display, Formatter}, 3 | io::BufReader, 4 | path::Path, 5 | process::{Command, Stdio}, 6 | str::FromStr, 7 | }; 8 | 9 | use cargo_metadata::{ 10 | diagnostic::{Diagnostic, DiagnosticSpan}, 11 | Message, 12 | }; 13 | 14 | use crate::{error::Result, resolver, CrateResolutionOptions, FileResolutionOptions}; 15 | 16 | pub fn get_unused<'a>( 17 | manifest_path: Option<&Path>, 18 | crate_resolution: &CrateResolutionOptions, 19 | file_resolution: &'a FileResolutionOptions, 20 | kinds: &'a [UnusedDiagnosticKind], 21 | ) -> Result + 'a> { 22 | let mut command = Command::new("cargo"); 23 | 24 | command.args(["check", "--all-targets", "--quiet", "--message-format", "json"]); 25 | 26 | match crate_resolution { 27 | CrateResolutionOptions::Root => {} 28 | CrateResolutionOptions::Workspace { exclude } => { 29 | command.arg("--workspace"); 30 | 31 | for package in *exclude { 32 | command.args(["--exclude", package]); 33 | } 34 | } 35 | CrateResolutionOptions::Package { packages } => { 36 | for package in *packages { 37 | command.args(["-p", package]); 38 | } 39 | } 40 | } 41 | 42 | let mut child = command.stdout(Stdio::piped()).spawn()?; 43 | let stdout = child.stdout.take().unwrap(); 44 | let reader = BufReader::new(stdout); 45 | 46 | let targets = resolver::get_targets(manifest_path, crate_resolution)?; 47 | 48 | let unused = Message::parse_stream(reader) 49 | .flatten() 50 | .filter_map(|message| { 51 | if let Message::CompilerMessage(message) = message { 52 | Some(message) 53 | } else { 54 | None 55 | } 56 | }) 57 | .filter(move |message| targets.contains(&message.target)) 58 | .map(|message| message.message) 59 | .filter_map(|diagnostic| UnusedDiagnostic::try_from(diagnostic).ok()) 60 | // Ignore unused warnings originating from macro expansions 61 | .filter(|diagnostic| diagnostic.span.expansion.is_none()) 62 | .filter(|diagnostic| kinds.is_empty() || kinds.contains(&diagnostic.kind)) 63 | .filter(|diagnostic| file_resolution.is_included(&diagnostic.span.file_name)); 64 | 65 | Ok(unused) 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct UnusedDiagnostic { 70 | pub kind: UnusedDiagnosticKind, 71 | pub ident: String, 72 | pub span: DiagnosticSpan, 73 | } 74 | 75 | impl TryFrom for UnusedDiagnostic { 76 | type Error = NotUnusedDiagnostic; 77 | 78 | fn try_from(value: Diagnostic) -> Result { 79 | let message = value.message; 80 | 81 | let (first, message) = message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 82 | match UnusedDiagnosticKind::from_str(first) { 83 | Ok(kind) => { 84 | let message = match kind { 85 | UnusedDiagnosticKind::Constant 86 | | UnusedDiagnosticKind::Static 87 | | UnusedDiagnosticKind::Function 88 | | UnusedDiagnosticKind::Struct 89 | | UnusedDiagnosticKind::Enum 90 | | UnusedDiagnosticKind::Union => message, 91 | UnusedDiagnosticKind::TypeAlias => { 92 | let (alias, message) = 93 | message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 94 | 95 | if alias != "alias" { 96 | return Err(NotUnusedDiagnostic); 97 | } 98 | 99 | message 100 | } 101 | UnusedDiagnosticKind::AssociatedFunction => { 102 | let (function, message) = 103 | message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 104 | 105 | if function != "function" { 106 | return Err(NotUnusedDiagnostic); 107 | } 108 | 109 | message 110 | } 111 | UnusedDiagnosticKind::MacroDefinition => return Err(NotUnusedDiagnostic), 112 | }; 113 | 114 | let (mut ident, message) = message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 115 | ident = ident.strip_prefix('`').ok_or(NotUnusedDiagnostic)?; 116 | ident = ident.strip_suffix('`').ok_or(NotUnusedDiagnostic)?; 117 | let ident = ident.to_owned(); 118 | 119 | let suffix = match kind { 120 | UnusedDiagnosticKind::Constant 121 | | UnusedDiagnosticKind::Static 122 | | UnusedDiagnosticKind::Function 123 | | UnusedDiagnosticKind::Enum 124 | | UnusedDiagnosticKind::Union 125 | | UnusedDiagnosticKind::TypeAlias 126 | | UnusedDiagnosticKind::AssociatedFunction => "is never used", 127 | UnusedDiagnosticKind::Struct => "is never constructed", 128 | UnusedDiagnosticKind::MacroDefinition => return Err(NotUnusedDiagnostic), 129 | }; 130 | 131 | if message != suffix { 132 | return Err(NotUnusedDiagnostic); 133 | } 134 | 135 | let span = value.spans.into_iter().next().ok_or(NotUnusedDiagnostic)?; 136 | 137 | Ok(UnusedDiagnostic { kind, ident, span }) 138 | } 139 | Err(_) => { 140 | if first != "unused" { 141 | return Err(NotUnusedDiagnostic); 142 | } 143 | 144 | let (mut kind, message) = message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 145 | kind = kind.strip_suffix(':').unwrap_or(kind); 146 | let kind: UnusedDiagnosticKind = kind.parse()?; 147 | 148 | let message = match kind { 149 | UnusedDiagnosticKind::Constant 150 | | UnusedDiagnosticKind::Static 151 | | UnusedDiagnosticKind::Function 152 | | UnusedDiagnosticKind::Struct 153 | | UnusedDiagnosticKind::Enum 154 | | UnusedDiagnosticKind::Union 155 | | UnusedDiagnosticKind::TypeAlias 156 | | UnusedDiagnosticKind::AssociatedFunction => return Err(NotUnusedDiagnostic), 157 | UnusedDiagnosticKind::MacroDefinition => { 158 | let (definition, message) = 159 | message.split_once(' ').ok_or(NotUnusedDiagnostic)?; 160 | 161 | if definition != "definition:" { 162 | return Err(NotUnusedDiagnostic); 163 | } 164 | 165 | message 166 | } 167 | }; 168 | 169 | let mut split = message.splitn(2, ' '); 170 | let mut ident = split.next().unwrap(); 171 | let message = split.next().unwrap_or_default(); 172 | 173 | ident = ident.strip_prefix('`').ok_or(NotUnusedDiagnostic)?; 174 | ident = ident.strip_suffix('`').ok_or(NotUnusedDiagnostic)?; 175 | let ident = ident.to_owned(); 176 | 177 | if !message.is_empty() { 178 | return Err(NotUnusedDiagnostic); 179 | } 180 | 181 | let span = value.spans.into_iter().next().ok_or(NotUnusedDiagnostic)?; 182 | 183 | Ok(UnusedDiagnostic { kind, ident, span }) 184 | } 185 | } 186 | } 187 | } 188 | 189 | #[derive(Debug, PartialEq)] 190 | pub enum UnusedDiagnosticKind { 191 | Constant, 192 | Static, 193 | Function, 194 | Struct, 195 | Enum, 196 | Union, 197 | TypeAlias, 198 | AssociatedFunction, 199 | MacroDefinition, 200 | } 201 | 202 | impl FromStr for UnusedDiagnosticKind { 203 | type Err = NotUnusedDiagnostic; 204 | 205 | fn from_str(s: &str) -> Result { 206 | match s 207 | .chars() 208 | .filter(|c| c.is_alphanumeric()) 209 | .map(|c| c.to_ascii_lowercase()) 210 | .collect::() 211 | .as_str() 212 | { 213 | "constant" => Ok(UnusedDiagnosticKind::Constant), 214 | "static" => Ok(UnusedDiagnosticKind::Static), 215 | "function" => Ok(UnusedDiagnosticKind::Function), 216 | "struct" => Ok(UnusedDiagnosticKind::Struct), 217 | "enum" => Ok(UnusedDiagnosticKind::Enum), 218 | "union" => Ok(UnusedDiagnosticKind::Union), 219 | "type" | "typealias" => Ok(UnusedDiagnosticKind::TypeAlias), 220 | "associated" | "associatedfunction" => Ok(UnusedDiagnosticKind::AssociatedFunction), 221 | "macro" | "macrodefinition" => Ok(UnusedDiagnosticKind::MacroDefinition), 222 | _ => Err(NotUnusedDiagnostic), 223 | } 224 | } 225 | } 226 | 227 | #[derive(Debug)] 228 | pub struct NotUnusedDiagnostic; 229 | 230 | impl Display for NotUnusedDiagnostic { 231 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 232 | write!(f, "not an unused-diagnostic") 233 | } 234 | } 235 | 236 | impl std::error::Error for NotUnusedDiagnostic {} 237 | -------------------------------------------------------------------------------- /src/vcs/check_vcs.rs: -------------------------------------------------------------------------------- 1 | // Portions of the below code are inspired by/taken from Cargo, https://github.com/rust-lang/cargo/ 2 | // Copyright (c) 2016-2021 The Cargo Developers 3 | 4 | use std::path::Path; 5 | 6 | // Check if we are in an existing repo. We define that to be true if either: 7 | // 8 | // 1. We are in a git repo and the path to the new package is not an ignored 9 | // path in that repo. 10 | // 2. We are in an HG repo. 11 | pub fn existing_vcs_repo(path: &Path, cwd: &Path) -> bool { 12 | in_git_repo(path) || hgrepo_discover(path, cwd).is_ok() 13 | } 14 | 15 | fn in_git_repo(path: &Path) -> bool { 16 | if let Ok(repo) = git2::Repository::discover(path) { 17 | // Don't check if the working directory itself is ignored. 18 | if repo.workdir().map_or(false, |workdir| workdir == path) { 19 | true 20 | } else { 21 | !repo.is_path_ignored(path).unwrap_or(false) 22 | } 23 | } else { 24 | false 25 | } 26 | } 27 | 28 | fn hgrepo_discover(path: &Path, cwd: &Path) -> std::io::Result<()> { 29 | std::process::Command::new("hg") 30 | .current_dir(cwd) 31 | .arg("--cwd") 32 | .arg(path) 33 | .arg("root") 34 | .output()?; 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/vcs/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | mod check_vcs; 4 | 5 | pub fn status(path: impl AsRef) -> Status { 6 | check_version_control(path.as_ref()) 7 | } 8 | 9 | pub enum Status { 10 | Clean, 11 | Unclean { 12 | dirty: Vec, 13 | staged: Vec, 14 | }, 15 | NoVCS, 16 | Error(git2::Error), 17 | } 18 | 19 | // Portions of the below code are inspired by/taken from Cargo, https://github.com/rust-lang/cargo/ 20 | // Copyright (c) 2016-2021 The Cargo Developers 21 | 22 | fn check_version_control(path: &Path) -> Status { 23 | if !check_vcs::existing_vcs_repo(path, path) { 24 | return Status::NoVCS; 25 | } 26 | 27 | let mut dirty = Vec::new(); 28 | let mut staged = Vec::new(); 29 | if let Ok(repo) = git2::Repository::discover(path) { 30 | let mut repo_opts = git2::StatusOptions::new(); 31 | repo_opts.include_ignored(false); 32 | repo_opts.include_untracked(true); 33 | let statuses = match repo.statuses(Some(&mut repo_opts)) { 34 | Ok(value) => value, 35 | Err(error) => return Status::Error(error), 36 | }; 37 | for status in statuses.iter() { 38 | if let Some(path) = status.path() { 39 | match status.status() { 40 | git2::Status::CURRENT => (), 41 | git2::Status::INDEX_NEW 42 | | git2::Status::INDEX_MODIFIED 43 | | git2::Status::INDEX_DELETED 44 | | git2::Status::INDEX_RENAMED 45 | | git2::Status::INDEX_TYPECHANGE => staged.push(path.to_string()), 46 | _ => dirty.push(path.to_string()), 47 | }; 48 | } 49 | } 50 | } 51 | 52 | if dirty.is_empty() && staged.is_empty() { 53 | Status::Clean 54 | } else { 55 | Status::Unclean { dirty, staged } 56 | } 57 | } 58 | --------------------------------------------------------------------------------