├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── config.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cranky.toml 3 | -------------------------------------------------------------------------------- /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 = "anyhow" 7 | version = "1.0.58" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 10 | 11 | [[package]] 12 | name = "cargo-cranky" 13 | version = "0.3.0" 14 | dependencies = [ 15 | "anyhow", 16 | "serde", 17 | "toml", 18 | ] 19 | 20 | [[package]] 21 | name = "proc-macro2" 22 | version = "1.0.40" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 25 | dependencies = [ 26 | "unicode-ident", 27 | ] 28 | 29 | [[package]] 30 | name = "quote" 31 | version = "1.0.20" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 34 | dependencies = [ 35 | "proc-macro2", 36 | ] 37 | 38 | [[package]] 39 | name = "serde" 40 | version = "1.0.138" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" 43 | dependencies = [ 44 | "serde_derive", 45 | ] 46 | 47 | [[package]] 48 | name = "serde_derive" 49 | version = "1.0.138" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" 52 | dependencies = [ 53 | "proc-macro2", 54 | "quote", 55 | "syn", 56 | ] 57 | 58 | [[package]] 59 | name = "syn" 60 | version = "1.0.98" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 63 | dependencies = [ 64 | "proc-macro2", 65 | "quote", 66 | "unicode-ident", 67 | ] 68 | 69 | [[package]] 70 | name = "toml" 71 | version = "0.5.9" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 74 | dependencies = [ 75 | "serde", 76 | ] 77 | 78 | [[package]] 79 | name = "unicode-ident" 80 | version = "1.0.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 83 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-cranky" 3 | authors = ["Eric Seppanen "] 4 | description = "Easy to configure wrapper for clippy" 5 | keywords = ["cargo", "clippy", "lint", "lints"] 6 | categories = ["development-tools::cargo-plugins"] 7 | version = "0.3.0" 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | repository = "https://github.com/ericseppanen/cargo-cranky" 11 | readme = "README.md" 12 | rust-version = "1.56" 13 | 14 | [dependencies] 15 | anyhow = "1.0.58" 16 | serde = { version = "1.0.138", features = ["derive"] } 17 | toml = "0.5.9" 18 | 19 | [package.metadata.release] 20 | dev-version = false 21 | pre-release-commit-message = "release {{version}}" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-cranky 2 | 3 | I wish that I could check in a file that would specify Rust lints for my entire Cargo workspace, and have that be applied to all the crates, libraries, binaries, and examples. 4 | 5 | Doing this with just Rust/Cargo/Clippy can be a bit of a pain. `cargo-cranky` makes it a little easier! 6 | 7 | `cargo-cranky` is just a wrapper around `cargo clippy`; it examines your `Cranky.toml` config file, and constructs the necessary `cargo clippy` command line. Most arguments are passed through to clippy, so it should work from every context where clippy works (IDEs, CI scripts, etc). 8 | 9 | For example, if `Cranky.toml` contains this: 10 | 11 | ```toml 12 | warn = [ 13 | "clippy::empty_structs_with_brackets", 14 | "clippy::cast_possible_truncation", 15 | ] 16 | ``` 17 | 18 | and I run `cargo cranky`, I get those extra lints: 19 | ```txt 20 | warning: found empty brackets on struct declaration 21 | --> src/main.rs:11:12 22 | | 23 | 11 | struct Unit {} 24 | | ^^^ 25 | ``` 26 | 27 | ```txt 28 | warning: casting `u64` to `u8` may truncate the value 29 | --> src/main.rs:23:9 30 | | 31 | 23 | x as u8 32 | ``` 33 | 34 | This is exactly the same as manually running `cargo clippy` with the extra parameters `--warn clippy::empty_structs_with_brackets` and `--warn clippy::cast_possible_truncation`. 35 | 36 | You may find some useful clippy lints for your project in the [clippy documentation][clippy-docs]. I recommend browsing the "pedantic" and "restriction" groups. 37 | 38 | ### Installing 39 | 40 | `cargo install cargo-cranky` 41 | 42 | ### Configuring 43 | 44 | Create a file called `Cranky.toml` at the top of your project tree. The file can contain keys `allow`, `warn`, or `deny` that contain an array of clippy lint names. 45 | 46 | Example: 47 | ```toml 48 | deny = [ 49 | # My crate should never need unsafe code. 50 | "unsafe_code", 51 | ] 52 | 53 | warn = [ 54 | "clippy::empty_structs_with_brackets", 55 | "clippy::cast_possible_truncation", 56 | ] 57 | 58 | allow = [ 59 | "clippy::double_comparisons", 60 | ] 61 | ``` 62 | 63 | Note: in the case of overlap, `allow` will always override `warn`, which in turn will always override `deny`. The order of these fields in `Cranky.toml` has no effect. 64 | 65 | ### FAQ 66 | 67 | **Can I specify non-clippy lints?** 68 | 69 | Yes! Try for example `unsafe_code` or `missing_docs`. 70 | 71 | Note: Clippy lints should be specified using the long syntax, e.g. `clippy::some_lint_name`. Clippy will issue a warning if the prefix is missing. 72 | 73 | **Does it work with vscode?** 74 | 75 | Yes! Just type `cranky` into the "Check On Save: Command" setting, or drop this into `settings.json`: 76 | ```txt 77 | { 78 | "rust-analyzer.check.command": "cranky" 79 | } 80 | ``` 81 | 82 | Set it back to "check" (or "clippy") to return to the previous behavior. 83 | 84 | **Is this reckless or non-idiomatic?** 85 | 86 | That depends on how you use it. If your goal is to enforce a non-idiomatic coding style, that's probably not a great idea. 87 | 88 | If you want to suppress lints that are enabled by default, it's probably better to do that using the `#[allow(clippy::some_lint)]` syntax in the source file, since that gives you a chance to add a comment explaining your reasoning. 89 | 90 | The main goal of this tool is to make it easier to enable additional clippy lints, that improve code maintainability or safety (i.e. `clippy::cast_possible_truncation`). 91 | 92 | **I have ~~complaints~~ suggestions!** 93 | 94 | Please [file a GitHub issue][github-issue] if you have ideas that could make this tool better. 95 | 96 | 97 | [github-issue]: https://github.com/ericseppanen/cargo-cranky/issues 98 | [clippy]: https://github.com/rust-lang/rust-clippy#readme 99 | [clippy-docs]: https://rust-lang.github.io/rust-clippy/stable/index.html 100 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs; 3 | use std::io; 4 | 5 | use anyhow::{Context, Result}; 6 | use serde::Deserialize; 7 | 8 | use crate::Options; 9 | 10 | #[derive(Debug, Default, PartialEq, Deserialize)] 11 | pub(crate) struct CrankyConfig { 12 | #[serde(default)] 13 | allow: Vec, 14 | #[serde(default)] 15 | warn: Vec, 16 | #[serde(default)] 17 | deny: Vec, 18 | } 19 | 20 | impl CrankyConfig { 21 | pub(crate) fn get_config(options: &Options) -> Result { 22 | // Search for Cranky.toml in all parent directories. 23 | let mut dir = current_dir() 24 | .expect("current dir") 25 | .canonicalize() 26 | .expect("canonicalize current dir"); 27 | 28 | loop { 29 | let mut config_path = dir.clone(); 30 | config_path.push("Cranky.toml"); 31 | match fs::read(&config_path) { 32 | Ok(toml_bytes) => { 33 | if options.verbose > 0 { 34 | eprintln!("Read config file at {:?}", config_path); 35 | } 36 | let config: CrankyConfig = toml::from_slice(&toml_bytes)?; 37 | return Ok(config); 38 | } 39 | Err(e) => { 40 | match e.kind() { 41 | // Not found? Go up one directory and try again. 42 | io::ErrorKind::NotFound => match dir.parent() { 43 | None => break, 44 | Some(parent) => dir = parent.to_owned(), 45 | }, 46 | // Any other error kind is fatal. 47 | _ => { 48 | Err(e).with_context(|| format!("Failed to read {:?}", config_path))?; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | if options.verbose > 0 { 56 | eprintln!("No Cranky.toml file found."); 57 | } 58 | 59 | // We didn't find a config file. Just run clippy with no additional arguments. 60 | Ok(CrankyConfig::default()) 61 | } 62 | 63 | pub(crate) fn extra_right_args(&self) -> Vec { 64 | let mut args = Vec::new(); 65 | for lint in &self.deny { 66 | args.push(format!("-D{}", lint)); 67 | } 68 | for lint in &self.warn { 69 | args.push(format!("-W{}", lint)); 70 | } 71 | for lint in &self.allow { 72 | args.push(format!("-A{}", lint)); 73 | } 74 | args 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod test { 80 | use super::*; 81 | 82 | #[test] 83 | fn parse_toml_1() { 84 | let toml_bytes = br#" 85 | warn = [ 86 | "aaa", 87 | "bbb", 88 | ]"#; 89 | let config: CrankyConfig = toml::from_slice(toml_bytes).unwrap(); 90 | 91 | assert_eq!( 92 | config, 93 | CrankyConfig { 94 | allow: vec![], 95 | warn: vec!["aaa".into(), "bbb".into()], 96 | deny: vec![], 97 | } 98 | ) 99 | } 100 | 101 | #[test] 102 | fn parse_toml_2() { 103 | let toml_bytes = br#" 104 | allow = [ "aaa" ] 105 | warn = [ "bbb" ] 106 | deny = [ "ccc" ] 107 | "#; 108 | let config: CrankyConfig = toml::from_slice(toml_bytes).unwrap(); 109 | 110 | assert_eq!( 111 | config, 112 | CrankyConfig { 113 | allow: vec!["aaa".into()], 114 | warn: vec!["bbb".into()], 115 | deny: vec!["ccc".into()], 116 | } 117 | ); 118 | 119 | let args = config.extra_right_args().join(" "); 120 | // Ordering matters! deny -> warn -> allow is the intended behavior. 121 | assert_eq!(args, "-Dccc -Wbbb -Aaaa"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::process::{exit, Command}; 4 | 5 | use crate::config::CrankyConfig; 6 | 7 | mod config; 8 | 9 | const USAGE: &str = "Usage: 10 | cargo cranky [options] [--] [...] 11 | 12 | Options: 13 | -h, --help Show this help text. 14 | -v, --verbose Print the inner `cargo clippy` command (additional 15 | invocations will be passed though). 16 | --dry-run Don't run `cargo clippy`; just print what would be run. 17 | "; 18 | 19 | #[derive(Debug, Default)] 20 | struct Options { 21 | dry_run: bool, 22 | verbose: usize, 23 | } 24 | 25 | fn main() -> Result<(), Box> { 26 | let mut left_args: Vec = Vec::default(); 27 | let mut right_args: Vec = Vec::default(); 28 | 29 | let mut found_double_dash = false; 30 | 31 | let mut options = Options::default(); 32 | 33 | // Discard the first two arguments, which are (0) the bin name, and (1) the cargo subcommand name "cranky". 34 | let mut arg_iter = env::args(); 35 | // Ignore the first argument (the name of the binary) 36 | arg_iter.next(); 37 | // The second argument is probably the subcommand ("cranky") if run as "cargo cranky". 38 | // But if it was run as cargo-cranky we may get confused. So enforce that it's present 39 | // and is the expected subcommand. 40 | let subcommand = arg_iter.next(); 41 | if subcommand != Some("cranky".into()) { 42 | eprint!("{}", USAGE); 43 | } 44 | 45 | for arg in arg_iter { 46 | if arg == "-h" || arg == "--help" { 47 | print!("{}", USAGE); 48 | exit(0); 49 | } 50 | if arg == "--dry-run" { 51 | options.dry_run = true; 52 | continue; 53 | } 54 | if arg == "-v" || arg == "--verbose" { 55 | options.verbose += 1; 56 | if options.verbose == 1 { 57 | // Swallow the first call to --verbose. Subsequent calls will be passed through. 58 | continue; 59 | } 60 | } 61 | match (found_double_dash, arg == "--") { 62 | (false, false) => left_args.push(arg), 63 | (false, true) => found_double_dash = true, 64 | (true, false) => right_args.push(arg), 65 | (true, true) => panic!("found >1 double-dash argument"), 66 | } 67 | } 68 | 69 | let config = CrankyConfig::get_config(&options)?; 70 | 71 | right_args.append(&mut config.extra_right_args()); 72 | 73 | let all_args = if right_args.is_empty() { 74 | left_args 75 | } else { 76 | left_args.push("--".to_string()); 77 | left_args.append(&mut right_args); 78 | left_args 79 | }; 80 | 81 | if options.dry_run || options.verbose > 0 { 82 | let print_args = all_args.join(" "); 83 | println!("> cargo clippy {}", print_args); 84 | } 85 | 86 | if !options.dry_run { 87 | let cmd_result = Command::new("cargo").arg("clippy").args(all_args).status(); 88 | let exit_code = match cmd_result { 89 | Ok(exit_status) => { 90 | // Subprocess may return no exit code if it was killed by a signal. 91 | exit_status.code().unwrap_or(1) 92 | } 93 | Err(_) => 1, 94 | }; 95 | exit(exit_code); 96 | } 97 | Ok(()) 98 | } 99 | --------------------------------------------------------------------------------