You've already forked lint-policy
231 lines
6.5 KiB
Rust
231 lines
6.5 KiB
Rust
//! 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)
|
|
.replace('-', "_")
|
|
}
|