You've already forked lint-policy
Initial commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "lint-policy-cli"
|
||||
version = "0.0.1"
|
||||
description = "Track rust lints"
|
||||
repository = "https://git.skrundz.dev/codegen/lints"
|
||||
keywords = ["cargo", "lint"]
|
||||
categories = ["command-line-utilities"]
|
||||
include = ["/src"]
|
||||
publish = true
|
||||
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "lint-policy"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
lint-policy = { workspace = true, features = ["alloc", "serde"] }
|
||||
|
||||
anyhow = { workspace = true, features = ["std"] }
|
||||
clap = { workspace = true, features = ["color", "derive", "help", "std", "suggestions", "usage"] }
|
||||
itertools = { workspace = true }
|
||||
serde = { workspace = true, features = ["alloc", "derive"] }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true, features = ["display", "parse", "serde"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,40 @@
|
||||
# lint-policy-cli
|
||||
|
||||
[](https://crates.io/crates/lint-policy-cli)
|
||||
|
||||
Generate lints for Cargo.toml
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
cargo install lint-policy-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`lint-policy` accepts one or two inputs. Each input may be either:
|
||||
|
||||
- a Rust toolchain (for example `+1.85`)
|
||||
- a TOML file
|
||||
|
||||
With one input, it prints the generated lint policy:
|
||||
|
||||
```sh
|
||||
lint-policy +1.85
|
||||
```
|
||||
|
||||
With two inputs, it prints the differences between the two policies:
|
||||
|
||||
```sh
|
||||
lint-policy +1.86 +1.85
|
||||
lint-policy +1.86 ./1.85.toml
|
||||
lint-policy ./1.86.toml +1.85
|
||||
```
|
||||
|
||||
By default, the diff compares both the presence of lints and their configured levels. To compare only the set of lint names, ignoring changes to lint levels, pass `--ignore-level`:
|
||||
|
||||
```sh
|
||||
lint-policy --ignore-level +1.86 +1.85
|
||||
```
|
||||
|
||||
When `--ignore-level` is enabled, only added and removed lints are reported. Lints that exist in both inputs but have different levels are treated as unchanged.
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Command line argument parsing.
|
||||
|
||||
use lint_policy::cargo;
|
||||
|
||||
use crate::group::LintGroups;
|
||||
use crate::source::Source;
|
||||
|
||||
/// Command line argument parser struct.
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub(crate) struct Args {
|
||||
/// New lint source, or only source when generating.
|
||||
#[arg(value_name = "NEW")]
|
||||
new: Source,
|
||||
/// Old lint source when generating a diff.
|
||||
#[arg(value_name = "OLD")]
|
||||
old: Option<Source>,
|
||||
|
||||
/// Only diff new and missing lints.
|
||||
#[arg(long)]
|
||||
ignore_level: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Reads the lints and groups of the new source.
|
||||
pub(crate) fn read_new_source(&self) -> (cargo::Lints, LintGroups) {
|
||||
(self.new.read_lints(), self.new.read_groups())
|
||||
}
|
||||
|
||||
/// Reads the lints and groups of the old source.
|
||||
pub(crate) fn read_old_source(&self) -> Option<(cargo::Lints, LintGroups)> {
|
||||
self.old.as_ref().map(|s| (s.read_lints(), s.read_groups()))
|
||||
}
|
||||
|
||||
/// Returns true if the level should be ignored while diffing.
|
||||
pub(crate) const fn ignore_level(&self) -> bool {
|
||||
self.ignore_level
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Diffing lint.
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
extern crate alloc;
|
||||
use alloc::collections::BTreeSet;
|
||||
|
||||
use lint_policy::cargo::{LintSetting, LintTable};
|
||||
|
||||
use crate::group::GroupTable;
|
||||
|
||||
/// Represents a difference between two lint tables.
|
||||
pub(crate) enum Diff<'a> {
|
||||
/// A lint was added in the new table.
|
||||
Added {
|
||||
/// The name of the lint.
|
||||
name: &'a str,
|
||||
/// The new lint settings.
|
||||
new: &'a LintSetting,
|
||||
},
|
||||
/// A lint was removed from the new table.
|
||||
Removed {
|
||||
/// The name of the lint.
|
||||
name: &'a str,
|
||||
/// The old lint settings.
|
||||
old: &'a LintSetting,
|
||||
},
|
||||
/// A lint's settings were changed in the new table.
|
||||
Modified {
|
||||
/// The name of the lint.
|
||||
name: &'a str,
|
||||
/// The old lint settings.
|
||||
old: &'a LintSetting,
|
||||
/// The new lint settings.
|
||||
new: &'a LintSetting,
|
||||
},
|
||||
}
|
||||
|
||||
/// Compute the difference between lints tables.
|
||||
pub(crate) fn diff<'a>(
|
||||
old_lints: &'a LintTable,
|
||||
new_lints: &'a LintTable,
|
||||
new_groups: &'a GroupTable,
|
||||
) -> Vec<Diff<'a>> {
|
||||
// Filter groups
|
||||
let mut ignored_old = BTreeSet::new();
|
||||
let mut ignored_new = BTreeSet::new();
|
||||
|
||||
for (group, lints) in new_groups {
|
||||
if old_lints.contains_key(group) {
|
||||
_ = ignored_old.insert(group);
|
||||
_ = ignored_new.insert(group);
|
||||
ignored_new.extend(lints.iter());
|
||||
}
|
||||
}
|
||||
|
||||
// Diff remaining lints
|
||||
let mut diffs = Vec::new();
|
||||
|
||||
let mut old = old_lints
|
||||
.iter()
|
||||
.filter(|(name, _)| !ignored_old.contains(name))
|
||||
.peekable();
|
||||
let mut new = new_lints
|
||||
.iter()
|
||||
.filter(|(name, _)| !ignored_new.contains(name))
|
||||
.peekable();
|
||||
|
||||
loop {
|
||||
match (old.peek(), new.peek()) {
|
||||
(Some((ok, ov)), Some((nk, nv))) => match ok.cmp(nk) {
|
||||
Ordering::Less => {
|
||||
diffs.push(Diff::Removed { name: ok, old: ov });
|
||||
_ = old.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
diffs.push(Diff::Added { name: nk, new: nv });
|
||||
_ = new.next();
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if ov.level != nv.level {
|
||||
diffs.push(Diff::Modified {
|
||||
name: ok,
|
||||
old: ov,
|
||||
new: nv,
|
||||
});
|
||||
}
|
||||
_ = old.next();
|
||||
_ = new.next();
|
||||
}
|
||||
},
|
||||
(Some((ok, ov)), None) => {
|
||||
diffs.push(Diff::Removed { name: ok, old: ov });
|
||||
_ = old.next();
|
||||
}
|
||||
(None, Some((nk, nv))) => {
|
||||
diffs.push(Diff::Added { name: nk, new: nv });
|
||||
_ = new.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
diffs
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Lint groups.
|
||||
|
||||
extern crate alloc;
|
||||
use alloc::collections::BTreeMap;
|
||||
|
||||
use lint_policy::Linter;
|
||||
|
||||
/// A group table.
|
||||
pub(crate) type GroupTable = BTreeMap<String, Vec<String>>;
|
||||
|
||||
/// A struct containing tables for each linter.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct LintGroups {
|
||||
/// Table of clippy groups.
|
||||
pub clippy: Option<GroupTable>,
|
||||
/// Table of rust groups.
|
||||
pub rust: Option<GroupTable>,
|
||||
/// Table of rustdoc groups.
|
||||
pub rustdoc: Option<GroupTable>,
|
||||
}
|
||||
|
||||
impl LintGroups {
|
||||
/// Get a reference to the group table corresponding to the given [Linter].
|
||||
pub(crate) const fn get_groups_for(&self, linter: Linter) -> Option<&GroupTable> {
|
||||
match linter {
|
||||
Linter::Clippy => self.clippy.as_ref(),
|
||||
Linter::Rust => self.rust.as_ref(),
|
||||
Linter::Rustdoc => self.rustdoc.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the group table corresponding to the given [Linter].
|
||||
pub(crate) fn set_groups_for(&mut self, linter: Linter, lints: GroupTable) {
|
||||
match linter {
|
||||
Linter::Clippy => self.clippy = Some(lints),
|
||||
Linter::Rust => self.rust = Some(lints),
|
||||
Linter::Rustdoc => self.rustdoc = Some(lints),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Generate lints for Cargo.toml.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser as _;
|
||||
use lint_policy::Linter;
|
||||
use lint_policy::cargo::{LintTable, Manifest};
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
mod cli;
|
||||
mod diff;
|
||||
mod group;
|
||||
mod source;
|
||||
|
||||
use cli::Args;
|
||||
use diff::{Diff, diff};
|
||||
|
||||
use crate::group::GroupTable;
|
||||
|
||||
/// An empty lint table for unwrapping table references.
|
||||
static EMPTY_LINTS: LintTable = LintTable::new();
|
||||
/// An empty group table for unwrapping table references.
|
||||
static EMPTY_GROUPS: GroupTable = GroupTable::new();
|
||||
|
||||
#[cfg_attr(test, expect(clippy::missing_errors_doc, reason = "main function"))]
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let new_lints = args.read_new_source();
|
||||
let old_lints = args.read_old_source();
|
||||
|
||||
match old_lints {
|
||||
None => {
|
||||
for linter in Linter::iter() {
|
||||
// section
|
||||
#[expect(clippy::print_stdout, reason = "printing out actual output")]
|
||||
{
|
||||
let mut manifest = Manifest::default();
|
||||
manifest
|
||||
.workspace
|
||||
.lints
|
||||
.set_lints_for(linter, LintTable::default());
|
||||
print!("{}", toml::to_string(&manifest)?);
|
||||
}
|
||||
|
||||
// lints
|
||||
if let Some(lints) = new_lints.0.get_lints_for(linter) {
|
||||
let mut value = String::new();
|
||||
#[expect(clippy::print_stdout, reason = "printing out actual output")]
|
||||
for lint in lints {
|
||||
_ = serde::Serialize::serialize(
|
||||
&lint.1,
|
||||
toml::ser::ValueSerializer::new(&mut value),
|
||||
)?;
|
||||
println!("{} = {}", lint.0, value);
|
||||
value.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(old_lints) => {
|
||||
for linter in Linter::iter() {
|
||||
// section
|
||||
#[expect(clippy::print_stdout, reason = "printing out actual output")]
|
||||
{
|
||||
let mut manifest = Manifest::default();
|
||||
manifest
|
||||
.workspace
|
||||
.lints
|
||||
.set_lints_for(linter, LintTable::default());
|
||||
print!("{}", toml::to_string(&manifest)?);
|
||||
}
|
||||
|
||||
// diff
|
||||
let mut value = String::new();
|
||||
let old_lints = old_lints.0.get_lints_for(linter).unwrap_or(&EMPTY_LINTS);
|
||||
let (new_lints, new_groups) = (
|
||||
new_lints.0.get_lints_for(linter).unwrap_or(&EMPTY_LINTS),
|
||||
new_lints.1.get_groups_for(linter).unwrap_or(&EMPTY_GROUPS),
|
||||
);
|
||||
for diff in diff(old_lints, new_lints, new_groups) {
|
||||
#[expect(clippy::print_stdout, reason = "printing out actual output")]
|
||||
match diff {
|
||||
Diff::Added { name, new } => {
|
||||
_ = serde::Serialize::serialize(
|
||||
&new,
|
||||
toml::ser::ValueSerializer::new(&mut value),
|
||||
)?;
|
||||
println!("+ {name} = {value}");
|
||||
}
|
||||
Diff::Removed { name, old } => {
|
||||
_ = serde::Serialize::serialize(
|
||||
&old,
|
||||
toml::ser::ValueSerializer::new(&mut value),
|
||||
)?;
|
||||
println!("- {name} = {value}");
|
||||
}
|
||||
Diff::Modified { name, old, new } => {
|
||||
if !args.ignore_level() {
|
||||
_ = serde::Serialize::serialize(
|
||||
&old,
|
||||
toml::ser::ValueSerializer::new(&mut value),
|
||||
)?;
|
||||
print!("~ {name} = {value}");
|
||||
value.clear();
|
||||
_ = serde::Serialize::serialize(
|
||||
&new,
|
||||
toml::ser::ValueSerializer::new(&mut value),
|
||||
)?;
|
||||
println!(" => {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
value.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Lint sources.
|
||||
|
||||
use core::convert::Infallible;
|
||||
use core::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lint_policy::cargo;
|
||||
|
||||
use crate::group::LintGroups;
|
||||
|
||||
mod toml;
|
||||
mod toolchain;
|
||||
|
||||
/// A lint source.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Source {
|
||||
/// A toolchain lint source specified by version.
|
||||
Toolchain(String),
|
||||
/// A TOML lint source specified by path.
|
||||
Toml(PathBuf),
|
||||
}
|
||||
|
||||
impl FromStr for Source {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.starts_with('+') {
|
||||
Ok(Self::Toolchain(s.to_owned()))
|
||||
} else {
|
||||
Ok(Self::Toml(PathBuf::from(s.to_owned())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// Query the source for the list of included lints, panicking on any error.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the underlying source implemenation encounters and error.
|
||||
pub(crate) fn read_lints(&self) -> cargo::Lints {
|
||||
match self {
|
||||
Self::Toolchain(toolchain) => toolchain::read_lints(toolchain),
|
||||
Self::Toml(filepath) => toml::read_lints(filepath),
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the source for the list of included lint groups, panicking on any error.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the underlying source implemenation encounters and error.
|
||||
pub(crate) fn read_groups(&self) -> LintGroups {
|
||||
match self {
|
||||
Self::Toolchain(toolchain) => toolchain::read_groups(toolchain),
|
||||
Self::Toml(filepath) => toml::read_groups(filepath),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! TOML lint source.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use lint_policy::cargo;
|
||||
|
||||
use crate::source::LintGroups;
|
||||
|
||||
/// Query a TOML file for the list of included lints, panicking on any error.
|
||||
///
|
||||
/// # Panics
|
||||
/// If an error occurs while reading the file or parsing the content.
|
||||
pub(crate) fn read_lints(filepath: &Path) -> cargo::Lints {
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "errors here are not deisgned to be recoverable"
|
||||
)]
|
||||
private_read_lints(filepath)
|
||||
.with_context(|| format!("reading lints from {}", filepath.display()))
|
||||
.expect("toml source error")
|
||||
}
|
||||
|
||||
/// Query a TOML file for the list of included lints.
|
||||
///
|
||||
/// # Errors
|
||||
/// IO can fail while reading the file, and TOML parsing can fail.
|
||||
fn private_read_lints(filepath: &Path) -> anyhow::Result<cargo::Lints> {
|
||||
let contents = fs::read_to_string(filepath)?;
|
||||
let manifest: cargo::Manifest = toml::from_str(&contents)?;
|
||||
Ok(manifest.workspace.lints)
|
||||
}
|
||||
|
||||
/// Query a TOML file for the list of included groups.
|
||||
///
|
||||
/// There is currently no differentiator between lints and groups so this function
|
||||
/// just returns [Default].
|
||||
pub(crate) fn read_groups(_: &Path) -> LintGroups {
|
||||
LintGroups::default()
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Toolchain lint source.
|
||||
|
||||
use core::str::{FromStr as _, Utf8Error};
|
||||
use std::io;
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use itertools::Itertools as _;
|
||||
use lint_policy::cargo::{LintSetting, LintTable, Lints};
|
||||
use lint_policy::{Level, Linter};
|
||||
use strum::{IntoEnumIterator as _, ParseError};
|
||||
|
||||
use crate::group::GroupTable;
|
||||
use crate::source::LintGroups;
|
||||
|
||||
/// Query the toolchain version for the list of supported lints, panicking on any error.
|
||||
///
|
||||
/// # Panics
|
||||
/// If an error occurs while running the toolchain or parsing the output.
|
||||
pub(crate) fn read_lints(toolchain: &str) -> Lints {
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "errors here are not deisgned to be recoverable"
|
||||
)]
|
||||
private_read_lints(toolchain)
|
||||
.with_context(|| format!("gathering lints from {toolchain:?}"))
|
||||
.expect("toolchain source error")
|
||||
}
|
||||
|
||||
/// Query the toolchain version for the list of supported lints, panicking on any error.
|
||||
///
|
||||
/// # Panics
|
||||
/// If an error occurs while running the toolchain or parsing the output.
|
||||
pub(crate) fn read_groups(toolchain: &str) -> LintGroups {
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "errors here are not deisgned to be recoverable"
|
||||
)]
|
||||
private_read_groups(toolchain)
|
||||
.with_context(|| format!("gathering lint groups from {toolchain:?}"))
|
||||
.expect("toolchain source error")
|
||||
}
|
||||
|
||||
/// Possible errors that can occur while querying a toolchain version.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ToolchainError {
|
||||
/// Executing a [Command] failed.
|
||||
#[error("failed to execute command: {0}")]
|
||||
Command(#[from] io::Error),
|
||||
/// The toolchain did not exit successfully.
|
||||
#[error("command exited with code {0}")]
|
||||
Status(ExitStatus),
|
||||
/// The output from the toolchain was not UTF-8.
|
||||
#[error("output is not utf8: {0}")]
|
||||
NotUtf8(#[from] Utf8Error),
|
||||
}
|
||||
|
||||
/// Possible errors that can occur while parsing lines of the toolchain output.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum LineError {
|
||||
/// The output does not match the expected format.
|
||||
#[error("output is malformed: {0}")]
|
||||
Malformed(String),
|
||||
/// The lint level is not recognized.
|
||||
#[error("invalid level: {0}")]
|
||||
InvalidLevel(String),
|
||||
}
|
||||
|
||||
/// Query the toolchain version for the list of supported lints.
|
||||
///
|
||||
/// # Errors
|
||||
/// Either running the [Command] or parsing the output could fail.
|
||||
fn private_read_lints(toolchain: &str) -> anyhow::Result<Lints> {
|
||||
let mut lints = Lints::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for linter in Linter::iter() {
|
||||
let (l, e) = read_linter_lints(linter, toolchain)?;
|
||||
lints.set_lints_for(linter, l);
|
||||
errors.push(e);
|
||||
}
|
||||
|
||||
if errors.iter().any(|e| !e.is_empty()) {
|
||||
for errors in &errors {
|
||||
#[expect(clippy::print_stderr, reason = "printing out actual error")]
|
||||
for error in errors {
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
anyhow::bail!("encountered errors for toolchain");
|
||||
}
|
||||
|
||||
Ok(lints)
|
||||
}
|
||||
|
||||
/// Run the linter and parse its output.
|
||||
///
|
||||
/// # Errors
|
||||
/// Either running the [Command] or parsing the output could fail.
|
||||
fn read_linter_lints(
|
||||
linter: Linter,
|
||||
toolchain: &str,
|
||||
) -> Result<(LintTable, Vec<LineError>), ToolchainError> {
|
||||
let mut command = match linter {
|
||||
Linter::Clippy => Command::new("clippy-driver"),
|
||||
Linter::Rust => Command::new("rustc"),
|
||||
Linter::Rustdoc => Command::new("rustdoc"),
|
||||
};
|
||||
let output = command.arg(toolchain).args(["-W", "help"]).output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(ToolchainError::Status(output.status));
|
||||
}
|
||||
|
||||
let stdout = core::str::from_utf8(&output.stdout)?;
|
||||
|
||||
let lint_begin_line = match linter {
|
||||
Linter::Rust => "Lint checks provided by rustc:",
|
||||
Linter::Clippy | Linter::Rustdoc => "Lint checks loaded by this crate:",
|
||||
};
|
||||
|
||||
let lint_lines = stdout
|
||||
.lines()
|
||||
.skip_while(|&line| line != lint_begin_line)
|
||||
.skip(4)
|
||||
.take_while(|line| !line.is_empty());
|
||||
|
||||
let lints = lint_lines.map(|line| {
|
||||
let (name, level) = line
|
||||
.trim()
|
||||
.split_once(' ')
|
||||
.ok_or_else(|| LineError::Malformed(line.to_owned()))
|
||||
.and_then(|(name, rest)| {
|
||||
rest.trim()
|
||||
.split_once(' ')
|
||||
.map(|(level, _)| (name, level))
|
||||
.ok_or_else(|| LineError::Malformed(line.to_owned()))
|
||||
})?;
|
||||
Level::from_str(level)
|
||||
.map(|level| (strip_lint_name(linter, name), LintSetting::level(level)))
|
||||
.map_err(|ParseError::VariantNotFound| LineError::InvalidLevel(level.to_owned()))
|
||||
});
|
||||
|
||||
Ok(lints.partition_result())
|
||||
}
|
||||
|
||||
/// Query the toolchain version for the list of supported lints, panicking on any error.
|
||||
///
|
||||
/// # Errors
|
||||
/// Either running the [Command] or parsing the output could fail.
|
||||
fn private_read_groups(toolchain: &str) -> anyhow::Result<LintGroups> {
|
||||
let mut groups = LintGroups::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for linter in Linter::iter() {
|
||||
let (g, e) = read_linter_groups(linter, toolchain)?;
|
||||
groups.set_groups_for(linter, g);
|
||||
errors.push(e);
|
||||
}
|
||||
|
||||
if errors.iter().any(|e| !e.is_empty()) {
|
||||
for errors in &errors {
|
||||
#[expect(clippy::print_stderr, reason = "printing out actual error")]
|
||||
for error in errors {
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
anyhow::bail!("encountered errors for toolchain");
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// Run the linter and parse its output.
|
||||
///
|
||||
/// # Errors
|
||||
/// Either running the [Command] or parsing the output could fail.
|
||||
fn read_linter_groups(
|
||||
linter: Linter,
|
||||
toolchain: &str,
|
||||
) -> Result<(GroupTable, Vec<LineError>), ToolchainError> {
|
||||
let mut command = match linter {
|
||||
Linter::Clippy => Command::new("clippy-driver"),
|
||||
Linter::Rust => Command::new("rustc"),
|
||||
Linter::Rustdoc => Command::new("rustdoc"),
|
||||
};
|
||||
let output = command.arg(toolchain).args(["-W", "help"]).output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(ToolchainError::Status(output.status));
|
||||
}
|
||||
|
||||
let stdout = core::str::from_utf8(&output.stdout)?;
|
||||
|
||||
let lint_begin_line = match linter {
|
||||
Linter::Rust => "Lint groups provided by rustc:",
|
||||
Linter::Clippy | Linter::Rustdoc => "Lint groups loaded by this crate:",
|
||||
};
|
||||
|
||||
let lint_lines = stdout
|
||||
.lines()
|
||||
.skip_while(|&line| line != lint_begin_line)
|
||||
.skip(4)
|
||||
.take_while(|line| !line.is_empty());
|
||||
|
||||
let lints = lint_lines.map(|line| {
|
||||
line.trim()
|
||||
.split_once(' ')
|
||||
.ok_or_else(|| LineError::Malformed(line.to_owned()))
|
||||
.map(|(name, rest)| {
|
||||
(
|
||||
strip_lint_name(linter, name),
|
||||
rest.split(',')
|
||||
.map(|s| strip_lint_name(linter, s))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
Ok(lints.partition_result())
|
||||
}
|
||||
|
||||
/// Strip the linter's name from the front of the lint name.
|
||||
fn strip_lint_name(linter: Linter, name: &str) -> String {
|
||||
let name = name.trim();
|
||||
name.strip_prefix(linter.as_ref())
|
||||
.and_then(|name| name.strip_prefix("::"))
|
||||
.unwrap_or(name)
|
||||
.to_owned()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "lint-policy"
|
||||
version = "0.0.1"
|
||||
description = "Model used by cargo-lint-policy"
|
||||
repository = "https://git.skrundz.dev/codegen/lints"
|
||||
keywords = ["no-std"]
|
||||
categories = ["development-tools::cargo-plugins"]
|
||||
include = ["/src"]
|
||||
publish = true
|
||||
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
toml = { workspace = true, features = ["display", "parse", "serde"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
alloc = ["serde?/alloc"]
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
# lint-policy
|
||||
|
||||
[](https://crates.io/crates/lint-policy)
|
||||
|
||||
Model used by lint-policy-cli
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Types for serializing and deserializing a workspace TOML file with a lint table.
|
||||
|
||||
extern crate alloc;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::{Level, Linter};
|
||||
|
||||
/// A lint table.
|
||||
pub type LintTable = BTreeMap<String, LintSetting>;
|
||||
|
||||
/// Top level TOML manifest table.
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub struct Manifest {
|
||||
/// The workspace table.
|
||||
pub workspace: Workspace,
|
||||
}
|
||||
|
||||
/// The TOML workspace table.
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub struct Workspace {
|
||||
/// The lints table.
|
||||
pub lints: Lints,
|
||||
}
|
||||
|
||||
/// The TOML lints table.
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[expect(clippy::exhaustive_structs, reason = "Linters are added infrequently")]
|
||||
pub struct Lints {
|
||||
/// Table of clippy lints.
|
||||
pub clippy: Option<LintTable>,
|
||||
/// Table of rust lints.
|
||||
pub rust: Option<LintTable>,
|
||||
/// Table of rustdoc lints.
|
||||
pub rustdoc: Option<LintTable>,
|
||||
}
|
||||
|
||||
impl Lints {
|
||||
/// Get a reference to the lint table corresponding to the given [Linter].
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn get_lints_for(&self, linter: Linter) -> Option<&LintTable> {
|
||||
match linter {
|
||||
Linter::Clippy => self.clippy.as_ref(),
|
||||
Linter::Rust => self.rust.as_ref(),
|
||||
Linter::Rustdoc => self.rustdoc.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lint table corresponding to the given [Linter].
|
||||
#[inline]
|
||||
pub fn set_lints_for(&mut self, linter: Linter, lints: LintTable) {
|
||||
match linter {
|
||||
Linter::Clippy => self.clippy = Some(lints),
|
||||
Linter::Rust => self.rust = Some(lints),
|
||||
Linter::Rustdoc => self.rustdoc = Some(lints),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings applied to a lint, contains a [Level] and an optional priority.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct LintSetting {
|
||||
/// The lint [Level].
|
||||
pub level: Level,
|
||||
/// The lint priority.
|
||||
pub priority: Option<i8>,
|
||||
}
|
||||
|
||||
impl LintSetting {
|
||||
/// Construct a new [`LintSetting`] from a [Level].
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn level(level: Level) -> Self {
|
||||
Self {
|
||||
level,
|
||||
priority: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [`serde::Serialize`] and [`serde::Deserialize`] for [`LintSetting`].
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impl {
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use super::{Level, LintSetting};
|
||||
|
||||
/// An enum to facilitate serializing and deserializing of different formats.
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum LintSettingRepr {
|
||||
/// The lint setting is ser/de as a plain string.
|
||||
Level(Level),
|
||||
/// The lint setting is ser/de as a table.
|
||||
Table {
|
||||
/// The lint level.
|
||||
level: Level,
|
||||
/// The lint priority.
|
||||
priority: i8,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serialize for LintSetting {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let value = self
|
||||
.priority
|
||||
.map_or(LintSettingRepr::Level(self.level), |priority| {
|
||||
LintSettingRepr::Table {
|
||||
level: self.level,
|
||||
priority,
|
||||
}
|
||||
});
|
||||
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LintSetting {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match LintSettingRepr::deserialize(deserializer)? {
|
||||
LintSettingRepr::Level(level) => Ok(Self {
|
||||
level,
|
||||
priority: None,
|
||||
}),
|
||||
LintSettingRepr::Table { level, priority } => Ok(Self {
|
||||
level,
|
||||
priority: Some(priority),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
*place = Self::deserialize(deserializer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use crate::cargo::{LintSetting, LintTable};
|
||||
use crate::{Level, Linter};
|
||||
|
||||
use super::Manifest;
|
||||
#[cfg(feature = "serde")]
|
||||
use super::{Lints, Workspace};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "serde")]
|
||||
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||
fn serde() {
|
||||
let manifest = Manifest {
|
||||
workspace: Workspace {
|
||||
lints: Lints {
|
||||
clippy: Some(LintTable::default()),
|
||||
rust: Some(LintTable::default()),
|
||||
rustdoc: Some(LintTable::default()),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
"[workspace.lints.clippy]\n\n[workspace.lints.rust]\n\n[workspace.lints.rustdoc]\n",
|
||||
toml::to_string(&manifest).expect("serialize manifest"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||
fn getset() {
|
||||
for linter in Linter::iter() {
|
||||
let mut manifest = Manifest::default();
|
||||
|
||||
manifest
|
||||
.workspace
|
||||
.lints
|
||||
.set_lints_for(linter, LintTable::default());
|
||||
|
||||
for l in Linter::iter() {
|
||||
let lints = manifest.workspace.lints.get_lints_for(l);
|
||||
assert_eq!(l == linter, lints.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||
fn lintsetting_level() {
|
||||
for level in Level::iter() {
|
||||
assert_eq!(level, LintSetting::level(level).level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! This module holds the definition of available lint levels.
|
||||
|
||||
/// Lint levels that are usable in Cargo.toml.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
strum::AsRefStr,
|
||||
strum::IntoStaticStr,
|
||||
strum::EnumIter,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[expect(clippy::exhaustive_enums, reason = "Levels are not expected to change")]
|
||||
pub enum Level {
|
||||
/// Do nothing.
|
||||
Allow,
|
||||
/// Produce a warning if you violate the lint.
|
||||
Warn,
|
||||
/// Produce an error if you violate the lint.
|
||||
Deny,
|
||||
/// Same as `Deny` but can not be overridden.
|
||||
Forbid,
|
||||
}
|
||||
|
||||
/// Implementation of [`serde::Serialize`] and [`serde::Deserialize`] for [Level].
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impl {
|
||||
use core::str::FromStr as _;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use strum::{ParseError, VariantNames as _};
|
||||
|
||||
use super::Level;
|
||||
|
||||
impl Serialize for Level {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Level {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = <&str>::deserialize(deserializer)?;
|
||||
|
||||
Self::from_str(value).map_err(|ParseError::VariantNotFound| {
|
||||
serde::de::Error::unknown_variant(value, Self::VARIANTS)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
*place = Self::deserialize(deserializer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Level;
|
||||
|
||||
/// Test that ordering goes in the right direction.
|
||||
#[test]
|
||||
#[expect(clippy::missing_panics_doc, reason = "unit test")]
|
||||
fn ordering() {
|
||||
assert!(Level::Allow < Level::Warn);
|
||||
assert!(Level::Warn < Level::Deny);
|
||||
assert!(Level::Deny < Level::Forbid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Model used by cargo-lint-policy.
|
||||
|
||||
#![no_std]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod level;
|
||||
mod linter;
|
||||
|
||||
pub use level::Level;
|
||||
pub use linter::Linter;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
|
||||
pub mod cargo;
|
||||
|
||||
// dev-dependencies cannot be optional and `toml` is only used when the `alloc`
|
||||
// and `serde` features are active
|
||||
#[cfg(test)]
|
||||
use toml as _;
|
||||
@@ -0,0 +1,72 @@
|
||||
//! This module holds the definition of available lint providers.
|
||||
|
||||
/// Linters that are configurable in Cargo.toml.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
strum::AsRefStr,
|
||||
strum::IntoStaticStr,
|
||||
strum::EnumIter,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[expect(clippy::exhaustive_enums, reason = "Linters are added infrequently")]
|
||||
pub enum Linter {
|
||||
/// clippy.
|
||||
Clippy,
|
||||
/// rust.
|
||||
Rust,
|
||||
/// rustdoc.
|
||||
Rustdoc,
|
||||
}
|
||||
|
||||
/// Implementation of [`serde::Serialize`] and [`serde::Deserialize`] for [Linter].
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impl {
|
||||
use core::str::FromStr as _;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use strum::{ParseError, VariantNames as _};
|
||||
|
||||
use super::Linter;
|
||||
|
||||
impl Serialize for Linter {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Linter {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = <&str>::deserialize(deserializer)?;
|
||||
|
||||
Self::from_str(value).map_err(|ParseError::VariantNotFound| {
|
||||
serde::de::Error::unknown_variant(value, Self::VARIANTS)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
*place = Self::deserialize(deserializer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "private-crate"
|
||||
version = "0.0.0"
|
||||
# description = ""
|
||||
# repository = ""
|
||||
# keywords = []
|
||||
# categories = []
|
||||
# include = ["/src"]
|
||||
# publish = true
|
||||
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
# crate = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
# private-crate
|
||||
|
||||
[](https://crates.io/crates/private-crate)
|
||||
|
||||
Unpublished crate
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Sample Private Crate.
|
||||
|
||||
#![no_std]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
// #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
|
||||
Reference in New Issue
Block a user