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;
9use toml_edit::ImDocument;
10
11const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
12pub const LINTS: &[Lint] = &[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: &ImDocument<String>,
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: &ImDocument<String>,
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 message = if let Some(span) =
138 get_span(manifest.document(), &["lints", "cargo", lint_name], false)
139 {
140 Level::Error
141 .title(&title)
142 .snippet(
143 Snippet::source(manifest.contents())
144 .origin(&manifest_path)
145 .annotation(Level::Error.span(span).label(&label))
146 .fold(true),
147 )
148 .footer(Level::Help.title(&help))
149 } else {
150 let lint_span = get_span(
151 ws_document,
152 &["workspace", "lints", "cargo", lint_name],
153 false,
154 )
155 .expect(&format!(
156 "could not find `cargo::{lint_name}` in `[lints]`, or `[workspace.lints]` "
157 ));
158
159 let inherited_note = if let (Some(inherit_span_key), Some(inherit_span_value)) = (
160 get_span(manifest.document(), &["lints", "workspace"], false),
161 get_span(manifest.document(), &["lints", "workspace"], true),
162 ) {
163 Level::Note.title(&second_title).snippet(
164 Snippet::source(manifest.contents())
165 .origin(&manifest_path)
166 .annotation(
167 Level::Note.span(inherit_span_key.start..inherit_span_value.end),
168 )
169 .fold(true),
170 )
171 } else {
172 Level::Note.title(&second_title)
173 };
174
175 Level::Error
176 .title(&title)
177 .snippet(
178 Snippet::source(ws_contents)
179 .origin(&ws_path)
180 .annotation(Level::Error.span(lint_span).label(&label))
181 .fold(true),
182 )
183 .footer(inherited_note)
184 .footer(Level::Help.title(&help))
185 };
186
187 *error_count += 1;
188 gctx.shell().print_message(message)?;
189 }
190 Ok(())
191}
192
193pub fn get_span(
194 document: &ImDocument<String>,
195 path: &[&str],
196 get_value: bool,
197) -> Option<Range<usize>> {
198 let mut table = document.as_item().as_table_like()?;
199 let mut iter = path.into_iter().peekable();
200 while let Some(key) = iter.next() {
201 let (key, item) = table.get_key_value(key)?;
202 if iter.peek().is_none() {
203 return if get_value {
204 item.span()
205 } else {
206 let leaf_decor = key.dotted_decor();
207 let leaf_prefix_span = leaf_decor.prefix().and_then(|p| p.span());
208 let leaf_suffix_span = leaf_decor.suffix().and_then(|s| s.span());
209 if let (Some(leaf_prefix_span), Some(leaf_suffix_span)) =
210 (leaf_prefix_span, leaf_suffix_span)
211 {
212 Some(leaf_prefix_span.start..leaf_suffix_span.end)
213 } else {
214 key.span()
215 }
216 };
217 }
218 if item.is_table_like() {
219 table = item.as_table_like().unwrap();
220 }
221 if item.is_array() && iter.peek().is_some() {
222 let array = item.as_array().unwrap();
223 let next = iter.next().unwrap();
224 return array.iter().find_map(|item| {
225 if next == &item.to_string() {
226 item.span()
227 } else {
228 None
229 }
230 });
231 }
232 }
233 None
234}
235
236pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
239 diff_paths(path, gctx.cwd())
240 .unwrap_or_else(|| path.to_path_buf())
241 .display()
242 .to_string()
243}
244
245#[derive(Copy, Clone, Debug)]
246pub struct LintGroup {
247 pub name: &'static str,
248 pub default_level: LintLevel,
249 pub desc: &'static str,
250 pub edition_lint_opts: Option<(Edition, LintLevel)>,
251 pub feature_gate: Option<&'static Feature>,
252}
253
254const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
256 name: "test_dummy_unstable",
257 desc: "test_dummy_unstable is meant to only be used in tests",
258 default_level: LintLevel::Allow,
259 edition_lint_opts: None,
260 feature_gate: Some(Feature::test_dummy_unstable()),
261};
262
263#[derive(Copy, Clone, Debug)]
264pub struct Lint {
265 pub name: &'static str,
266 pub desc: &'static str,
267 pub groups: &'static [LintGroup],
268 pub default_level: LintLevel,
269 pub edition_lint_opts: Option<(Edition, LintLevel)>,
270 pub feature_gate: Option<&'static Feature>,
271 pub docs: Option<&'static str>,
275}
276
277impl Lint {
278 pub fn level(
279 &self,
280 pkg_lints: &TomlToolLints,
281 edition: Edition,
282 unstable_features: &Features,
283 ) -> (LintLevel, LintLevelReason) {
284 if self
287 .feature_gate
288 .is_some_and(|f| !unstable_features.is_enabled(f))
289 {
290 return (LintLevel::Allow, LintLevelReason::Default);
291 }
292
293 self.groups
294 .iter()
295 .map(|g| {
296 (
297 g.name,
298 level_priority(
299 g.name,
300 g.default_level,
301 g.edition_lint_opts,
302 pkg_lints,
303 edition,
304 ),
305 )
306 })
307 .chain(std::iter::once((
308 self.name,
309 level_priority(
310 self.name,
311 self.default_level,
312 self.edition_lint_opts,
313 pkg_lints,
314 edition,
315 ),
316 )))
317 .max_by_key(|(n, (l, _, p))| (l == &LintLevel::Forbid, *p, std::cmp::Reverse(*n)))
318 .map(|(_, (l, r, _))| (l, r))
319 .unwrap()
320 }
321}
322
323#[derive(Copy, Clone, Debug, PartialEq)]
324pub enum LintLevel {
325 Allow,
326 Warn,
327 Deny,
328 Forbid,
329}
330
331impl Display for LintLevel {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 match self {
334 LintLevel::Allow => write!(f, "allow"),
335 LintLevel::Warn => write!(f, "warn"),
336 LintLevel::Deny => write!(f, "deny"),
337 LintLevel::Forbid => write!(f, "forbid"),
338 }
339 }
340}
341
342impl LintLevel {
343 pub fn to_diagnostic_level(self) -> Level {
344 match self {
345 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
346 LintLevel::Warn => Level::Warning,
347 LintLevel::Deny => Level::Error,
348 LintLevel::Forbid => Level::Error,
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 == LintLevel::Forbid || lint_level == LintLevel::Deny {
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 = format!(
460 "`cargo::{}` is set to `{lint_level}` {reason}",
461 IM_A_TEAPOT.name
462 );
463
464 let key_span = get_span(manifest.document(), &["package", "im-a-teapot"], false).unwrap();
465 let value_span = get_span(manifest.document(), &["package", "im-a-teapot"], true).unwrap();
466 let message = level
467 .title(IM_A_TEAPOT.desc)
468 .snippet(
469 Snippet::source(manifest.contents())
470 .origin(&manifest_path)
471 .annotation(level.span(key_span.start..value_span.end))
472 .fold(true),
473 )
474 .footer(Level::Note.title(&emitted_reason));
475
476 gctx.shell().print_message(message)?;
477 }
478 Ok(())
479}
480
481const UNKNOWN_LINTS: Lint = Lint {
482 name: "unknown_lints",
483 desc: "unknown lint",
484 groups: &[],
485 default_level: LintLevel::Warn,
486 edition_lint_opts: None,
487 feature_gate: None,
488 docs: Some(
489 r#"
490### What it does
491Checks for unknown lints in the `[lints.cargo]` table
492
493### Why it is bad
494- The lint name could be misspelled, leading to confusion as to why it is
495 not working as expected
496- The unknown lint could end up causing an error if `cargo` decides to make
497 a lint with the same name in the future
498
499### Example
500```toml
501[lints.cargo]
502this-lint-does-not-exist = "warn"
503```
504"#,
505 ),
506};
507
508fn output_unknown_lints(
509 unknown_lints: Vec<&String>,
510 manifest: &Manifest,
511 manifest_path: &str,
512 pkg_lints: &TomlToolLints,
513 ws_contents: &str,
514 ws_document: &ImDocument<String>,
515 ws_path: &str,
516 error_count: &mut usize,
517 gctx: &GlobalContext,
518) -> CargoResult<()> {
519 let (lint_level, reason) =
520 UNKNOWN_LINTS.level(pkg_lints, manifest.edition(), manifest.unstable_features());
521 if lint_level == LintLevel::Allow {
522 return Ok(());
523 }
524
525 let level = lint_level.to_diagnostic_level();
526 let mut emitted_source = None;
527 for lint_name in unknown_lints {
528 if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
529 *error_count += 1;
530 }
531 let title = format!("{}: `{lint_name}`", UNKNOWN_LINTS.desc);
532 let second_title = format!("`cargo::{}` was inherited", lint_name);
533 let underscore_lint_name = lint_name.replace("-", "_");
534 let matching = if let Some(lint) = LINTS.iter().find(|l| l.name == underscore_lint_name) {
535 Some((lint.name, "lint"))
536 } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == underscore_lint_name) {
537 Some((group.name, "group"))
538 } else {
539 None
540 };
541 let help =
542 matching.map(|(name, kind)| format!("there is a {kind} with a similar name: `{name}`"));
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 .expect(&format!(
560 "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
579 level
580 .title(&title)
581 .snippet(
582 Snippet::source(ws_contents)
583 .origin(&ws_path)
584 .annotation(Level::Error.span(lint_span))
585 .fold(true),
586 )
587 .footer(inherited_note)
588 };
589
590 if emitted_source.is_none() {
591 emitted_source = Some(format!(
592 "`cargo::{}` is set to `{lint_level}` {reason}",
593 UNKNOWN_LINTS.name
594 ));
595 message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
596 }
597
598 if let Some(help) = help.as_ref() {
599 message = message.footer(Level::Help.title(help));
600 }
601
602 gctx.shell().print_message(message)?;
603 }
604
605 Ok(())
606}
607
608#[cfg(test)]
609mod tests {
610 use itertools::Itertools;
611 use snapbox::ToDebug;
612 use std::collections::HashSet;
613
614 #[test]
615 fn ensure_sorted_lints() {
616 let location = std::panic::Location::caller();
618 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
619
620 let actual = super::LINTS
621 .iter()
622 .map(|l| l.name.to_uppercase())
623 .collect::<Vec<_>>();
624
625 let mut expected = actual.clone();
626 expected.sort();
627 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
628 }
629
630 #[test]
631 fn ensure_sorted_lint_groups() {
632 let location = std::panic::Location::caller();
634 println!(
635 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
636 location.file(),
637 );
638 let actual = super::LINT_GROUPS
639 .iter()
640 .map(|l| l.name.to_uppercase())
641 .collect::<Vec<_>>();
642
643 let mut expected = actual.clone();
644 expected.sort();
645 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
646 }
647
648 #[test]
649 fn ensure_updated_lints() {
650 let path = snapbox::utils::current_rs!();
651 let expected = std::fs::read_to_string(&path).unwrap();
652 let expected = expected
653 .lines()
654 .filter_map(|l| {
655 if l.ends_with(": Lint = Lint {") {
656 Some(
657 l.chars()
658 .skip(6)
659 .take_while(|c| *c != ':')
660 .collect::<String>(),
661 )
662 } else {
663 None
664 }
665 })
666 .collect::<HashSet<_>>();
667 let actual = super::LINTS
668 .iter()
669 .map(|l| l.name.to_uppercase())
670 .collect::<HashSet<_>>();
671 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
672
673 let mut need_added = String::new();
674 for name in &diff {
675 need_added.push_str(&format!("{}\n", name));
676 }
677 assert!(
678 diff.is_empty(),
679 "\n`LINTS` did not contain all `Lint`s found in {}\n\
680 Please add the following to `LINTS`:\n\
681 {}",
682 path.display(),
683 need_added
684 );
685 }
686
687 #[test]
688 fn ensure_updated_lint_groups() {
689 let path = snapbox::utils::current_rs!();
690 let expected = std::fs::read_to_string(&path).unwrap();
691 let expected = expected
692 .lines()
693 .filter_map(|l| {
694 if l.ends_with(": LintGroup = LintGroup {") {
695 Some(
696 l.chars()
697 .skip(6)
698 .take_while(|c| *c != ':')
699 .collect::<String>(),
700 )
701 } else {
702 None
703 }
704 })
705 .collect::<HashSet<_>>();
706 let actual = super::LINT_GROUPS
707 .iter()
708 .map(|l| l.name.to_uppercase())
709 .collect::<HashSet<_>>();
710 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
711
712 let mut need_added = String::new();
713 for name in &diff {
714 need_added.push_str(&format!("{}\n", name));
715 }
716 assert!(
717 diff.is_empty(),
718 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
719 Please add the following to `LINT_GROUPS`:\n\
720 {}",
721 path.display(),
722 need_added
723 );
724 }
725}