1use crate::core::{Edition, Feature, Features, Manifest, Package};
2use crate::{CargoResult, GlobalContext};
3use annotate_snippets::{Level, Snippet};
4use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
5use pathdiff::diff_paths;
6use std::fmt::Display;
7use std::ops::Range;
8use std::path::Path;
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 message = if let Some(span) =
137 get_span(manifest.document(), &["lints", "cargo", lint_name], false)
138 {
139 Level::Error
140 .title(&title)
141 .snippet(
142 Snippet::source(manifest.contents())
143 .origin(&manifest_path)
144 .annotation(Level::Error.span(span).label(&label))
145 .fold(true),
146 )
147 .footer(Level::Help.title(&help))
148 } else {
149 let lint_span = get_span(
150 ws_document,
151 &["workspace", "lints", "cargo", lint_name],
152 false,
153 )
154 .unwrap_or_else(|| {
155 panic!("could not find `cargo::{lint_name}` in `[lints]`, or `[workspace.lints]` ")
156 });
157
158 let inherited_note = if let (Some(inherit_span_key), Some(inherit_span_value)) = (
159 get_span(manifest.document(), &["lints", "workspace"], false),
160 get_span(manifest.document(), &["lints", "workspace"], true),
161 ) {
162 Level::Note.title(&second_title).snippet(
163 Snippet::source(manifest.contents())
164 .origin(&manifest_path)
165 .annotation(
166 Level::Note.span(inherit_span_key.start..inherit_span_value.end),
167 )
168 .fold(true),
169 )
170 } else {
171 Level::Note.title(&second_title)
172 };
173
174 Level::Error
175 .title(&title)
176 .snippet(
177 Snippet::source(ws_contents)
178 .origin(&ws_path)
179 .annotation(Level::Error.span(lint_span).label(&label))
180 .fold(true),
181 )
182 .footer(inherited_note)
183 .footer(Level::Help.title(&help))
184 };
185
186 *error_count += 1;
187 gctx.shell().print_message(message)?;
188 }
189 Ok(())
190}
191
192pub fn get_span(
193 document: &toml::Spanned<toml::de::DeTable<'static>>,
194 path: &[&str],
195 get_value: bool,
196) -> Option<Range<usize>> {
197 let mut table = document.get_ref();
198 let mut iter = path.into_iter().peekable();
199 while let Some(key) = iter.next() {
200 let key_s: &str = key.as_ref();
201 let (key, item) = table.get_key_value(key_s)?;
202 if iter.peek().is_none() {
203 return if get_value {
204 Some(item.span())
205 } else {
206 Some(key.span())
207 };
208 }
209 if let Some(next_table) = item.get_ref().as_table() {
210 table = next_table;
211 }
212 if iter.peek().is_some() {
213 if let Some(array) = item.get_ref().as_array() {
214 let next = iter.next().unwrap();
215 return array.iter().find_map(|item| match item.get_ref() {
216 toml::de::DeValue::String(s) if s == next => Some(item.span()),
217 _ => None,
218 });
219 }
220 }
221 }
222 None
223}
224
225pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
228 diff_paths(path, gctx.cwd())
229 .unwrap_or_else(|| path.to_path_buf())
230 .display()
231 .to_string()
232}
233
234#[derive(Copy, Clone, Debug)]
235pub struct LintGroup {
236 pub name: &'static str,
237 pub default_level: LintLevel,
238 pub desc: &'static str,
239 pub edition_lint_opts: Option<(Edition, LintLevel)>,
240 pub feature_gate: Option<&'static Feature>,
241}
242
243const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
245 name: "test_dummy_unstable",
246 desc: "test_dummy_unstable is meant to only be used in tests",
247 default_level: LintLevel::Allow,
248 edition_lint_opts: None,
249 feature_gate: Some(Feature::test_dummy_unstable()),
250};
251
252#[derive(Copy, Clone, Debug)]
253pub struct Lint {
254 pub name: &'static str,
255 pub desc: &'static str,
256 pub groups: &'static [LintGroup],
257 pub default_level: LintLevel,
258 pub edition_lint_opts: Option<(Edition, LintLevel)>,
259 pub feature_gate: Option<&'static Feature>,
260 pub docs: Option<&'static str>,
264}
265
266impl Lint {
267 pub fn level(
268 &self,
269 pkg_lints: &TomlToolLints,
270 edition: Edition,
271 unstable_features: &Features,
272 ) -> (LintLevel, LintLevelReason) {
273 if self
276 .feature_gate
277 .is_some_and(|f| !unstable_features.is_enabled(f))
278 {
279 return (LintLevel::Allow, LintLevelReason::Default);
280 }
281
282 self.groups
283 .iter()
284 .map(|g| {
285 (
286 g.name,
287 level_priority(
288 g.name,
289 g.default_level,
290 g.edition_lint_opts,
291 pkg_lints,
292 edition,
293 ),
294 )
295 })
296 .chain(std::iter::once((
297 self.name,
298 level_priority(
299 self.name,
300 self.default_level,
301 self.edition_lint_opts,
302 pkg_lints,
303 edition,
304 ),
305 )))
306 .max_by_key(|(n, (l, _, p))| (l == &LintLevel::Forbid, *p, std::cmp::Reverse(*n)))
307 .map(|(_, (l, r, _))| (l, r))
308 .unwrap()
309 }
310
311 fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
312 format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
313 }
314}
315
316#[derive(Copy, Clone, Debug, PartialEq)]
317pub enum LintLevel {
318 Allow,
319 Warn,
320 Deny,
321 Forbid,
322}
323
324impl Display for LintLevel {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 match self {
327 LintLevel::Allow => write!(f, "allow"),
328 LintLevel::Warn => write!(f, "warn"),
329 LintLevel::Deny => write!(f, "deny"),
330 LintLevel::Forbid => write!(f, "forbid"),
331 }
332 }
333}
334
335impl LintLevel {
336 pub fn is_error(&self) -> bool {
337 self == &LintLevel::Forbid || self == &LintLevel::Deny
338 }
339
340 pub fn to_diagnostic_level(self) -> Level {
341 match self {
342 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
343 LintLevel::Warn => Level::Warning,
344 LintLevel::Deny => Level::Error,
345 LintLevel::Forbid => Level::Error,
346 }
347 }
348}
349
350impl From<TomlLintLevel> for LintLevel {
351 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
352 match toml_lint_level {
353 TomlLintLevel::Allow => LintLevel::Allow,
354 TomlLintLevel::Warn => LintLevel::Warn,
355 TomlLintLevel::Deny => LintLevel::Deny,
356 TomlLintLevel::Forbid => LintLevel::Forbid,
357 }
358 }
359}
360
361#[derive(Copy, Clone, Debug, PartialEq, Eq)]
362pub enum LintLevelReason {
363 Default,
364 Edition(Edition),
365 Package,
366}
367
368impl Display for LintLevelReason {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 match self {
371 LintLevelReason::Default => write!(f, "by default"),
372 LintLevelReason::Edition(edition) => write!(f, "in edition {}", edition),
373 LintLevelReason::Package => write!(f, "in `[lints]`"),
374 }
375 }
376}
377
378impl LintLevelReason {
379 fn is_user_specified(&self) -> bool {
380 match self {
381 LintLevelReason::Default => false,
382 LintLevelReason::Edition(_) => false,
383 LintLevelReason::Package => true,
384 }
385 }
386}
387
388fn level_priority(
389 name: &str,
390 default_level: LintLevel,
391 edition_lint_opts: Option<(Edition, LintLevel)>,
392 pkg_lints: &TomlToolLints,
393 edition: Edition,
394) -> (LintLevel, LintLevelReason, i8) {
395 let (unspecified_level, reason) = if let Some(level) = edition_lint_opts
396 .filter(|(e, _)| edition >= *e)
397 .map(|(_, l)| l)
398 {
399 (level, LintLevelReason::Edition(edition))
400 } else {
401 (default_level, LintLevelReason::Default)
402 };
403
404 if unspecified_level == LintLevel::Forbid {
406 return (unspecified_level, reason, 0);
407 }
408
409 if let Some(defined_level) = pkg_lints.get(name) {
410 (
411 defined_level.level().into(),
412 LintLevelReason::Package,
413 defined_level.priority(),
414 )
415 } else {
416 (unspecified_level, reason, 0)
417 }
418}
419
420const IM_A_TEAPOT: Lint = Lint {
422 name: "im_a_teapot",
423 desc: "`im_a_teapot` is specified",
424 groups: &[TEST_DUMMY_UNSTABLE],
425 default_level: LintLevel::Allow,
426 edition_lint_opts: None,
427 feature_gate: Some(Feature::test_dummy_unstable()),
428 docs: None,
429};
430
431pub fn check_im_a_teapot(
432 pkg: &Package,
433 path: &Path,
434 pkg_lints: &TomlToolLints,
435 error_count: &mut usize,
436 gctx: &GlobalContext,
437) -> CargoResult<()> {
438 let manifest = pkg.manifest();
439 let (lint_level, reason) =
440 IM_A_TEAPOT.level(pkg_lints, manifest.edition(), manifest.unstable_features());
441
442 if lint_level == LintLevel::Allow {
443 return Ok(());
444 }
445
446 if manifest
447 .normalized_toml()
448 .package()
449 .is_some_and(|p| p.im_a_teapot.is_some())
450 {
451 if lint_level.is_error() {
452 *error_count += 1;
453 }
454 let level = lint_level.to_diagnostic_level();
455 let manifest_path = rel_cwd_manifest_path(path, gctx);
456 let emitted_reason = IM_A_TEAPOT.emitted_source(lint_level, reason);
457
458 let key_span = get_span(manifest.document(), &["package", "im-a-teapot"], false).unwrap();
459 let value_span = get_span(manifest.document(), &["package", "im-a-teapot"], true).unwrap();
460 let message = level
461 .title(IM_A_TEAPOT.desc)
462 .snippet(
463 Snippet::source(manifest.contents())
464 .origin(&manifest_path)
465 .annotation(level.span(key_span.start..value_span.end))
466 .fold(true),
467 )
468 .footer(Level::Note.title(&emitted_reason));
469
470 gctx.shell().print_message(message)?;
471 }
472 Ok(())
473}
474
475const UNKNOWN_LINTS: Lint = Lint {
476 name: "unknown_lints",
477 desc: "unknown lint",
478 groups: &[],
479 default_level: LintLevel::Warn,
480 edition_lint_opts: None,
481 feature_gate: None,
482 docs: Some(
483 r#"
484### What it does
485Checks for unknown lints in the `[lints.cargo]` table
486
487### Why it is bad
488- The lint name could be misspelled, leading to confusion as to why it is
489 not working as expected
490- The unknown lint could end up causing an error if `cargo` decides to make
491 a lint with the same name in the future
492
493### Example
494```toml
495[lints.cargo]
496this-lint-does-not-exist = "warn"
497```
498"#,
499 ),
500};
501
502fn output_unknown_lints(
503 unknown_lints: Vec<&String>,
504 manifest: &Manifest,
505 manifest_path: &str,
506 pkg_lints: &TomlToolLints,
507 ws_contents: &str,
508 ws_document: &toml::Spanned<toml::de::DeTable<'static>>,
509 ws_path: &str,
510 error_count: &mut usize,
511 gctx: &GlobalContext,
512) -> CargoResult<()> {
513 let (lint_level, reason) =
514 UNKNOWN_LINTS.level(pkg_lints, manifest.edition(), manifest.unstable_features());
515 if lint_level == LintLevel::Allow {
516 return Ok(());
517 }
518
519 let level = lint_level.to_diagnostic_level();
520 let mut emitted_source = None;
521 for lint_name in unknown_lints {
522 if lint_level.is_error() {
523 *error_count += 1;
524 }
525 let title = format!("{}: `{lint_name}`", UNKNOWN_LINTS.desc);
526 let second_title = format!("`cargo::{}` was inherited", lint_name);
527 let underscore_lint_name = lint_name.replace("-", "_");
528 let matching = if let Some(lint) = LINTS.iter().find(|l| l.name == underscore_lint_name) {
529 Some((lint.name, "lint"))
530 } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == underscore_lint_name) {
531 Some((group.name, "group"))
532 } else {
533 None
534 };
535 let help =
536 matching.map(|(name, kind)| format!("there is a {kind} with a similar name: `{name}`"));
537
538 let mut footers = Vec::new();
539 if emitted_source.is_none() {
540 emitted_source = Some(UNKNOWN_LINTS.emitted_source(lint_level, reason));
541 footers.push(Level::Note.title(emitted_source.as_ref().unwrap()));
542 }
543
544 let mut message = if let Some(span) =
545 get_span(manifest.document(), &["lints", "cargo", lint_name], false)
546 {
547 level.title(&title).snippet(
548 Snippet::source(manifest.contents())
549 .origin(&manifest_path)
550 .annotation(Level::Error.span(span))
551 .fold(true),
552 )
553 } else {
554 let lint_span = get_span(
555 ws_document,
556 &["workspace", "lints", "cargo", lint_name],
557 false,
558 )
559 .unwrap_or_else(|| {
560 panic!("could not find `cargo::{lint_name}` in `[lints]`, or `[workspace.lints]` ")
561 });
562
563 let inherited_note = if let (Some(inherit_span_key), Some(inherit_span_value)) = (
564 get_span(manifest.document(), &["lints", "workspace"], false),
565 get_span(manifest.document(), &["lints", "workspace"], true),
566 ) {
567 Level::Note.title(&second_title).snippet(
568 Snippet::source(manifest.contents())
569 .origin(&manifest_path)
570 .annotation(
571 Level::Note.span(inherit_span_key.start..inherit_span_value.end),
572 )
573 .fold(true),
574 )
575 } else {
576 Level::Note.title(&second_title)
577 };
578 footers.push(inherited_note);
579
580 level.title(&title).snippet(
581 Snippet::source(ws_contents)
582 .origin(&ws_path)
583 .annotation(Level::Error.span(lint_span))
584 .fold(true),
585 )
586 };
587
588 if let Some(help) = help.as_ref() {
589 footers.push(Level::Help.title(help));
590 }
591 for footer in footers {
592 message = message.footer(footer);
593 }
594
595 gctx.shell().print_message(message)?;
596 }
597
598 Ok(())
599}
600
601#[cfg(test)]
602mod tests {
603 use itertools::Itertools;
604 use snapbox::ToDebug;
605 use std::collections::HashSet;
606
607 #[test]
608 fn ensure_sorted_lints() {
609 let location = std::panic::Location::caller();
611 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
612
613 let actual = super::LINTS
614 .iter()
615 .map(|l| l.name.to_uppercase())
616 .collect::<Vec<_>>();
617
618 let mut expected = actual.clone();
619 expected.sort();
620 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
621 }
622
623 #[test]
624 fn ensure_sorted_lint_groups() {
625 let location = std::panic::Location::caller();
627 println!(
628 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
629 location.file(),
630 );
631 let actual = super::LINT_GROUPS
632 .iter()
633 .map(|l| l.name.to_uppercase())
634 .collect::<Vec<_>>();
635
636 let mut expected = actual.clone();
637 expected.sort();
638 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
639 }
640
641 #[test]
642 fn ensure_updated_lints() {
643 let path = snapbox::utils::current_rs!();
644 let expected = std::fs::read_to_string(&path).unwrap();
645 let expected = expected
646 .lines()
647 .filter_map(|l| {
648 if l.ends_with(": Lint = Lint {") {
649 Some(
650 l.chars()
651 .skip(6)
652 .take_while(|c| *c != ':')
653 .collect::<String>(),
654 )
655 } else {
656 None
657 }
658 })
659 .collect::<HashSet<_>>();
660 let actual = super::LINTS
661 .iter()
662 .map(|l| l.name.to_uppercase())
663 .collect::<HashSet<_>>();
664 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
665
666 let mut need_added = String::new();
667 for name in &diff {
668 need_added.push_str(&format!("{}\n", name));
669 }
670 assert!(
671 diff.is_empty(),
672 "\n`LINTS` did not contain all `Lint`s found in {}\n\
673 Please add the following to `LINTS`:\n\
674 {}",
675 path.display(),
676 need_added
677 );
678 }
679
680 #[test]
681 fn ensure_updated_lint_groups() {
682 let path = snapbox::utils::current_rs!();
683 let expected = std::fs::read_to_string(&path).unwrap();
684 let expected = expected
685 .lines()
686 .filter_map(|l| {
687 if l.ends_with(": LintGroup = LintGroup {") {
688 Some(
689 l.chars()
690 .skip(6)
691 .take_while(|c| *c != ':')
692 .collect::<String>(),
693 )
694 } else {
695 None
696 }
697 })
698 .collect::<HashSet<_>>();
699 let actual = super::LINT_GROUPS
700 .iter()
701 .map(|l| l.name.to_uppercase())
702 .collect::<HashSet<_>>();
703 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
704
705 let mut need_added = String::new();
706 for name in &diff {
707 need_added.push_str(&format!("{}\n", name));
708 }
709 assert!(
710 diff.is_empty(),
711 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
712 Please add the following to `LINT_GROUPS`:\n\
713 {}",
714 path.display(),
715 need_added
716 );
717 }
718}