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