#
cargo/util/
lints.rs

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