lints 0.1.0

Writes [lints.rust] to stdout such that all lints are denied or allowed.
//! Consult [`README.md`](https://crates.io/crates/lints/).
/// CLI-argument parsing.
mod args;
/// `rustc` execution.
mod cmd;
/// Parsing of `rustc -Whelp` output.
mod parse;
use args::{Cmd, E as ArgsErr};
use cmd::E as CmdErr;
use core::{convert, str};
use parse::{Data, E as ParseErr, LintGroup, WARNINGS};
#[cfg(target_os = "openbsd")]
use priv_sep::{Permissions, Promises};
use std::{
    collections::HashSet,
    env,
    io::{self, Error, Write as _},
    process::ExitCode,
};
/// Writes `err` to `stderr`.
#[expect(
    clippy::needless_pass_by_value,
    reason = "Error is small and makes the signature more pleasant when dealing with Result::map_or_else"
)]
fn into_exit_code(err: Error) -> ExitCode {
    writeln!(io::stderr().lock(), "{err}").map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE)
}
/// Help message.
const HELP_MSG: &str =
    "Denies or allows all lints based on rustc -Whelp and what command is passed

Usage: lints [COMMAND] [OPTIONS]

Commands:
    allow      Allows all lints
    deny       Denies all lints
    help       This message
    version    Prints version info

Options:
  --all                      Writes all individual lints even if they're already part of a lint group
  --allow-undefined-lints    Lint groups that contain undefined lints are allowed

When nothing or deny is passed, all lint groups and allow-by-default lints that are not part of a group
are set to deny.
When allow is passed, \"warnings\" and all deny-by-default lints are set to allow.
stdout is written to in the format of the TOML table, lints.rust, which can be added to Cargo.toml.";
/// Version of `lints`.
const VERSION: &str = concat!("lints ", env!("CARGO_PKG_VERSION"));
/// No-op.
#[expect(clippy::unnecessary_wraps, reason = "unify with OpenBSD")]
#[cfg(not(target_os = "openbsd"))]
const fn priv_sep<Never>() -> Result<(), Never> {
    Ok(())
}
/// `pledge(2)`s `c"exec proc rpath stdio unveil"` before `unveil(2)`ing no file-system access.
#[cfg(target_os = "openbsd")]
fn priv_sep() -> Result<(), Error> {
    Promises::pledge_raw(c"exec proc rpath stdio unveil")
        .and_then(|()| Permissions::unveil_raw(c"/", c""))
        .map_err(Error::from)
}
/// Transforms `ascii` into a `str` based on the assumption that it's already been verified to be valid
/// UTF-8.
#[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
fn as_str(ascii: &[u8]) -> &str {
    str::from_utf8(ascii).unwrap_or_else(|_e| {
        unreachable!("names of lints and lint groups are verified to be a subset of ASCII")
    })
}
/// Entry to application.
#[expect(clippy::too_many_lines, reason = "101 lines is OK")]
fn main() -> ExitCode {
    priv_sep().map_or_else(into_exit_code, |()| {
        Cmd::try_from_args(env::args_os()).map_or_else(ArgsErr::into_exit_code, |cmd| match cmd {
            Cmd::Allow(all, allow_undefined) | Cmd::Deny(all, allow_undefined) => {
                cmd::execute_rustc().map_or_else(CmdErr::into_exit_code, |output| {
                    Data::new(&output, allow_undefined)
                        .map_err(ParseErr::into_exit_code)
                        .and_then(|mut data| {
                            let mut stdout = io::stdout().lock();
                            if all {
                                let level = if matches!(cmd, Cmd::Allow(_, _)) {
                                    "allow"
                                } else {
                                    "deny"
                                };
                                // Since `"warnings"` is both a lint and lint group, `data.groups`
                                // does not contain it; instead `data.warn` is the set of all
                                // `warn`-by-default lints sans the lint `"warnings"`.
                                // We write groups first, so we add an empty `"warnings"` group.
                                data.groups.push(LintGroup {
                                    name: WARNINGS.as_bytes(),
                                    lints: HashSet::new(),
                                });
                                data.groups.sort_unstable();
                                data.groups
                                    .into_iter()
                                    .try_fold((), |(), group| {
                                        writeln!(
                                            stdout,
                                            "{} = {{ level = \"{level}\", priority = -1 }}",
                                            as_str(group.name)
                                        )
                                    })
                                    .and_then(|()| {
                                        // There are likely more `warn`-by-default lints, so
                                        // we use its memory allocation to append the other lints.
                                        data.warn.extend_from_slice(&data.allow);
                                        // Reduce memory when we can.
                                        drop(data.allow);
                                        data.warn.extend_from_slice(&data.deny);
                                        // Reduce memory when we can.
                                        drop(data.deny);
                                        data.warn.sort_unstable();
                                        data.warn.iter().try_fold((), |(), lint| {
                                            writeln!(
                                                stdout,
                                                "{} = {{ level = \"{level}\", priority = -1 }}",
                                                as_str(lint)
                                            )
                                        })
                                    })
                            } else if matches!(cmd, Cmd::Allow(_, _)) {
                                writeln!(
                                    stdout,
                                    "{WARNINGS} = {{ level = \"allow\", priority = -1 }}"
                                )
                                .and_then(|()| {
                                    data.deny.iter().try_fold((), |(), lint| {
                                        writeln!(
                                            stdout,
                                            "{} = {{ level = \"allow\", priority = -1 }}",
                                            as_str(lint)
                                        )
                                    })
                                })
                            } else {
                                // Since `"warnings"` is both a lint and lint group, `data.groups`
                                // does not contain it; instead `data.warn` is the set of all
                                // `warn`-by-default lints sans the lint `"warnings"`.
                                // We write groups first, so we add an empty `"warnings"` group.
                                data.groups.push(LintGroup {
                                    name: WARNINGS.as_bytes(),
                                    lints: HashSet::new(),
                                });
                                data.groups.sort_unstable();
                                data.groups
                                    .iter()
                                    .try_fold((), |(), group| {
                                        writeln!(
                                            stdout,
                                            "{} = {{ level = \"deny\", priority = -1 }}",
                                            as_str(group.name)
                                        )
                                    })
                                    .and_then(|()| {
                                        data.allow.iter().try_fold((), |(), lint| {
                                            if data.groups.iter().any(|group| {
                                                group
                                                    .lints
                                                    .iter()
                                                    .any(|group_lint| group_lint == lint)
                                            }) {
                                                Ok(())
                                            } else {
                                                writeln!(
                                                    stdout,
                                                    "{} = {{ level = \"deny\", priority = -1 }}",
                                                    as_str(lint)
                                                )
                                            }
                                        })
                                    })
                            }
                            .map_err(into_exit_code)
                        })
                        .map_or_else(convert::identity, |()| ExitCode::SUCCESS)
                })
            }
            Cmd::Help => writeln!(io::stdout().lock(), "{HELP_MSG}")
                .map_or_else(into_exit_code, |()| ExitCode::SUCCESS),
            Cmd::Version => writeln!(io::stdout().lock(), "{VERSION}")
                .map_or_else(into_exit_code, |()| ExitCode::SUCCESS),
        })
    })
}