blob: 94c382f2e8e3f7cdf1a57ddbb1215d9054848a09 [file] [edit]
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);
}
}