1use std::borrow::Cow;
2use std::cmp::{Reverse, max_by_key};
3use std::fmt::Display;
4use std::ops::Range;
5use std::path::Path;
6
7use cargo_util_schemas::manifest::RustVersion;
8use cargo_util_schemas::manifest::TomlLintLevel;
9use cargo_util_schemas::manifest::TomlToolLints;
10use cargo_util_terminal::report::AnnotationKind;
11use cargo_util_terminal::report::Group;
12use cargo_util_terminal::report::Level;
13use cargo_util_terminal::report::Snippet;
14use pathdiff::diff_paths;
15
16use crate::core::Workspace;
17use crate::core::{Edition, Feature, Features, MaybePackage, Package};
18use crate::{CargoResult, GlobalContext};
19
20pub mod rules;
21pub use rules::LINTS;
22
23pub static LINT_GROUPS: &[LintGroup] = &[
24 COMPLEXITY,
25 CORRECTNESS,
26 NURSERY,
27 PEDANTIC,
28 PERF,
29 RESTRICTION,
30 STYLE,
31 SUSPICIOUS,
32 TEST_DUMMY_UNSTABLE,
33];
34
35pub enum ManifestFor<'a> {
37 Package(&'a Package),
39 Workspace {
41 ws: &'a Workspace<'a>,
42 maybe_pkg: &'a MaybePackage,
43 },
44}
45
46impl ManifestFor<'_> {
47 fn lint_level(&self, pkg_lints: &TomlToolLints, lint: &Lint) -> (LintLevel, LintLevelSource) {
48 lint.level(pkg_lints, self.rust_version(), self.unstable_features())
49 }
50
51 pub fn rust_version(&self) -> Option<&RustVersion> {
52 match self {
53 ManifestFor::Package(p) => p.rust_version(),
54 ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(),
55 }
56 }
57
58 pub fn contents(&self) -> Option<&str> {
59 match self {
60 ManifestFor::Package(p) => p.manifest().contents(),
61 ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(),
62 }
63 }
64
65 pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
66 match self {
67 ManifestFor::Package(p) => p.manifest().document(),
68 ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(),
69 }
70 }
71
72 pub fn edition(&self) -> Edition {
73 match self {
74 ManifestFor::Package(p) => p.manifest().edition(),
75 ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(),
76 }
77 }
78
79 pub fn unstable_features(&self) -> &Features {
80 match self {
81 ManifestFor::Package(p) => p.manifest().unstable_features(),
82 ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(),
83 }
84 }
85}
86
87impl<'a> From<&'a Package> for ManifestFor<'a> {
88 fn from(value: &'a Package) -> ManifestFor<'a> {
89 ManifestFor::Package(value)
90 }
91}
92
93impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
94 fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> {
95 ManifestFor::Workspace { ws, maybe_pkg }
96 }
97}
98
99pub fn analyze_cargo_lints_table(
100 manifest: ManifestFor<'_>,
101 manifest_path: &Path,
102 cargo_lints: &TomlToolLints,
103 error_count: &mut usize,
104 gctx: &GlobalContext,
105) -> CargoResult<()> {
106 let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
107 let mut unknown_lints = Vec::new();
108 for lint_name in cargo_lints.keys().map(|name| name) {
109 let Some((name, default_level, feature_gate)) = find_lint_or_group(lint_name) else {
110 unknown_lints.push(lint_name);
111 continue;
112 };
113
114 let (_, source, _) = level_priority(name, *default_level, cargo_lints);
115
116 if !source.is_user_specified() {
118 continue;
119 }
120
121 if let Some(feature_gate) = feature_gate
123 && !manifest.unstable_features().is_enabled(feature_gate)
124 {
125 report_feature_not_enabled(
126 name,
127 feature_gate,
128 &manifest,
129 &manifest_path,
130 error_count,
131 gctx,
132 )?;
133 }
134 }
135
136 rules::output_unknown_lints(
137 unknown_lints,
138 &manifest,
139 &manifest_path,
140 cargo_lints,
141 error_count,
142 gctx,
143 )?;
144
145 Ok(())
146}
147
148fn find_lint_or_group<'a>(
149 name: &str,
150) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> {
151 if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
152 Some((
153 lint.name,
154 &lint.primary_group.default_level,
155 &lint.feature_gate,
156 ))
157 } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
158 Some((group.name, &group.default_level, &group.feature_gate))
159 } else {
160 None
161 }
162}
163
164fn report_feature_not_enabled(
165 lint_name: &str,
166 feature_gate: &Feature,
167 manifest: &ManifestFor<'_>,
168 manifest_path: &str,
169 error_count: &mut usize,
170 gctx: &GlobalContext,
171) -> CargoResult<()> {
172 let dash_feature_name = feature_gate.name().replace("_", "-");
173 let title = format!("use of unstable lint `{}`", lint_name);
174 let label = format!(
175 "this is behind `{}`, which is not enabled",
176 dash_feature_name
177 );
178 let help = format!(
179 "consider adding `cargo-features = [\"{}\"]` to the top of the manifest",
180 dash_feature_name
181 );
182
183 let key_path = match manifest {
184 ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
185 ManifestFor::Workspace { .. } => &["workspace", "lints", "cargo", lint_name][..],
186 };
187
188 let mut error = Group::with_title(Level::ERROR.primary_title(title));
189
190 if let Some(document) = manifest.document()
191 && let Some(contents) = manifest.contents()
192 {
193 let Some(span) = get_key_value_span(document, key_path) else {
194 return Ok(());
196 };
197
198 error = error.element(
199 Snippet::source(contents)
200 .path(manifest_path)
201 .annotation(AnnotationKind::Primary.span(span.key).label(label)),
202 )
203 }
204
205 let report = [error.element(Level::HELP.message(help))];
206
207 *error_count += 1;
208 gctx.shell().print_report(&report, true)?;
209
210 Ok(())
211}
212
213#[derive(Clone)]
214pub struct TomlSpan {
215 pub key: Range<usize>,
216 pub value: Range<usize>,
217}
218
219#[derive(Copy, Clone)]
220pub enum TomlIndex<'i> {
221 Key(&'i str),
222 Offset(usize),
223}
224
225impl<'i> TomlIndex<'i> {
226 fn as_key(&self) -> Option<&'i str> {
227 match self {
228 TomlIndex::Key(key) => Some(key),
229 TomlIndex::Offset(_) => None,
230 }
231 }
232}
233
234pub trait AsIndex {
235 fn as_index<'i>(&'i self) -> TomlIndex<'i>;
236}
237
238impl AsIndex for TomlIndex<'_> {
239 fn as_index<'i>(&'i self) -> TomlIndex<'i> {
240 match self {
241 TomlIndex::Key(key) => TomlIndex::Key(key),
242 TomlIndex::Offset(offset) => TomlIndex::Offset(*offset),
243 }
244 }
245}
246
247impl AsIndex for &str {
248 fn as_index<'i>(&'i self) -> TomlIndex<'i> {
249 TomlIndex::Key(self)
250 }
251}
252
253impl AsIndex for String {
254 fn as_index<'i>(&'i self) -> TomlIndex<'i> {
255 TomlIndex::Key(self.as_str())
256 }
257}
258
259impl AsIndex for usize {
260 fn as_index<'i>(&'i self) -> TomlIndex<'i> {
261 TomlIndex::Offset(*self)
262 }
263}
264
265pub fn get_key_value<'doc, 'i>(
266 document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
267 path: &[impl AsIndex],
268) -> Option<(
269 &'doc toml::Spanned<Cow<'doc, str>>,
270 &'doc toml::Spanned<toml::de::DeValue<'static>>,
271)> {
272 let table = document.get_ref();
273 let mut iter = path.into_iter();
274 let index0 = iter.next()?.as_index();
275 let key0 = index0.as_key()?;
276 let (mut current_key, mut current_item) = table.get_key_value(key0)?;
277
278 while let Some(index) = iter.next() {
279 match index.as_index() {
280 TomlIndex::Key(key) => {
281 if let Some(table) = current_item.get_ref().as_table() {
282 (current_key, current_item) = table.get_key_value(key)?;
283 } else if let Some(array) = current_item.get_ref().as_array() {
284 current_item = array.iter().find(|item| match item.get_ref() {
285 toml::de::DeValue::String(s) => s == key,
286 _ => false,
287 })?;
288 } else {
289 return None;
290 }
291 }
292 TomlIndex::Offset(offset) => {
293 let array = current_item.get_ref().as_array()?;
294 current_item = array.get(offset)?;
295 }
296 }
297 }
298 Some((current_key, current_item))
299}
300
301pub fn get_key_value_span<'i>(
302 document: &toml::Spanned<toml::de::DeTable<'static>>,
303 path: &[impl AsIndex],
304) -> Option<TomlSpan> {
305 get_key_value(document, path).map(|(k, v)| TomlSpan {
306 key: k.span(),
307 value: v.span(),
308 })
309}
310
311pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
314 diff_paths(path, gctx.cwd())
315 .unwrap_or_else(|| path.to_path_buf())
316 .display()
317 .to_string()
318}
319
320#[derive(Clone, Debug)]
321pub struct LintGroup {
322 pub name: &'static str,
323 pub default_level: LintLevel,
324 pub desc: &'static str,
325 pub feature_gate: Option<&'static Feature>,
326 pub hidden: bool,
327}
328
329const COMPLEXITY: LintGroup = LintGroup {
330 name: "complexity",
331 desc: "code that does something simple but in a complex way",
332 default_level: LintLevel::Warn,
333 feature_gate: None,
334 hidden: false,
335};
336
337const CORRECTNESS: LintGroup = LintGroup {
338 name: "correctness",
339 desc: "code that is outright wrong or useless",
340 default_level: LintLevel::Deny,
341 feature_gate: None,
342 hidden: false,
343};
344
345const NURSERY: LintGroup = LintGroup {
346 name: "nursery",
347 desc: "new lints that are still under development",
348 default_level: LintLevel::Allow,
349 feature_gate: None,
350 hidden: false,
351};
352
353const PEDANTIC: LintGroup = LintGroup {
354 name: "pedantic",
355 desc: "lints which are rather strict or have occasional false positives",
356 default_level: LintLevel::Allow,
357 feature_gate: None,
358 hidden: false,
359};
360
361const PERF: LintGroup = LintGroup {
362 name: "perf",
363 desc: "code that can be written to run faster",
364 default_level: LintLevel::Warn,
365 feature_gate: None,
366 hidden: false,
367};
368
369const RESTRICTION: LintGroup = LintGroup {
370 name: "restriction",
371 desc: "lints which prevent the use of Cargo features",
372 default_level: LintLevel::Allow,
373 feature_gate: None,
374 hidden: false,
375};
376
377const STYLE: LintGroup = LintGroup {
378 name: "style",
379 desc: "code that should be written in a more idiomatic way",
380 default_level: LintLevel::Warn,
381 feature_gate: None,
382 hidden: false,
383};
384
385const SUSPICIOUS: LintGroup = LintGroup {
386 name: "suspicious",
387 desc: "code that is most likely wrong or useless",
388 default_level: LintLevel::Warn,
389 feature_gate: None,
390 hidden: false,
391};
392
393const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
395 name: "test_dummy_unstable",
396 desc: "test_dummy_unstable is meant to only be used in tests",
397 default_level: LintLevel::Allow,
398 feature_gate: Some(Feature::test_dummy_unstable()),
399 hidden: true,
400};
401
402#[derive(Clone, Debug)]
403pub struct Lint {
404 pub name: &'static str,
405 pub desc: &'static str,
406 pub primary_group: &'static LintGroup,
407 pub msrv: Option<RustVersion>,
413 pub feature_gate: Option<&'static Feature>,
414 pub docs: Option<&'static str>,
418}
419
420impl Lint {
421 pub fn level(
422 &self,
423 pkg_lints: &TomlToolLints,
424 pkg_rust_version: Option<&RustVersion>,
425 unstable_features: &Features,
426 ) -> (LintLevel, LintLevelSource) {
427 if self
430 .feature_gate
431 .is_some_and(|f| !unstable_features.is_enabled(f))
432 {
433 return (LintLevel::Allow, LintLevelSource::Default);
434 }
435
436 if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
437 let pkg_rust_version = pkg_rust_version.to_partial();
438 if !msrv.is_compatible_with(&pkg_rust_version) {
439 return (LintLevel::Allow, LintLevelSource::Default);
440 }
441 }
442
443 let lint_level_priority =
444 level_priority(self.name, self.primary_group.default_level, pkg_lints);
445
446 let group_level_priority = level_priority(
447 self.primary_group.name,
448 self.primary_group.default_level,
449 pkg_lints,
450 );
451
452 let (_, (l, s, _)) = max_by_key(
453 (self.name, lint_level_priority),
454 (self.primary_group.name, group_level_priority),
455 |(n, (l, s, p))| {
456 (
457 l == &LintLevel::Forbid,
458 *s != LintLevelSource::Default,
459 *p,
460 Reverse(*n),
461 )
462 },
463 );
464 (l, s)
465 }
466
467 pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
468 format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
469 }
470}
471
472#[derive(Copy, Clone, Debug, PartialEq)]
473pub enum LintLevel {
474 Allow,
475 Warn,
476 Deny,
477 Forbid,
478}
479
480impl Display for LintLevel {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 match self {
483 LintLevel::Allow => write!(f, "allow"),
484 LintLevel::Warn => write!(f, "warn"),
485 LintLevel::Deny => write!(f, "deny"),
486 LintLevel::Forbid => write!(f, "forbid"),
487 }
488 }
489}
490
491impl LintLevel {
492 pub fn is_warn(&self) -> bool {
493 self == &LintLevel::Warn
494 }
495
496 pub fn is_error(&self) -> bool {
497 self == &LintLevel::Forbid || self == &LintLevel::Deny
498 }
499
500 pub fn to_diagnostic_level(self) -> Level<'static> {
501 match self {
502 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
503 LintLevel::Warn => Level::WARNING,
504 LintLevel::Deny => Level::ERROR,
505 LintLevel::Forbid => Level::ERROR,
506 }
507 }
508
509 pub fn force(self) -> bool {
510 match self {
511 Self::Allow => false,
512 Self::Warn => true,
513 Self::Deny => true,
514 Self::Forbid => true,
515 }
516 }
517}
518
519impl From<TomlLintLevel> for LintLevel {
520 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
521 match toml_lint_level {
522 TomlLintLevel::Allow => LintLevel::Allow,
523 TomlLintLevel::Warn => LintLevel::Warn,
524 TomlLintLevel::Deny => LintLevel::Deny,
525 TomlLintLevel::Forbid => LintLevel::Forbid,
526 }
527 }
528}
529
530#[derive(Copy, Clone, Debug, PartialEq, Eq)]
531pub enum LintLevelSource {
532 Default,
533 Package,
534}
535
536impl Display for LintLevelSource {
537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538 match self {
539 LintLevelSource::Default => write!(f, "by default"),
540 LintLevelSource::Package => write!(f, "in `[lints]`"),
541 }
542 }
543}
544
545impl LintLevelSource {
546 fn is_user_specified(&self) -> bool {
547 match self {
548 LintLevelSource::Default => false,
549 LintLevelSource::Package => true,
550 }
551 }
552}
553
554fn level_priority(
555 name: &str,
556 default_level: LintLevel,
557 pkg_lints: &TomlToolLints,
558) -> (LintLevel, LintLevelSource, i8) {
559 if let Some(defined_level) = pkg_lints.get(name) {
560 (
561 defined_level.level().into(),
562 LintLevelSource::Package,
563 defined_level.priority(),
564 )
565 } else {
566 (default_level, LintLevelSource::Default, 0)
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use itertools::Itertools;
573 use snapbox::ToDebug;
574 use std::collections::HashSet;
575
576 use super::*;
577
578 fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
579 Lint {
580 name,
581 desc: "test lint",
582 primary_group: group,
583 msrv: None,
584 feature_gate: None,
585 docs: None,
586 }
587 }
588
589 #[test]
590 fn lint_level_prefers_user_specified_over_default() {
591 let lint = test_lint("unused_dependencies", &STYLE);
592
593 let mut pkg_lints = TomlToolLints::new();
594 pkg_lints.insert(
595 "unused_dependencies".to_string(),
596 cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
597 );
598 let features = Features::default();
599
600 let (level, source) = lint.level(&pkg_lints, None, &features);
601 assert_eq!(level, LintLevel::Deny);
602 assert_eq!(source, LintLevelSource::Package);
603 }
604
605 #[test]
606 fn lint_level_group_overrides_default() {
607 let lint = test_lint("non_kebab_case_bins", &STYLE);
608
609 let mut pkg_lints = TomlToolLints::new();
610 pkg_lints.insert(
611 "style".to_string(),
612 cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
613 );
614 let features = Features::default();
615
616 let (level, source) = lint.level(&pkg_lints, None, &features);
617 assert_eq!(level, LintLevel::Deny);
618 assert_eq!(source, LintLevelSource::Package);
619 }
620
621 #[test]
622 fn ensure_lint_groups_do_not_default_to_forbid() {
623 let forbid_groups = super::LINT_GROUPS
624 .iter()
625 .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
626 .collect::<Vec<_>>();
627
628 assert!(
629 forbid_groups.is_empty(),
630 "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
631 {}\n",
632 forbid_groups.iter().map(|g| g.name).join("\n")
633 );
634 }
635
636 #[test]
637 fn ensure_sorted_lints() {
638 let location = std::panic::Location::caller();
640 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
641
642 let actual = super::LINTS
643 .iter()
644 .map(|l| l.name.to_uppercase())
645 .collect::<Vec<_>>();
646
647 let mut expected = actual.clone();
648 expected.sort();
649 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
650 }
651
652 #[test]
653 fn ensure_sorted_lint_groups() {
654 let location = std::panic::Location::caller();
656 println!(
657 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
658 location.file(),
659 );
660 let actual = super::LINT_GROUPS
661 .iter()
662 .map(|l| l.name.to_uppercase())
663 .collect::<Vec<_>>();
664
665 let mut expected = actual.clone();
666 expected.sort();
667 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
668 }
669
670 #[test]
671 fn ensure_updated_lints() {
672 let dir = snapbox::utils::current_dir!().join("rules");
673 let mut expected = HashSet::new();
674 for entry in std::fs::read_dir(&dir).unwrap() {
675 let entry = entry.unwrap();
676 let path = entry.path();
677 if path.ends_with("mod.rs") {
678 continue;
679 }
680 let lint_name = path.file_stem().unwrap().to_string_lossy();
681 assert!(expected.insert(lint_name.into()), "duplicate lint found");
682 }
683
684 let actual = super::LINTS
685 .iter()
686 .map(|l| l.name.to_string())
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!("{name}\n"));
693 }
694 assert!(
695 diff.is_empty(),
696 "\n`LINTS` did not contain all `Lint`s found in {}\n\
697 Please add the following to `LINTS`:\n\
698 {need_added}",
699 dir.display(),
700 );
701 }
702
703 #[test]
704 fn ensure_updated_lint_groups() {
705 let path = snapbox::utils::current_rs!();
706 let expected = std::fs::read_to_string(&path).unwrap();
707 let expected = expected
708 .lines()
709 .filter_map(|l| {
710 if l.ends_with(": LintGroup = LintGroup {") {
711 Some(
712 l.chars()
713 .skip(6)
714 .take_while(|c| *c != ':')
715 .collect::<String>(),
716 )
717 } else {
718 None
719 }
720 })
721 .collect::<HashSet<_>>();
722 let actual = super::LINT_GROUPS
723 .iter()
724 .map(|l| l.name.to_uppercase())
725 .collect::<HashSet<_>>();
726 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
727
728 let mut need_added = String::new();
729 for name in &diff {
730 need_added.push_str(&format!("{}\n", name));
731 }
732 assert!(
733 diff.is_empty(),
734 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
735 Please add the following to `LINT_GROUPS`:\n\
736 {}",
737 path.display(),
738 need_added
739 );
740 }
741}