| use std::cmp::Reverse; |
| use std::fmt::Display; |
| |
| use cargo_util_schemas::manifest; |
| use cargo_util_terminal::report::Level; |
| |
| use crate::context::WarningHandling; |
| use crate::util::GlobalContext; |
| use crate::workspace::{Feature, Features}; |
| |
| #[derive(Clone, Debug)] |
| pub struct Lint { |
| pub name: &'static str, |
| pub desc: &'static str, |
| pub primary_group: &'static LintGroup, |
| /// The minimum supported Rust version for applying this lint |
| /// |
| /// Note: If the lint is on by default and did not qualify as a hard-warning before the |
| /// linting system, then at earliest an MSRV of 1.78 is required as `[lints.cargo]` was a hard |
| /// error before then. |
| pub msrv: Option<manifest::RustVersion>, |
| pub feature_gate: Option<&'static Feature>, |
| /// This is a markdown formatted string that will be used when generating |
| /// the lint documentation. If docs is `None`, the lint will not be |
| /// documented. |
| pub docs: Option<&'static str>, |
| } |
| |
| impl Lint { |
| pub fn level( |
| &self, |
| pkg_lints: &manifest::TomlToolLints, |
| pkg_rust_version: Option<&manifest::RustVersion>, |
| unstable_features: &Features, |
| gctx: &GlobalContext, |
| ) -> LintLevelProduct { |
| // We should return `Allow` if a lint is behind a feature, but it is |
| // not enabled, that way the lint does not run. |
| if self |
| .feature_gate |
| .is_some_and(|f| !unstable_features.is_enabled(f)) |
| { |
| let level = LintLevel::Allow; |
| let source = LintLevelSource::Default; |
| return LintLevelProduct { level, source }; |
| } |
| |
| if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) { |
| let pkg_rust_version = pkg_rust_version.to_partial(); |
| if !msrv.is_compatible_with(&pkg_rust_version) { |
| let level = LintLevel::Allow; |
| let source = LintLevelSource::Default; |
| return LintLevelProduct { level, source }; |
| } |
| } |
| |
| let lint_level_priority = |
| level_priority(self.name, self.primary_group.default_level, pkg_lints); |
| |
| let group_level_priority = level_priority( |
| self.primary_group.name, |
| self.primary_group.default_level, |
| pkg_lints, |
| ); |
| |
| let default_group = if LintLevel::Warn <= self.primary_group.default_level { |
| let lint_level_priority = |
| level_priority("default", self.primary_group.default_level, pkg_lints); |
| Some(("default", lint_level_priority)) |
| } else { |
| None |
| }; |
| |
| let (_, (level, source, _)) = [ |
| (self.name, lint_level_priority), |
| (self.primary_group.name, group_level_priority), |
| ] |
| .into_iter() |
| .chain(default_group) |
| .max_by_key(|(n, (l, s, p))| { |
| ( |
| l == &LintLevel::Forbid, |
| *s != LintLevelSource::Default, |
| *p, |
| Reverse(*n), |
| ) |
| }) |
| .unwrap(); |
| |
| let (level, source) = match (level, gctx.warning_handling().ok()) { |
| // `Deny` needs to be handled later, at the end of the operation |
| (LintLevel::Warn, Some(WarningHandling::Allow)) => { |
| (LintLevel::Allow, LintLevelSource::Default) |
| } |
| _ => (level, source), |
| }; |
| |
| LintLevelProduct { level, source } |
| } |
| |
| pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String { |
| format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,) |
| } |
| } |
| |
| pub struct LintLevelProduct { |
| pub level: LintLevel, |
| pub source: LintLevelSource, |
| } |
| |
| #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| pub enum LintLevel { |
| Allow, |
| Warn, |
| Deny, |
| Forbid, |
| } |
| |
| impl Display for LintLevel { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| LintLevel::Allow => write!(f, "allow"), |
| LintLevel::Warn => write!(f, "warn"), |
| LintLevel::Deny => write!(f, "deny"), |
| LintLevel::Forbid => write!(f, "forbid"), |
| } |
| } |
| } |
| |
| impl LintLevel { |
| pub fn is_warn(&self) -> bool { |
| self == &LintLevel::Warn |
| } |
| |
| pub fn is_error(&self) -> bool { |
| self == &LintLevel::Forbid || self == &LintLevel::Deny |
| } |
| |
| pub fn to_diagnostic_level(self) -> Level<'static> { |
| match self { |
| LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"), |
| LintLevel::Warn => Level::WARNING, |
| LintLevel::Deny => Level::ERROR, |
| LintLevel::Forbid => Level::ERROR, |
| } |
| } |
| |
| pub fn force(self) -> bool { |
| match self { |
| Self::Allow => false, |
| Self::Warn => true, |
| Self::Deny => true, |
| Self::Forbid => true, |
| } |
| } |
| } |
| |
| impl From<manifest::TomlLintLevel> for LintLevel { |
| fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel { |
| match toml_lint_level { |
| manifest::TomlLintLevel::Allow => LintLevel::Allow, |
| manifest::TomlLintLevel::Warn => LintLevel::Warn, |
| manifest::TomlLintLevel::Deny => LintLevel::Deny, |
| manifest::TomlLintLevel::Forbid => LintLevel::Forbid, |
| } |
| } |
| } |
| |
| #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| pub enum LintLevelSource { |
| Default, |
| Package, |
| } |
| |
| impl Display for LintLevelSource { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| LintLevelSource::Default => write!(f, "by default"), |
| LintLevelSource::Package => write!(f, "in `[lints]`"), |
| } |
| } |
| } |
| |
| impl LintLevelSource { |
| pub(crate) fn is_user_specified(&self) -> bool { |
| match self { |
| LintLevelSource::Default => false, |
| LintLevelSource::Package => true, |
| } |
| } |
| } |
| |
| pub(crate) fn level_priority( |
| name: &str, |
| default_level: LintLevel, |
| pkg_lints: &manifest::TomlToolLints, |
| ) -> (LintLevel, LintLevelSource, i8) { |
| if let Some(defined_level) = pkg_lints.get(name) { |
| ( |
| defined_level.level().into(), |
| LintLevelSource::Package, |
| defined_level.priority(), |
| ) |
| } else { |
| (default_level, LintLevelSource::Default, 0) |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| pub struct LintGroup { |
| pub name: &'static str, |
| pub default_level: LintLevel, |
| pub desc: &'static str, |
| pub feature_gate: Option<&'static Feature>, |
| pub hidden: bool, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| const STYLE: LintGroup = LintGroup { |
| name: "style", |
| desc: "code that should be written in a more idiomatic way", |
| default_level: LintLevel::Warn, |
| feature_gate: None, |
| hidden: false, |
| }; |
| |
| fn gctx() -> GlobalContext { |
| let cwd = std::env::current_dir().unwrap(); |
| GlobalContext::new( |
| cargo_util_terminal::Shell::new(), |
| cwd.clone(), |
| home::cargo_home_with_cwd(&cwd).unwrap(), |
| ) |
| } |
| |
| fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint { |
| Lint { |
| name, |
| desc: "test lint", |
| primary_group: group, |
| msrv: None, |
| feature_gate: None, |
| docs: None, |
| } |
| } |
| |
| #[test] |
| fn lint_level_prefers_user_specified_over_default() { |
| let lint = test_lint("unused_dependencies", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "unused_dependencies".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Deny), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn lint_level_group_overrides_default() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "style".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Deny), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn default_group_overrides_default() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "default".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Deny), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn default_before_primary() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "default".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Deny), |
| ); |
| pkg_lints.insert( |
| "style".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Allow), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn default_after_primary() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "style".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Allow), |
| ); |
| pkg_lints.insert( |
| "default".to_string(), |
| manifest::TomlLint::Level(manifest::TomlLintLevel::Deny), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn default_higher_than_primary() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "default".to_string(), |
| manifest::TomlLint::Config(manifest::TomlLintConfig { |
| level: manifest::TomlLintLevel::Deny, |
| priority: 1, |
| config: Default::default(), |
| }), |
| ); |
| pkg_lints.insert( |
| "style".to_string(), |
| manifest::TomlLint::Config(manifest::TomlLintConfig { |
| level: manifest::TomlLintLevel::Allow, |
| priority: -1, |
| config: Default::default(), |
| }), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Deny); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| |
| #[test] |
| fn default_lower_than_primary() { |
| let lint = test_lint("non_kebab_case_bins", &STYLE); |
| |
| let mut pkg_lints = manifest::TomlToolLints::new(); |
| pkg_lints.insert( |
| "default".to_string(), |
| manifest::TomlLint::Config(manifest::TomlLintConfig { |
| level: manifest::TomlLintLevel::Deny, |
| priority: -1, |
| config: Default::default(), |
| }), |
| ); |
| pkg_lints.insert( |
| "style".to_string(), |
| manifest::TomlLint::Config(manifest::TomlLintConfig { |
| level: manifest::TomlLintLevel::Allow, |
| priority: 1, |
| config: Default::default(), |
| }), |
| ); |
| let features = Features::default(); |
| |
| let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx()); |
| assert_eq!(level, LintLevel::Allow); |
| assert_eq!(source, LintLevelSource::Package); |
| } |
| } |