Initial commit

This commit is contained in:
2026-07-15 00:17:48 -06:00
commit 23ee571b26
43 changed files with 15941 additions and 0 deletions
+36
View File
@@ -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
+40
View File
@@ -0,0 +1,40 @@
# lint-policy-cli
[![crates.io](https://img.shields.io/crates/v/lint-policy-cli.svg)](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.
+39
View File
@@ -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
}
}
+105
View File
@@ -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
}
+40
View File
@@ -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),
}
}
}
+122
View File
@@ -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(())
}
+57
View File
@@ -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),
}
}
}
+41
View File
@@ -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()
}
+33
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
# lint-policy
[![crates.io](https://img.shields.io/crates/v/lint-policy.svg)](https://crates.io/crates/lint-policy)
Model used by lint-policy-cli
+214
View File
@@ -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);
}
}
}
+90
View File
@@ -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);
}
}
+19
View File
@@ -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 _;
+72
View File
@@ -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(())
}
}
}
+24
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
# private-crate
[![crates.io](https://img.shields.io/crates/v/private-crate.svg)](https://crates.io/crates/private-crate)
Unpublished crate
+5
View File
@@ -0,0 +1,5 @@
//! Sample Private Crate.
#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
// #[cfg_attr(docsrs, doc(cfg(feature = "std")))]