//! 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 { 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), 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 { 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), 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::>(), ) }) }); 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('-', "_") }