You've already forked lint-policy
Initial commit
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user