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 if !reason.is_user_specified() {
46 continue;
47 }
48
49 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
227pub 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
245const 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 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 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 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
431const 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 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 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 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}