#
cargo/lints/
mod.rs

    1use crate::core::{Edition, Feature, Features, MaybePackage, Package};
    2use crate::{CargoResult, GlobalContext};
    3
    4use annotate_snippets::AnnotationKind;
    5use annotate_snippets::Group;
    6use annotate_snippets::Level;
    7use annotate_snippets::Snippet;
    8use cargo_util_schemas::manifest::TomlLintLevel;
    9use cargo_util_schemas::manifest::TomlToolLints;
    10use pathdiff::diff_paths;
    11
    12use std::borrow::Cow;
    13use std::cmp::{Reverse, max_by_key};
    14use std::fmt::Display;
    15use std::ops::Range;
    16use std::path::Path;
    17
    18pub mod rules;
    19pub use rules::LINTS;
    20
    21pub const LINT_GROUPS: &[LintGroup] = &[
    22    COMPLEXITY,
    23    CORRECTNESS,
    24    NURSERY,
    25    PEDANTIC,
    26    PERF,
    27    RESTRICTION,
    28    STYLE,
    29    SUSPICIOUS,
    30    TEST_DUMMY_UNSTABLE,
    31];
    32
    33/// Scope at which a lint runs: package-level or workspace-level.
    34pub enum ManifestFor<'a> {
    35    /// Lint runs for a specific package.
    36    Package(&'a Package),
    37    /// Lint runs for workspace-level config.
    38    Workspace(&'a MaybePackage),
    39}
    40
    41impl ManifestFor<'_> {
    42    fn lint_level(&self, pkg_lints: &TomlToolLints, lint: Lint) -> (LintLevel, LintLevelReason) {
    43        lint.level(pkg_lints, self.edition(), self.unstable_features())
    44    }
    45
    46    pub fn contents(&self) -> Option<&str> {
    47        match self {
    48            ManifestFor::Package(p) => p.manifest().contents(),
    49            ManifestFor::Workspace(p) => p.contents(),
    50        }
    51    }
    52
    53    pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
    54        match self {
    55            ManifestFor::Package(p) => p.manifest().document(),
    56            ManifestFor::Workspace(p) => p.document(),
    57        }
    58    }
    59
    60    pub fn edition(&self) -> Edition {
    61        match self {
    62            ManifestFor::Package(p) => p.manifest().edition(),
    63            ManifestFor::Workspace(p) => p.edition(),
    64        }
    65    }
    66
    67    pub fn unstable_features(&self) -> &Features {
    68        match self {
    69            ManifestFor::Package(p) => p.manifest().unstable_features(),
    70            ManifestFor::Workspace(p) => p.unstable_features(),
    71        }
    72    }
    73}
    74
    75impl<'a> From<&'a Package> for ManifestFor<'a> {
    76    fn from(value: &'a Package) -> ManifestFor<'a> {
    77        ManifestFor::Package(value)
    78    }
    79}
    80
    81impl<'a> From<&'a MaybePackage> for ManifestFor<'a> {
    82    fn from(value: &'a MaybePackage) -> ManifestFor<'a> {
    83        ManifestFor::Workspace(value)
    84    }
    85}
    86
    87pub fn analyze_cargo_lints_table(
    88    manifest: ManifestFor<'_>,
    89    manifest_path: &Path,
    90    cargo_lints: &TomlToolLints,
    91    error_count: &mut usize,
    92    gctx: &GlobalContext,
    93) -> CargoResult<()> {
    94    let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
    95    let mut unknown_lints = Vec::new();
    96    for lint_name in cargo_lints.keys().map(|name| name) {
    97        let Some((name, default_level, edition_lint_opts, feature_gate)) =
    98            find_lint_or_group(lint_name)
    99        else {
    100            unknown_lints.push(lint_name);
    101            continue;
    102        };
    103
    104        let (_, reason, _) = level_priority(
    105            name,
    106            *default_level,
    107            *edition_lint_opts,
    108            cargo_lints,
    109            manifest.edition(),
    110        );
    111
    112        // Only run analysis on user-specified lints
    113        if !reason.is_user_specified() {
    114            continue;
    115        }
    116
    117        // Only run this on lints that are gated by a feature
    118        if let Some(feature_gate) = feature_gate
    119            && !manifest.unstable_features().is_enabled(feature_gate)
    120        {
    121            report_feature_not_enabled(
    122                name,
    123                feature_gate,
    124                &manifest,
    125                &manifest_path,
    126                error_count,
    127                gctx,
    128            )?;
    129        }
    130    }
    131
    132    rules::output_unknown_lints(
    133        unknown_lints,
    134        &manifest,
    135        &manifest_path,
    136        cargo_lints,
    137        error_count,
    138        gctx,
    139    )?;
    140
    141    Ok(())
    142}
    143
    144fn find_lint_or_group<'a>(
    145    name: &str,
    146) -> Option<(
    147    &'static str,
    148    &LintLevel,
    149    &Option<(Edition, LintLevel)>,
    150    &Option<&'static Feature>,
    151)> {
    152    if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
    153        Some((
    154            lint.name,
    155            &lint.primary_group.default_level,
    156            &lint.edition_lint_opts,
    157            &lint.feature_gate,
    158        ))
    159    } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
    160        Some((group.name, &group.default_level, &None, &group.feature_gate))
    161    } else {
    162        None
    163    }
    164}
    165
    166fn report_feature_not_enabled(
    167    lint_name: &str,
    168    feature_gate: &Feature,
    169    manifest: &ManifestFor<'_>,
    170    manifest_path: &str,
    171    error_count: &mut usize,
    172    gctx: &GlobalContext,
    173) -> CargoResult<()> {
    174    let dash_feature_name = feature_gate.name().replace("_", "-");
    175    let title = format!("use of unstable lint `{}`", lint_name);
    176    let label = format!(
    177        "this is behind `{}`, which is not enabled",
    178        dash_feature_name
    179    );
    180    let help = format!(
    181        "consider adding `cargo-features = [\"{}\"]` to the top of the manifest",
    182        dash_feature_name
    183    );
    184
    185    let key_path = match manifest {
    186        ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
    187        ManifestFor::Workspace(_) => &["workspace", "lints", "cargo", lint_name][..],
    188    };
    189
    190    let mut error = Group::with_title(Level::ERROR.primary_title(title));
    191
    192    if let Some(document) = manifest.document()
    193        && let Some(contents) = manifest.contents()
    194    {
    195        let Some(span) = get_key_value_span(document, key_path) else {
    196            // This lint is handled by either package or workspace lint.
    197            return Ok(());
    198        };
    199
    200        error = error.element(
    201            Snippet::source(contents)
    202                .path(manifest_path)
    203                .annotation(AnnotationKind::Primary.span(span.key).label(label)),
    204        )
    205    }
    206
    207    let report = [error.element(Level::HELP.message(help))];
    208
    209    *error_count += 1;
    210    gctx.shell().print_report(&report, true)?;
    211
    212    Ok(())
    213}
    214
    215#[derive(Clone)]
    216pub struct TomlSpan {
    217    pub key: Range<usize>,
    218    pub value: Range<usize>,
    219}
    220
    221pub fn get_key_value<'doc>(
    222    document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
    223    path: &[&str],
    224) -> Option<(
    225    &'doc toml::Spanned<Cow<'doc, str>>,
    226    &'doc toml::Spanned<toml::de::DeValue<'static>>,
    227)> {
    228    let mut table = document.get_ref();
    229    let mut iter = path.into_iter().peekable();
    230    while let Some(key) = iter.next() {
    231        let key_s: &str = key.as_ref();
    232        let (key, item) = table.get_key_value(key_s)?;
    233        if iter.peek().is_none() {
    234            return Some((key, item));
    235        }
    236        if let Some(next_table) = item.get_ref().as_table() {
    237            table = next_table;
    238        }
    239        if iter.peek().is_some() {
    240            if let Some(array) = item.get_ref().as_array() {
    241                let next = iter.next().unwrap();
    242                return array.iter().find_map(|item| match item.get_ref() {
    243                    toml::de::DeValue::String(s) if s == next => Some((key, item)),
    244                    _ => None,
    245                });
    246            }
    247        }
    248    }
    249    None
    250}
    251
    252pub fn get_key_value_span(
    253    document: &toml::Spanned<toml::de::DeTable<'static>>,
    254    path: &[&str],
    255) -> Option<TomlSpan> {
    256    get_key_value(document, path).map(|(k, v)| TomlSpan {
    257        key: k.span(),
    258        value: v.span(),
    259    })
    260}
    261
    262/// Gets the relative path to a manifest from the current working directory, or
    263/// the absolute path of the manifest if a relative path cannot be constructed
    264pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
    265    diff_paths(path, gctx.cwd())
    266        .unwrap_or_else(|| path.to_path_buf())
    267        .display()
    268        .to_string()
    269}
    270
    271#[derive(Copy, Clone, Debug)]
    272pub struct LintGroup {
    273    pub name: &'static str,
    274    pub default_level: LintLevel,
    275    pub desc: &'static str,
    276    pub feature_gate: Option<&'static Feature>,
    277    pub hidden: bool,
    278}
    279
    280const COMPLEXITY: LintGroup = LintGroup {
    281    name: "complexity",
    282    desc: "code that does something simple but in a complex way",
    283    default_level: LintLevel::Warn,
    284    feature_gate: None,
    285    hidden: false,
    286};
    287
    288const CORRECTNESS: LintGroup = LintGroup {
    289    name: "correctness",
    290    desc: "code that is outright wrong or useless",
    291    default_level: LintLevel::Deny,
    292    feature_gate: None,
    293    hidden: false,
    294};
    295
    296const NURSERY: LintGroup = LintGroup {
    297    name: "nursery",
    298    desc: "new lints that are still under development",
    299    default_level: LintLevel::Allow,
    300    feature_gate: None,
    301    hidden: false,
    302};
    303
    304const PEDANTIC: LintGroup = LintGroup {
    305    name: "pedantic",
    306    desc: "lints which are rather strict or have occasional false positives",
    307    default_level: LintLevel::Allow,
    308    feature_gate: None,
    309    hidden: false,
    310};
    311
    312const PERF: LintGroup = LintGroup {
    313    name: "perf",
    314    desc: "code that can be written to run faster",
    315    default_level: LintLevel::Warn,
    316    feature_gate: None,
    317    hidden: false,
    318};
    319
    320const RESTRICTION: LintGroup = LintGroup {
    321    name: "restriction",
    322    desc: "lints which prevent the use of Cargo features",
    323    default_level: LintLevel::Allow,
    324    feature_gate: None,
    325    hidden: false,
    326};
    327
    328const STYLE: LintGroup = LintGroup {
    329    name: "style",
    330    desc: "code that should be written in a more idiomatic way",
    331    default_level: LintLevel::Warn,
    332    feature_gate: None,
    333    hidden: false,
    334};
    335
    336const SUSPICIOUS: LintGroup = LintGroup {
    337    name: "suspicious",
    338    desc: "code that is most likely wrong or useless",
    339    default_level: LintLevel::Warn,
    340    feature_gate: None,
    341    hidden: false,
    342};
    343
    344/// This lint group is only to be used for testing purposes
    345const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
    346    name: "test_dummy_unstable",
    347    desc: "test_dummy_unstable is meant to only be used in tests",
    348    default_level: LintLevel::Allow,
    349    feature_gate: Some(Feature::test_dummy_unstable()),
    350    hidden: true,
    351};
    352
    353#[derive(Copy, Clone, Debug)]
    354pub struct Lint {
    355    pub name: &'static str,
    356    pub desc: &'static str,
    357    pub primary_group: &'static LintGroup,
    358    pub edition_lint_opts: Option<(Edition, LintLevel)>,
    359    pub feature_gate: Option<&'static Feature>,
    360    /// This is a markdown formatted string that will be used when generating
    361    /// the lint documentation. If docs is `None`, the lint will not be
    362    /// documented.
    363    pub docs: Option<&'static str>,
    364}
    365
    366impl Lint {
    367    pub fn level(
    368        &self,
    369        pkg_lints: &TomlToolLints,
    370        edition: Edition,
    371        unstable_features: &Features,
    372    ) -> (LintLevel, LintLevelReason) {
    373        // We should return `Allow` if a lint is behind a feature, but it is
    374        // not enabled, that way the lint does not run.
    375        if self
    376            .feature_gate
    377            .is_some_and(|f| !unstable_features.is_enabled(f))
    378        {
    379            return (LintLevel::Allow, LintLevelReason::Default);
    380        }
    381
    382        let lint_level_priority = level_priority(
    383            self.name,
    384            self.primary_group.default_level,
    385            self.edition_lint_opts,
    386            pkg_lints,
    387            edition,
    388        );
    389
    390        let group_level_priority = level_priority(
    391            self.primary_group.name,
    392            self.primary_group.default_level,
    393            None,
    394            pkg_lints,
    395            edition,
    396        );
    397
    398        let (_, (l, r, _)) = max_by_key(
    399            (self.name, lint_level_priority),
    400            (self.primary_group.name, group_level_priority),
    401            |(n, (l, _, p))| (l == &LintLevel::Forbid, *p, Reverse(*n)),
    402        );
    403        (l, r)
    404    }
    405
    406    fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
    407        format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
    408    }
    409}
    410
    411#[derive(Copy, Clone, Debug, PartialEq)]
    412pub enum LintLevel {
    413    Allow,
    414    Warn,
    415    Deny,
    416    Forbid,
    417}
    418
    419impl Display for LintLevel {
    420    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    421        match self {
    422            LintLevel::Allow => write!(f, "allow"),
    423            LintLevel::Warn => write!(f, "warn"),
    424            LintLevel::Deny => write!(f, "deny"),
    425            LintLevel::Forbid => write!(f, "forbid"),
    426        }
    427    }
    428}
    429
    430impl LintLevel {
    431    pub fn is_error(&self) -> bool {
    432        self == &LintLevel::Forbid || self == &LintLevel::Deny
    433    }
    434
    435    pub fn to_diagnostic_level(self) -> Level<'static> {
    436        match self {
    437            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
    438            LintLevel::Warn => Level::WARNING,
    439            LintLevel::Deny => Level::ERROR,
    440            LintLevel::Forbid => Level::ERROR,
    441        }
    442    }
    443
    444    fn force(self) -> bool {
    445        match self {
    446            Self::Allow => false,
    447            Self::Warn => true,
    448            Self::Deny => true,
    449            Self::Forbid => true,
    450        }
    451    }
    452}
    453
    454impl From<TomlLintLevel> for LintLevel {
    455    fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
    456        match toml_lint_level {
    457            TomlLintLevel::Allow => LintLevel::Allow,
    458            TomlLintLevel::Warn => LintLevel::Warn,
    459            TomlLintLevel::Deny => LintLevel::Deny,
    460            TomlLintLevel::Forbid => LintLevel::Forbid,
    461        }
    462    }
    463}
    464
    465#[derive(Copy, Clone, Debug, PartialEq, Eq)]
    466pub enum LintLevelReason {
    467    Default,
    468    Edition(Edition),
    469    Package,
    470}
    471
    472impl Display for LintLevelReason {
    473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    474        match self {
    475            LintLevelReason::Default => write!(f, "by default"),
    476            LintLevelReason::Edition(edition) => write!(f, "in edition {}", edition),
    477            LintLevelReason::Package => write!(f, "in `[lints]`"),
    478        }
    479    }
    480}
    481
    482impl LintLevelReason {
    483    fn is_user_specified(&self) -> bool {
    484        match self {
    485            LintLevelReason::Default => false,
    486            LintLevelReason::Edition(_) => false,
    487            LintLevelReason::Package => true,
    488        }
    489    }
    490}
    491
    492fn level_priority(
    493    name: &str,
    494    default_level: LintLevel,
    495    edition_lint_opts: Option<(Edition, LintLevel)>,
    496    pkg_lints: &TomlToolLints,
    497    edition: Edition,
    498) -> (LintLevel, LintLevelReason, i8) {
    499    let (unspecified_level, reason) = if let Some(level) = edition_lint_opts
    500        .filter(|(e, _)| edition >= *e)
    501        .map(|(_, l)| l)
    502    {
    503        (level, LintLevelReason::Edition(edition))
    504    } else {
    505        (default_level, LintLevelReason::Default)
    506    };
    507
    508    // Don't allow the group to be overridden if the level is `Forbid`
    509    if unspecified_level == LintLevel::Forbid {
    510        return (unspecified_level, reason, 0);
    511    }
    512
    513    if let Some(defined_level) = pkg_lints.get(name) {
    514        (
    515            defined_level.level().into(),
    516            LintLevelReason::Package,
    517            defined_level.priority(),
    518        )
    519    } else {
    520        (unspecified_level, reason, 0)
    521    }
    522}
    523
    524#[cfg(test)]
    525mod tests {
    526    use itertools::Itertools;
    527    use snapbox::ToDebug;
    528    use std::collections::HashSet;
    529
    530    #[test]
    531    fn ensure_sorted_lints() {
    532        // This will be printed out if the fields are not sorted.
    533        let location = std::panic::Location::caller();
    534        println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
    535
    536        let actual = super::LINTS
    537            .iter()
    538            .map(|l| l.name.to_uppercase())
    539            .collect::<Vec<_>>();
    540
    541        let mut expected = actual.clone();
    542        expected.sort();
    543        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
    544    }
    545
    546    #[test]
    547    fn ensure_sorted_lint_groups() {
    548        // This will be printed out if the fields are not sorted.
    549        let location = std::panic::Location::caller();
    550        println!(
    551            "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
    552            location.file(),
    553        );
    554        let actual = super::LINT_GROUPS
    555            .iter()
    556            .map(|l| l.name.to_uppercase())
    557            .collect::<Vec<_>>();
    558
    559        let mut expected = actual.clone();
    560        expected.sort();
    561        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
    562    }
    563
    564    #[test]
    565    fn ensure_updated_lints() {
    566        let dir = snapbox::utils::current_dir!().join("rules");
    567        let mut expected = HashSet::new();
    568        for entry in std::fs::read_dir(&dir).unwrap() {
    569            let entry = entry.unwrap();
    570            let path = entry.path();
    571            if path.ends_with("mod.rs") {
    572                continue;
    573            }
    574            let lint_name = path.file_stem().unwrap().to_string_lossy();
    575            assert!(expected.insert(lint_name.into()), "duplicate lint found");
    576        }
    577
    578        let actual = super::LINTS
    579            .iter()
    580            .map(|l| l.name.to_string())
    581            .collect::<HashSet<_>>();
    582        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
    583
    584        let mut need_added = String::new();
    585        for name in &diff {
    586            need_added.push_str(&format!("{name}\n"));
    587        }
    588        assert!(
    589            diff.is_empty(),
    590            "\n`LINTS` did not contain all `Lint`s found in {}\n\
    591            Please add the following to `LINTS`:\n\
    592            {need_added}",
    593            dir.display(),
    594        );
    595    }
    596
    597    #[test]
    598    fn ensure_updated_lint_groups() {
    599        let path = snapbox::utils::current_rs!();
    600        let expected = std::fs::read_to_string(&path).unwrap();
    601        let expected = expected
    602            .lines()
    603            .filter_map(|l| {
    604                if l.ends_with(": LintGroup = LintGroup {") {
    605                    Some(
    606                        l.chars()
    607                            .skip(6)
    608                            .take_while(|c| *c != ':')
    609                            .collect::<String>(),
    610                    )
    611                } else {
    612                    None
    613                }
    614            })
    615            .collect::<HashSet<_>>();
    616        let actual = super::LINT_GROUPS
    617            .iter()
    618            .map(|l| l.name.to_uppercase())
    619            .collect::<HashSet<_>>();
    620        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
    621
    622        let mut need_added = String::new();
    623        for name in &diff {
    624            need_added.push_str(&format!("{}\n", name));
    625        }
    626        assert!(
    627            diff.is_empty(),
    628            "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
    629            Please add the following to `LINT_GROUPS`:\n\
    630            {}",
    631            path.display(),
    632            need_added
    633        );
    634    }
    635}