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}