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