#
cargo/util/
lints.rs

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