main.rs (9547B)
1 //! Consult [`README.md`](https://crates.io/crates/lints/). 2 /// CLI-argument parsing. 3 mod args; 4 /// Parsing of `rustc -Whelp` output. 5 mod parse; 6 /// `rustc` execution. 7 mod rustc; 8 use args::{Cmd, E as ArgsErr}; 9 use core::{convert, str}; 10 use parse::{Data, E as ParseErr, LintGroup, WARNINGS}; 11 #[cfg(target_os = "openbsd")] 12 use priv_sep::{Permissions, Promises}; 13 use rustc::E as RustcErr; 14 use std::{ 15 collections::HashSet, 16 env, 17 io::{self, Error, Write as _}, 18 process::ExitCode, 19 }; 20 /// Writes `err` to `stderr`. 21 #[expect( 22 clippy::needless_pass_by_value, 23 reason = "Error is small and makes the signature more pleasant when dealing with Result::map_or_else" 24 )] 25 fn into_exit_code(err: Error) -> ExitCode { 26 writeln!(io::stderr().lock(), "{err}").map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE) 27 } 28 /// Help message. 29 const HELP_MSG: &str = 30 "Denies or allows all lints based on rustc -Whelp and what command is passed 31 32 Usage: lints [COMMAND] [OPTIONS] 33 34 Commands: 35 allow Allows all lints 36 deny Denies all lints 37 help This message 38 version Prints version info 39 40 Options: 41 --all Writes all individual lints even if they're already part of a lint group 42 --allow-undefined-lints Lint groups that contain undefined lints are allowed 43 - If the last argument, stdin is read instead of getting the output from rustc 44 45 When nothing or deny is passed, all lint groups and allow-by-default lints that are not part of a group 46 are set to deny. 47 When allow is passed, \"warnings\" and all deny-by-default lints are set to allow. 48 stdout is written to in the format of the TOML table, lints.rust, which can be added to Cargo.toml."; 49 /// Version of `lints`. 50 const VERSION: &str = concat!("lints ", env!("CARGO_PKG_VERSION")); 51 /// No-op. 52 #[expect(clippy::unnecessary_wraps, reason = "unify with OpenBSD")] 53 #[cfg(not(target_os = "openbsd"))] 54 const fn priv_sep<Never>() -> Result<(), Never> { 55 Ok(()) 56 } 57 /// `pledge(2)`s `c"exec proc rpath stdio unveil"` before `unveil(2)`ing no file-system access. 58 #[cfg(target_os = "openbsd")] 59 fn priv_sep() -> Result<(), Error> { 60 Promises::pledge_raw(c"exec proc rpath stdio unveil") 61 .and_then(|()| Permissions::unveil_raw(c"/", c"")) 62 .map_err(Error::from) 63 } 64 /// Transforms `ascii` into a `str` based on the assumption that it's already been verified to be valid 65 /// UTF-8. 66 #[expect(clippy::unreachable, reason = "want to crash when there is a bug")] 67 fn as_str(ascii: &[u8]) -> &str { 68 str::from_utf8(ascii).unwrap_or_else(|_e| { 69 unreachable!("names of lints and lint groups are verified to be a subset of ASCII") 70 }) 71 } 72 /// Entry to application. 73 #[expect(clippy::too_many_lines, reason = "101 lines is OK")] 74 fn main() -> ExitCode { 75 priv_sep().map_or_else(into_exit_code, |()| { 76 Cmd::try_from_args(env::args_os()).map_or_else(ArgsErr::into_exit_code, |cmd| match cmd { 77 Cmd::Allow(opts) | Cmd::Deny(opts) => { 78 rustc::execute(opts.read_stdin).map_or_else(RustcErr::into_exit_code, |output| { 79 Data::new(&output, opts.allow_undefined_lints) 80 .map_err(ParseErr::into_exit_code) 81 .and_then(|mut data| { 82 let mut stdout = io::stdout().lock(); 83 if opts.all_lints { 84 let level = if matches!(cmd, Cmd::Allow(_)) { 85 "allow" 86 } else { 87 "deny" 88 }; 89 // Since `"warnings"` is both a lint and lint group, `data.groups` 90 // does not contain it; instead `data.warn` is the set of all 91 // `warn`-by-default lints sans the lint `"warnings"`. 92 // We write groups first, so we add an empty `"warnings"` group. 93 data.groups.push(LintGroup { 94 name: WARNINGS.as_bytes(), 95 lints: HashSet::new(), 96 }); 97 data.groups.sort_unstable(); 98 data.groups 99 .into_iter() 100 .try_fold((), |(), group| { 101 writeln!( 102 stdout, 103 "{} = {{ level = \"{level}\", priority = -1 }}", 104 as_str(group.name) 105 ) 106 }) 107 .and_then(|()| { 108 // There are likely more `warn`-by-default lints, so 109 // we use its memory allocation to append the other lints. 110 data.warn.extend_from_slice(&data.allow); 111 // Reduce memory when we can. 112 drop(data.allow); 113 data.warn.extend_from_slice(&data.deny); 114 // Reduce memory when we can. 115 drop(data.deny); 116 data.warn.sort_unstable(); 117 data.warn.iter().try_fold((), |(), lint| { 118 writeln!( 119 stdout, 120 "{} = {{ level = \"{level}\", priority = -1 }}", 121 as_str(lint) 122 ) 123 }) 124 }) 125 } else if matches!(cmd, Cmd::Allow(_)) { 126 writeln!( 127 stdout, 128 "{WARNINGS} = {{ level = \"allow\", priority = -1 }}" 129 ) 130 .and_then(|()| { 131 data.deny.iter().try_fold((), |(), lint| { 132 writeln!( 133 stdout, 134 "{} = {{ level = \"allow\", priority = -1 }}", 135 as_str(lint) 136 ) 137 }) 138 }) 139 } else { 140 // Since `"warnings"` is both a lint and lint group, `data.groups` 141 // does not contain it; instead `data.warn` is the set of all 142 // `warn`-by-default lints sans the lint `"warnings"`. 143 // We write groups first, so we add an empty `"warnings"` group. 144 data.groups.push(LintGroup { 145 name: WARNINGS.as_bytes(), 146 lints: HashSet::new(), 147 }); 148 data.groups.sort_unstable(); 149 data.groups 150 .iter() 151 .try_fold((), |(), group| { 152 writeln!( 153 stdout, 154 "{} = {{ level = \"deny\", priority = -1 }}", 155 as_str(group.name) 156 ) 157 }) 158 .and_then(|()| { 159 data.allow.iter().try_fold((), |(), lint| { 160 if data.groups.iter().any(|group| { 161 group 162 .lints 163 .iter() 164 .any(|group_lint| group_lint == lint) 165 }) { 166 Ok(()) 167 } else { 168 writeln!( 169 stdout, 170 "{} = {{ level = \"deny\", priority = -1 }}", 171 as_str(lint) 172 ) 173 } 174 }) 175 }) 176 } 177 .map_err(into_exit_code) 178 }) 179 .map_or_else(convert::identity, |()| ExitCode::SUCCESS) 180 }) 181 } 182 Cmd::Help => writeln!(io::stdout().lock(), "{HELP_MSG}") 183 .map_or_else(into_exit_code, |()| ExitCode::SUCCESS), 184 Cmd::Version => writeln!(io::stdout().lock(), "{VERSION}") 185 .map_or_else(into_exit_code, |()| ExitCode::SUCCESS), 186 }) 187 }) 188 }