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
+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()
}