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(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 (_, reason, _) = level_priority(name, *default_level, cargo_lints);
115
116 if !reason.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 usize {
254 fn as_index<'i>(&'i self) -> TomlIndex<'i> {
255 TomlIndex::Offset(*self)
256 }
257}
258
259pub fn get_key_value<'doc, 'i>(
260 document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
261 path: &[impl AsIndex],
262) -> Option<(
263 &'doc toml::Spanned<Cow<'doc, str>>,
264 &'doc toml::Spanned<toml::de::DeValue<'static>>,
265)> {
266 let table = document.get_ref();
267 let mut iter = path.into_iter();
268 let index0 = iter.next()?.as_index();
269 let key0 = index0.as_key()?;
270 let (mut current_key, mut current_item) = table.get_key_value(key0)?;
271
272 while let Some(index) = iter.next() {
273 match index.as_index() {
274 TomlIndex::Key(key) => {
275 if let Some(table) = current_item.get_ref().as_table() {
276 (current_key, current_item) = table.get_key_value(key)?;
277 } else if let Some(array) = current_item.get_ref().as_array() {
278 current_item = array.iter().find(|item| match item.get_ref() {
279 toml::de::DeValue::String(s) => s == key,
280 _ => false,
281 })?;
282 } else {
283 return None;
284 }
285 }
286 TomlIndex::Offset(offset) => {
287 let array = current_item.get_ref().as_array()?;
288 current_item = array.get(offset)?;
289 }
290 }
291 }
292 Some((current_key, current_item))
293}
294
295pub fn get_key_value_span<'i>(
296 document: &toml::Spanned<toml::de::DeTable<'static>>,
297 path: &[impl AsIndex],
298) -> Option<TomlSpan> {
299 get_key_value(document, path).map(|(k, v)| TomlSpan {
300 key: k.span(),
301 value: v.span(),
302 })
303}
304
305pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
308 diff_paths(path, gctx.cwd())
309 .unwrap_or_else(|| path.to_path_buf())
310 .display()
311 .to_string()
312}
313
314#[derive(Clone, Debug)]
315pub struct LintGroup {
316 pub name: &'static str,
317 pub default_level: LintLevel,
318 pub desc: &'static str,
319 pub feature_gate: Option<&'static Feature>,
320 pub hidden: bool,
321}
322
323const COMPLEXITY: LintGroup = LintGroup {
324 name: "complexity",
325 desc: "code that does something simple but in a complex way",
326 default_level: LintLevel::Warn,
327 feature_gate: None,
328 hidden: false,
329};
330
331const CORRECTNESS: LintGroup = LintGroup {
332 name: "correctness",
333 desc: "code that is outright wrong or useless",
334 default_level: LintLevel::Deny,
335 feature_gate: None,
336 hidden: false,
337};
338
339const NURSERY: LintGroup = LintGroup {
340 name: "nursery",
341 desc: "new lints that are still under development",
342 default_level: LintLevel::Allow,
343 feature_gate: None,
344 hidden: false,
345};
346
347const PEDANTIC: LintGroup = LintGroup {
348 name: "pedantic",
349 desc: "lints which are rather strict or have occasional false positives",
350 default_level: LintLevel::Allow,
351 feature_gate: None,
352 hidden: false,
353};
354
355const PERF: LintGroup = LintGroup {
356 name: "perf",
357 desc: "code that can be written to run faster",
358 default_level: LintLevel::Warn,
359 feature_gate: None,
360 hidden: false,
361};
362
363const RESTRICTION: LintGroup = LintGroup {
364 name: "restriction",
365 desc: "lints which prevent the use of Cargo features",
366 default_level: LintLevel::Allow,
367 feature_gate: None,
368 hidden: false,
369};
370
371const STYLE: LintGroup = LintGroup {
372 name: "style",
373 desc: "code that should be written in a more idiomatic way",
374 default_level: LintLevel::Warn,
375 feature_gate: None,
376 hidden: false,
377};
378
379const SUSPICIOUS: LintGroup = LintGroup {
380 name: "suspicious",
381 desc: "code that is most likely wrong or useless",
382 default_level: LintLevel::Warn,
383 feature_gate: None,
384 hidden: false,
385};
386
387const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
389 name: "test_dummy_unstable",
390 desc: "test_dummy_unstable is meant to only be used in tests",
391 default_level: LintLevel::Allow,
392 feature_gate: Some(Feature::test_dummy_unstable()),
393 hidden: true,
394};
395
396#[derive(Clone, Debug)]
397pub struct Lint {
398 pub name: &'static str,
399 pub desc: &'static str,
400 pub primary_group: &'static LintGroup,
401 pub msrv: Option<RustVersion>,
407 pub feature_gate: Option<&'static Feature>,
408 pub docs: Option<&'static str>,
412}
413
414impl Lint {
415 pub fn level(
416 &self,
417 pkg_lints: &TomlToolLints,
418 pkg_rust_version: Option<&RustVersion>,
419 unstable_features: &Features,
420 ) -> (LintLevel, LintLevelReason) {
421 if self
424 .feature_gate
425 .is_some_and(|f| !unstable_features.is_enabled(f))
426 {
427 return (LintLevel::Allow, LintLevelReason::Default);
428 }
429
430 if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
431 let pkg_rust_version = pkg_rust_version.to_partial();
432 if !msrv.is_compatible_with(&pkg_rust_version) {
433 return (LintLevel::Allow, LintLevelReason::Default);
434 }
435 }
436
437 let lint_level_priority =
438 level_priority(self.name, self.primary_group.default_level, pkg_lints);
439
440 let group_level_priority = level_priority(
441 self.primary_group.name,
442 self.primary_group.default_level,
443 pkg_lints,
444 );
445
446 let (_, (l, r, _)) = max_by_key(
447 (self.name, lint_level_priority),
448 (self.primary_group.name, group_level_priority),
449 |(n, (l, _, p))| (l == &LintLevel::Forbid, *p, Reverse(*n)),
450 );
451 (l, r)
452 }
453
454 fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
455 format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
456 }
457}
458
459#[derive(Copy, Clone, Debug, PartialEq)]
460pub enum LintLevel {
461 Allow,
462 Warn,
463 Deny,
464 Forbid,
465}
466
467impl Display for LintLevel {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469 match self {
470 LintLevel::Allow => write!(f, "allow"),
471 LintLevel::Warn => write!(f, "warn"),
472 LintLevel::Deny => write!(f, "deny"),
473 LintLevel::Forbid => write!(f, "forbid"),
474 }
475 }
476}
477
478impl LintLevel {
479 pub fn is_error(&self) -> bool {
480 self == &LintLevel::Forbid || self == &LintLevel::Deny
481 }
482
483 pub fn to_diagnostic_level(self) -> Level<'static> {
484 match self {
485 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
486 LintLevel::Warn => Level::WARNING,
487 LintLevel::Deny => Level::ERROR,
488 LintLevel::Forbid => Level::ERROR,
489 }
490 }
491
492 fn force(self) -> bool {
493 match self {
494 Self::Allow => false,
495 Self::Warn => true,
496 Self::Deny => true,
497 Self::Forbid => true,
498 }
499 }
500}
501
502impl From<TomlLintLevel> for LintLevel {
503 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
504 match toml_lint_level {
505 TomlLintLevel::Allow => LintLevel::Allow,
506 TomlLintLevel::Warn => LintLevel::Warn,
507 TomlLintLevel::Deny => LintLevel::Deny,
508 TomlLintLevel::Forbid => LintLevel::Forbid,
509 }
510 }
511}
512
513#[derive(Copy, Clone, Debug, PartialEq, Eq)]
514pub enum LintLevelReason {
515 Default,
516 Package,
517}
518
519impl Display for LintLevelReason {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 match self {
522 LintLevelReason::Default => write!(f, "by default"),
523 LintLevelReason::Package => write!(f, "in `[lints]`"),
524 }
525 }
526}
527
528impl LintLevelReason {
529 fn is_user_specified(&self) -> bool {
530 match self {
531 LintLevelReason::Default => false,
532 LintLevelReason::Package => true,
533 }
534 }
535}
536
537fn level_priority(
538 name: &str,
539 default_level: LintLevel,
540 pkg_lints: &TomlToolLints,
541) -> (LintLevel, LintLevelReason, i8) {
542 if let Some(defined_level) = pkg_lints.get(name) {
543 (
544 defined_level.level().into(),
545 LintLevelReason::Package,
546 defined_level.priority(),
547 )
548 } else {
549 (default_level, LintLevelReason::Default, 0)
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use itertools::Itertools;
556 use snapbox::ToDebug;
557 use std::collections::HashSet;
558
559 #[test]
560 fn ensure_lint_groups_do_not_default_to_forbid() {
561 let forbid_groups = super::LINT_GROUPS
562 .iter()
563 .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
564 .collect::<Vec<_>>();
565
566 assert!(
567 forbid_groups.is_empty(),
568 "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
569 {}\n",
570 forbid_groups.iter().map(|g| g.name).join("\n")
571 );
572 }
573
574 #[test]
575 fn ensure_sorted_lints() {
576 let location = std::panic::Location::caller();
578 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
579
580 let actual = super::LINTS
581 .iter()
582 .map(|l| l.name.to_uppercase())
583 .collect::<Vec<_>>();
584
585 let mut expected = actual.clone();
586 expected.sort();
587 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
588 }
589
590 #[test]
591 fn ensure_sorted_lint_groups() {
592 let location = std::panic::Location::caller();
594 println!(
595 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
596 location.file(),
597 );
598 let actual = super::LINT_GROUPS
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_updated_lints() {
610 let dir = snapbox::utils::current_dir!().join("rules");
611 let mut expected = HashSet::new();
612 for entry in std::fs::read_dir(&dir).unwrap() {
613 let entry = entry.unwrap();
614 let path = entry.path();
615 if path.ends_with("mod.rs") {
616 continue;
617 }
618 let lint_name = path.file_stem().unwrap().to_string_lossy();
619 assert!(expected.insert(lint_name.into()), "duplicate lint found");
620 }
621
622 let actual = super::LINTS
623 .iter()
624 .map(|l| l.name.to_string())
625 .collect::<HashSet<_>>();
626 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
627
628 let mut need_added = String::new();
629 for name in &diff {
630 need_added.push_str(&format!("{name}\n"));
631 }
632 assert!(
633 diff.is_empty(),
634 "\n`LINTS` did not contain all `Lint`s found in {}\n\
635 Please add the following to `LINTS`:\n\
636 {need_added}",
637 dir.display(),
638 );
639 }
640
641 #[test]
642 fn ensure_updated_lint_groups() {
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(": LintGroup = LintGroup {") {
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::LINT_GROUPS
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`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
673 Please add the following to `LINT_GROUPS`:\n\
674 {}",
675 path.display(),
676 need_added
677 );
678 }
679}