#
cargo/util/
lints.rs

    1use crate::core::{Edition, Feature, Features, Manifest, MaybePackage, Package};
    2use crate::{CargoResult, GlobalContext};
    3use annotate_snippets::{AnnotationKind, Group, Level, Patch, Snippet};
    4use cargo_util_schemas::manifest::{ProfilePackageSpec, TomlLintLevel, TomlToolLints};
    5use pathdiff::diff_paths;
    6use std::borrow::Cow;
    7use std::fmt::Display;
    8use std::ops::Range;
    9use std::path::Path;
    10
    11const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
    12pub const LINTS: &[Lint] = &[BLANKET_HINT_MOSTLY_UNUSED, 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: &toml::Spanned<toml::de::DeTable<'static>>,
    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: &toml::Spanned<toml::de::DeTable<'static>>,
    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 (contents, path, span) = if let Some(span) =
    138            get_key_value_span(manifest.document(), &["lints", "cargo", lint_name])
    139        {
    140            (manifest.contents(), manifest_path, span)
    141        } else if let Some(lint_span) =
    142            get_key_value_span(ws_document, &["workspace", "lints", "cargo", lint_name])
    143        {
    144            (ws_contents, ws_path, lint_span)
    145        } else {
    146            panic!("could not find `cargo::{lint_name}` in `[lints]`, or `[workspace.lints]` ")
    147        };
    148
    149        let mut report = Vec::new();
    150        report.push(
    151            Group::with_title(Level::ERROR.primary_title(title))
    152                .element(
    153                    Snippet::source(contents)
    154                        .path(path)
    155                        .annotation(AnnotationKind::Primary.span(span.key).label(label)),
    156                )
    157                .element(Level::HELP.message(help)),
    158        );
    159
    160        if let Some(inherit_span) = get_key_value_span(manifest.document(), &["lints", "workspace"])
    161        {
    162            report.push(
    163                Group::with_title(Level::NOTE.secondary_title(second_title)).element(
    164                    Snippet::source(manifest.contents())
    165                        .path(manifest_path)
    166                        .annotation(
    167                            AnnotationKind::Context
    168                                .span(inherit_span.key.start..inherit_span.value.end),
    169                        ),
    170                ),
    171            );
    172        }
    173
    174        *error_count += 1;
    175        gctx.shell().print_report(&report, true)?;
    176    }
    177    Ok(())
    178}
    179
    180#[derive(Clone)]
    181pub struct TomlSpan {
    182    pub key: Range<usize>,
    183    pub value: Range<usize>,
    184}
    185
    186pub fn get_key_value<'doc>(
    187    document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
    188    path: &[&str],
    189) -> Option<(
    190    &'doc toml::Spanned<Cow<'doc, str>>,
    191    &'doc toml::Spanned<toml::de::DeValue<'static>>,
    192)> {
    193    let mut table = document.get_ref();
    194    let mut iter = path.into_iter().peekable();
    195    while let Some(key) = iter.next() {
    196        let key_s: &str = key.as_ref();
    197        let (key, item) = table.get_key_value(key_s)?;
    198        if iter.peek().is_none() {
    199            return Some((key, item));
    200        }
    201        if let Some(next_table) = item.get_ref().as_table() {
    202            table = next_table;
    203        }
    204        if iter.peek().is_some() {
    205            if let Some(array) = item.get_ref().as_array() {
    206                let next = iter.next().unwrap();
    207                return array.iter().find_map(|item| match item.get_ref() {
    208                    toml::de::DeValue::String(s) if s == next => Some((key, item)),
    209                    _ => None,
    210                });
    211            }
    212        }
    213    }
    214    None
    215}
    216
    217pub fn get_key_value_span(
    218    document: &toml::Spanned<toml::de::DeTable<'static>>,
    219    path: &[&str],
    220) -> Option<TomlSpan> {
    221    get_key_value(document, path).map(|(k, v)| TomlSpan {
    222        key: k.span(),
    223        value: v.span(),
    224    })
    225}
    226
    227/// Gets the relative path to a manifest from the current working directory, or
    228/// the absolute path of the manifest if a relative path cannot be constructed
    229pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
    230    diff_paths(path, gctx.cwd())
    231        .unwrap_or_else(|| path.to_path_buf())
    232        .display()
    233        .to_string()
    234}
    235
    236#[derive(Copy, Clone, Debug)]
    237pub struct LintGroup {
    238    pub name: &'static str,
    239    pub default_level: LintLevel,
    240    pub desc: &'static str,
    241    pub edition_lint_opts: Option<(Edition, LintLevel)>,
    242    pub feature_gate: Option<&'static Feature>,
    243}
    244
    245/// This lint group is only to be used for testing purposes
    246const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
    247    name: "test_dummy_unstable",
    248    desc: "test_dummy_unstable is meant to only be used in tests",
    249    default_level: LintLevel::Allow,
    250    edition_lint_opts: None,
    251    feature_gate: Some(Feature::test_dummy_unstable()),
    252};
    253
    254#[derive(Copy, Clone, Debug)]
    255pub struct Lint {
    256    pub name: &'static str,
    257    pub desc: &'static str,
    258    pub groups: &'static [LintGroup],
    259    pub default_level: LintLevel,
    260    pub edition_lint_opts: Option<(Edition, LintLevel)>,
    261    pub feature_gate: Option<&'static Feature>,
    262    /// This is a markdown formatted string that will be used when generating
    263    /// the lint documentation. If docs is `None`, the lint will not be
    264    /// documented.
    265    pub docs: Option<&'static str>,
    266}
    267
    268impl Lint {
    269    pub fn level(
    270        &self,
    271        pkg_lints: &TomlToolLints,
    272        edition: Edition,
    273        unstable_features: &Features,
    274    ) -> (LintLevel, LintLevelReason) {
    275        // We should return `Allow` if a lint is behind a feature, but it is
    276        // not enabled, that way the lint does not run.
    277        if self
    278            .feature_gate
    279            .is_some_and(|f| !unstable_features.is_enabled(f))
    280        {
    281            return (LintLevel::Allow, LintLevelReason::Default);
    282        }
    283
    284        self.groups
    285            .iter()
    286            .map(|g| {
    287                (
    288                    g.name,
    289                    level_priority(
    290                        g.name,
    291                        g.default_level,
    292                        g.edition_lint_opts,
    293                        pkg_lints,
    294                        edition,
    295                    ),
    296                )
    297            })
    298            .chain(std::iter::once((
    299                self.name,
    300                level_priority(
    301                    self.name,
    302                    self.default_level,
    303                    self.edition_lint_opts,
    304                    pkg_lints,
    305                    edition,
    306                ),
    307            )))
    308            .max_by_key(|(n, (l, _, p))| (l == &LintLevel::Forbid, *p, std::cmp::Reverse(*n)))
    309            .map(|(_, (l, r, _))| (l, r))
    310            .unwrap()
    311    }
    312
    313    fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
    314        format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
    315    }
    316}
    317
    318#[derive(Copy, Clone, Debug, PartialEq)]
    319pub enum LintLevel {
    320    Allow,
    321    Warn,
    322    Deny,
    323    Forbid,
    324}
    325
    326impl Display for LintLevel {
    327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    328        match self {
    329            LintLevel::Allow => write!(f, "allow"),
    330            LintLevel::Warn => write!(f, "warn"),
    331            LintLevel::Deny => write!(f, "deny"),
    332            LintLevel::Forbid => write!(f, "forbid"),
    333        }
    334    }
    335}
    336
    337impl LintLevel {
    338    pub fn is_error(&self) -> bool {
    339        self == &LintLevel::Forbid || self == &LintLevel::Deny
    340    }
    341
    342    pub fn to_diagnostic_level(self) -> Level<'static> {
    343        match self {
    344            LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
    345            LintLevel::Warn => Level::WARNING,
    346            LintLevel::Deny => Level::ERROR,
    347            LintLevel::Forbid => Level::ERROR,
    348        }
    349    }
    350
    351    fn force(self) -> bool {
    352        match self {
    353            Self::Allow => false,
    354            Self::Warn => true,
    355            Self::Deny => true,
    356            Self::Forbid => true,
    357        }
    358    }
    359}
    360
    361impl From<TomlLintLevel> for LintLevel {
    362    fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
    363        match toml_lint_level {
    364            TomlLintLevel::Allow => LintLevel::Allow,
    365            TomlLintLevel::Warn => LintLevel::Warn,
    366            TomlLintLevel::Deny => LintLevel::Deny,
    367            TomlLintLevel::Forbid => LintLevel::Forbid,
    368        }
    369    }
    370}
    371
    372#[derive(Copy, Clone, Debug, PartialEq, Eq)]
    373pub enum LintLevelReason {
    374    Default,
    375    Edition(Edition),
    376    Package,
    377}
    378
    379impl Display for LintLevelReason {
    380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    381        match self {
    382            LintLevelReason::Default => write!(f, "by default"),
    383            LintLevelReason::Edition(edition) => write!(f, "in edition {}", edition),
    384            LintLevelReason::Package => write!(f, "in `[lints]`"),
    385        }
    386    }
    387}
    388
    389impl LintLevelReason {
    390    fn is_user_specified(&self) -> bool {
    391        match self {
    392            LintLevelReason::Default => false,
    393            LintLevelReason::Edition(_) => false,
    394            LintLevelReason::Package => true,
    395        }
    396    }
    397}
    398
    399fn level_priority(
    400    name: &str,
    401    default_level: LintLevel,
    402    edition_lint_opts: Option<(Edition, LintLevel)>,
    403    pkg_lints: &TomlToolLints,
    404    edition: Edition,
    405) -> (LintLevel, LintLevelReason, i8) {
    406    let (unspecified_level, reason) = if let Some(level) = edition_lint_opts
    407        .filter(|(e, _)| edition >= *e)
    408        .map(|(_, l)| l)
    409    {
    410        (level, LintLevelReason::Edition(edition))
    411    } else {
    412        (default_level, LintLevelReason::Default)
    413    };
    414
    415    // Don't allow the group to be overridden if the level is `Forbid`
    416    if unspecified_level == LintLevel::Forbid {
    417        return (unspecified_level, reason, 0);
    418    }
    419
    420    if let Some(defined_level) = pkg_lints.get(name) {
    421        (
    422            defined_level.level().into(),
    423            LintLevelReason::Package,
    424            defined_level.priority(),
    425        )
    426    } else {
    427        (unspecified_level, reason, 0)
    428    }
    429}
    430
    431/// This lint is only to be used for testing purposes
    432const IM_A_TEAPOT: Lint = Lint {
    433    name: "im_a_teapot",
    434    desc: "`im_a_teapot` is specified",
    435    groups: &[TEST_DUMMY_UNSTABLE],
    436    default_level: LintLevel::Allow,
    437    edition_lint_opts: None,
    438    feature_gate: Some(Feature::test_dummy_unstable()),
    439    docs: None,
    440};
    441
    442pub fn check_im_a_teapot(
    443    pkg: &Package,
    444    path: &Path,
    445    pkg_lints: &TomlToolLints,
    446    error_count: &mut usize,
    447    gctx: &GlobalContext,
    448) -> CargoResult<()> {
    449    let manifest = pkg.manifest();
    450    let (lint_level, reason) =
    451        IM_A_TEAPOT.level(pkg_lints, manifest.edition(), manifest.unstable_features());
    452
    453    if lint_level == LintLevel::Allow {
    454        return Ok(());
    455    }
    456
    457    if manifest
    458        .normalized_toml()
    459        .package()
    460        .is_some_and(|p| p.im_a_teapot.is_some())
    461    {
    462        if lint_level.is_error() {
    463            *error_count += 1;
    464        }
    465        let level = lint_level.to_diagnostic_level();
    466        let manifest_path = rel_cwd_manifest_path(path, gctx);
    467        let emitted_reason = IM_A_TEAPOT.emitted_source(lint_level, reason);
    468
    469        let span = get_key_value_span(manifest.document(), &["package", "im-a-teapot"]).unwrap();
    470
    471        let report = &[Group::with_title(level.primary_title(IM_A_TEAPOT.desc))
    472            .element(
    473                Snippet::source(manifest.contents())
    474                    .path(&manifest_path)
    475                    .annotation(AnnotationKind::Primary.span(span.key.start..span.value.end)),
    476            )
    477            .element(Level::NOTE.message(&emitted_reason))];
    478
    479        gctx.shell().print_report(report, lint_level.force())?;
    480    }
    481    Ok(())
    482}
    483
    484const BLANKET_HINT_MOSTLY_UNUSED: Lint = Lint {
    485    name: "blanket_hint_mostly_unused",
    486    desc: "blanket_hint_mostly_unused lint",
    487    groups: &[],
    488    default_level: LintLevel::Warn,
    489    edition_lint_opts: None,
    490    feature_gate: None,
    491    docs: Some(
    492        r#"
    493### What it does
    494Checks if `hint-mostly-unused` being applied to all dependencies.
    495
    496### Why it is bad
    497`hint-mostly-unused` indicates that most of a crate's API surface will go
    498unused by anything depending on it; this hint can speed up the build by
    499attempting to minimize compilation time for items that aren't used at all.
    500Misapplication to crates that don't fit that criteria will slow down the build
    501rather than speeding it up. It should be selectively applied to dependencies
    502that meet these criteria. Applying it globally is always a misapplication and
    503will likely slow down the build.
    504
    505### Example
    506```toml
    507[profile.dev.package."*"]
    508hint-mostly-unused = true
    509```
    510
    511Should instead be:
    512```toml
    513[profile.dev.package.huge-mostly-unused-dependency]
    514hint-mostly-unused = true
    515```
    516"#,
    517    ),
    518};
    519
    520pub fn blanket_hint_mostly_unused(
    521    maybe_pkg: &MaybePackage,
    522    path: &Path,
    523    pkg_lints: &TomlToolLints,
    524    error_count: &mut usize,
    525    gctx: &GlobalContext,
    526) -> CargoResult<()> {
    527    let (lint_level, reason) = BLANKET_HINT_MOSTLY_UNUSED.level(
    528        pkg_lints,
    529        maybe_pkg.edition(),
    530        maybe_pkg.unstable_features(),
    531    );
    532
    533    if lint_level == LintLevel::Allow {
    534        return Ok(());
    535    }
    536
    537    let level = lint_level.to_diagnostic_level();
    538    let manifest_path = rel_cwd_manifest_path(path, gctx);
    539    let mut paths = Vec::new();
    540
    541    if let Some(profiles) = maybe_pkg.profiles() {
    542        for (profile_name, top_level_profile) in &profiles.0 {
    543            if let Some(true) = top_level_profile.hint_mostly_unused {
    544                paths.push((
    545                    vec!["profile", profile_name.as_str(), "hint-mostly-unused"],
    546                    true,
    547                ));
    548            }
    549
    550            if let Some(build_override) = &top_level_profile.build_override
    551                && let Some(true) = build_override.hint_mostly_unused
    552            {
    553                paths.push((
    554                    vec![
    555                        "profile",
    556                        profile_name.as_str(),
    557                        "build-override",
    558                        "hint-mostly-unused",
    559                    ],
    560                    false,
    561                ));
    562            }
    563
    564            if let Some(packages) = &top_level_profile.package
    565                && let Some(profile) = packages.get(&ProfilePackageSpec::All)
    566                && let Some(true) = profile.hint_mostly_unused
    567            {
    568                paths.push((
    569                    vec![
    570                        "profile",
    571                        profile_name.as_str(),
    572                        "package",
    573                        "*",
    574                        "hint-mostly-unused",
    575                    ],
    576                    false,
    577                ));
    578            }
    579        }
    580    }
    581
    582    for (i, (path, show_per_pkg_suggestion)) in paths.iter().enumerate() {
    583        if lint_level.is_error() {
    584            *error_count += 1;
    585        }
    586        let title = "`hint-mostly-unused` is being blanket applied to all dependencies";
    587        let help_txt =
    588            "scope `hint-mostly-unused` to specific packages with a lot of unused object code";
    589        if let (Some(span), Some(table_span)) = (
    590            get_key_value_span(maybe_pkg.document(), &path),
    591            get_key_value_span(maybe_pkg.document(), &path[..path.len() - 1]),
    592        ) {
    593            let mut report = Vec::new();
    594            let mut primary_group = level.clone().primary_title(title).element(
    595                Snippet::source(maybe_pkg.contents())
    596                    .path(&manifest_path)
    597                    .annotation(
    598                        AnnotationKind::Primary.span(table_span.key.start..table_span.key.end),
    599                    )
    600                    .annotation(AnnotationKind::Context.span(span.key.start..span.value.end)),
    601            );
    602
    603            if *show_per_pkg_suggestion {
    604                report.push(
    605                    Level::HELP.secondary_title(help_txt).element(
    606                        Snippet::source(maybe_pkg.contents())
    607                            .path(&manifest_path)
    608                            .patch(Patch::new(
    609                                table_span.key.end..table_span.key.end,
    610                                ".package.<pkg_name>",
    611                            )),
    612                    ),
    613                );
    614            } else {
    615                primary_group = primary_group.element(Level::HELP.message(help_txt));
    616            }
    617
    618            if i == 0 {
    619                primary_group =
    620                    primary_group
    621                        .element(Level::NOTE.message(
    622                            BLANKET_HINT_MOSTLY_UNUSED.emitted_source(lint_level, reason),
    623                        ));
    624            }
    625
    626            // The primary group should always be first
    627            report.insert(0, primary_group);
    628
    629            gctx.shell().print_report(&report, lint_level.force())?;
    630        }
    631    }
    632
    633    Ok(())
    634}
    635
    636const UNKNOWN_LINTS: Lint = Lint {
    637    name: "unknown_lints",
    638    desc: "unknown lint",
    639    groups: &[],
    640    default_level: LintLevel::Warn,
    641    edition_lint_opts: None,
    642    feature_gate: None,
    643    docs: Some(
    644        r#"
    645### What it does
    646Checks for unknown lints in the `[lints.cargo]` table
    647
    648### Why it is bad
    649- The lint name could be misspelled, leading to confusion as to why it is
    650  not working as expected
    651- The unknown lint could end up causing an error if `cargo` decides to make
    652  a lint with the same name in the future
    653
    654### Example
    655```toml
    656[lints.cargo]
    657this-lint-does-not-exist = "warn"
    658```
    659"#,
    660    ),
    661};
    662
    663fn output_unknown_lints(
    664    unknown_lints: Vec<&String>,
    665    manifest: &Manifest,
    666    manifest_path: &str,
    667    pkg_lints: &TomlToolLints,
    668    ws_contents: &str,
    669    ws_document: &toml::Spanned<toml::de::DeTable<'static>>,
    670    ws_path: &str,
    671    error_count: &mut usize,
    672    gctx: &GlobalContext,
    673) -> CargoResult<()> {
    674    let (lint_level, reason) =
    675        UNKNOWN_LINTS.level(pkg_lints, manifest.edition(), manifest.unstable_features());
    676    if lint_level == LintLevel::Allow {
    677        return Ok(());
    678    }
    679
    680    let level = lint_level.to_diagnostic_level();
    681    let mut emitted_source = None;
    682    for lint_name in unknown_lints {
    683        if lint_level.is_error() {
    684            *error_count += 1;
    685        }
    686        let title = format!("{}: `{lint_name}`", UNKNOWN_LINTS.desc);
    687        let second_title = format!("`cargo::{}` was inherited", lint_name);
    688        let underscore_lint_name = lint_name.replace("-", "_");
    689        let matching = if let Some(lint) = LINTS.iter().find(|l| l.name == underscore_lint_name) {
    690            Some((lint.name, "lint"))
    691        } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == underscore_lint_name) {
    692            Some((group.name, "group"))
    693        } else {
    694            None
    695        };
    696        let help =
    697            matching.map(|(name, kind)| format!("there is a {kind} with a similar name: `{name}`"));
    698
    699        let (contents, path, span) = if let Some(span) =
    700            get_key_value_span(manifest.document(), &["lints", "cargo", lint_name])
    701        {
    702            (manifest.contents(), manifest_path, span)
    703        } else if let Some(lint_span) =
    704            get_key_value_span(ws_document, &["workspace", "lints", "cargo", lint_name])
    705        {
    706            (ws_contents, ws_path, lint_span)
    707        } else {
    708            panic!("could not find `cargo::{lint_name}` in `[lints]`, or `[workspace.lints]` ")
    709        };
    710
    711        let mut report = Vec::new();
    712        let mut group = Group::with_title(level.clone().primary_title(title)).element(
    713            Snippet::source(contents)
    714                .path(path)
    715                .annotation(AnnotationKind::Primary.span(span.key)),
    716        );
    717        if emitted_source.is_none() {
    718            emitted_source = Some(UNKNOWN_LINTS.emitted_source(lint_level, reason));
    719            group = group.element(Level::NOTE.message(emitted_source.as_ref().unwrap()));
    720        }
    721        if let Some(help) = help.as_ref() {
    722            group = group.element(Level::HELP.message(help));
    723        }
    724        report.push(group);
    725
    726        if let Some(inherit_span) = get_key_value_span(manifest.document(), &["lints", "workspace"])
    727        {
    728            report.push(
    729                Group::with_title(Level::NOTE.secondary_title(second_title)).element(
    730                    Snippet::source(manifest.contents())
    731                        .path(manifest_path)
    732                        .annotation(
    733                            AnnotationKind::Context
    734                                .span(inherit_span.key.start..inherit_span.value.end),
    735                        ),
    736                ),
    737            );
    738        }
    739
    740        gctx.shell().print_report(&report, lint_level.force())?;
    741    }
    742
    743    Ok(())
    744}
    745
    746#[cfg(test)]
    747mod tests {
    748    use itertools::Itertools;
    749    use snapbox::ToDebug;
    750    use std::collections::HashSet;
    751
    752    #[test]
    753    fn ensure_sorted_lints() {
    754        // This will be printed out if the fields are not sorted.
    755        let location = std::panic::Location::caller();
    756        println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
    757
    758        let actual = super::LINTS
    759            .iter()
    760            .map(|l| l.name.to_uppercase())
    761            .collect::<Vec<_>>();
    762
    763        let mut expected = actual.clone();
    764        expected.sort();
    765        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
    766    }
    767
    768    #[test]
    769    fn ensure_sorted_lint_groups() {
    770        // This will be printed out if the fields are not sorted.
    771        let location = std::panic::Location::caller();
    772        println!(
    773            "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
    774            location.file(),
    775        );
    776        let actual = super::LINT_GROUPS
    777            .iter()
    778            .map(|l| l.name.to_uppercase())
    779            .collect::<Vec<_>>();
    780
    781        let mut expected = actual.clone();
    782        expected.sort();
    783        snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
    784    }
    785
    786    #[test]
    787    fn ensure_updated_lints() {
    788        let path = snapbox::utils::current_rs!();
    789        let expected = std::fs::read_to_string(&path).unwrap();
    790        let expected = expected
    791            .lines()
    792            .filter_map(|l| {
    793                if l.ends_with(": Lint = Lint {") {
    794                    Some(
    795                        l.chars()
    796                            .skip(6)
    797                            .take_while(|c| *c != ':')
    798                            .collect::<String>(),
    799                    )
    800                } else {
    801                    None
    802                }
    803            })
    804            .collect::<HashSet<_>>();
    805        let actual = super::LINTS
    806            .iter()
    807            .map(|l| l.name.to_uppercase())
    808            .collect::<HashSet<_>>();
    809        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
    810
    811        let mut need_added = String::new();
    812        for name in &diff {
    813            need_added.push_str(&format!("{}\n", name));
    814        }
    815        assert!(
    816            diff.is_empty(),
    817            "\n`LINTS` did not contain all `Lint`s found in {}\n\
    818            Please add the following to `LINTS`:\n\
    819            {}",
    820            path.display(),
    821            need_added
    822        );
    823    }
    824
    825    #[test]
    826    fn ensure_updated_lint_groups() {
    827        let path = snapbox::utils::current_rs!();
    828        let expected = std::fs::read_to_string(&path).unwrap();
    829        let expected = expected
    830            .lines()
    831            .filter_map(|l| {
    832                if l.ends_with(": LintGroup = LintGroup {") {
    833                    Some(
    834                        l.chars()
    835                            .skip(6)
    836                            .take_while(|c| *c != ':')
    837                            .collect::<String>(),
    838                    )
    839                } else {
    840                    None
    841                }
    842            })
    843            .collect::<HashSet<_>>();
    844        let actual = super::LINT_GROUPS
    845            .iter()
    846            .map(|l| l.name.to_uppercase())
    847            .collect::<HashSet<_>>();
    848        let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
    849
    850        let mut need_added = String::new();
    851        for name in &diff {
    852            need_added.push_str(&format!("{}\n", name));
    853        }
    854        assert!(
    855            diff.is_empty(),
    856            "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
    857            Please add the following to `LINT_GROUPS`:\n\
    858            {}",
    859            path.display(),
    860            need_added
    861        );
    862    }
    863}