Files
lint-policy/crates/lint-policy-cli/src/source/toolchain.rs
T

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('-', "_")
}