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, 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 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, LintLevelReason) {
427 if self
430 .feature_gate
431 .is_some_and(|f| !unstable_features.is_enabled(f))
432 {
433 return (LintLevel::Allow, LintLevelReason::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, LintLevelReason::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, r, _)) = max_by_key(
453 (self.name, lint_level_priority),
454 (self.primary_group.name, group_level_priority),
455 |(n, (l, _, p))| (l == &LintLevel::Forbid, *p, Reverse(*n)),
456 );
457 (l, r)
458 }
459
460 pub fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
461 format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
462 }
463}
464
465#[derive(Copy, Clone, Debug, PartialEq)]
466pub enum LintLevel {
467 Allow,
468 Warn,
469 Deny,
470 Forbid,
471}
472
473impl Display for LintLevel {
474 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475 match self {
476 LintLevel::Allow => write!(f, "allow"),
477 LintLevel::Warn => write!(f, "warn"),
478 LintLevel::Deny => write!(f, "deny"),
479 LintLevel::Forbid => write!(f, "forbid"),
480 }
481 }
482}
483
484impl LintLevel {
485 pub fn is_warn(&self) -> bool {
486 self == &LintLevel::Warn
487 }
488
489 pub fn is_error(&self) -> bool {
490 self == &LintLevel::Forbid || self == &LintLevel::Deny
491 }
492
493 pub fn to_diagnostic_level(self) -> Level<'static> {
494 match self {
495 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
496 LintLevel::Warn => Level::WARNING,
497 LintLevel::Deny => Level::ERROR,
498 LintLevel::Forbid => Level::ERROR,
499 }
500 }
501
502 pub fn force(self) -> bool {
503 match self {
504 Self::Allow => false,
505 Self::Warn => true,
506 Self::Deny => true,
507 Self::Forbid => true,
508 }
509 }
510}
511
512impl From<TomlLintLevel> for LintLevel {
513 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
514 match toml_lint_level {
515 TomlLintLevel::Allow => LintLevel::Allow,
516 TomlLintLevel::Warn => LintLevel::Warn,
517 TomlLintLevel::Deny => LintLevel::Deny,
518 TomlLintLevel::Forbid => LintLevel::Forbid,
519 }
520 }
521}
522
523#[derive(Copy, Clone, Debug, PartialEq, Eq)]
524pub enum LintLevelReason {
525 Default,
526 Package,
527}
528
529impl Display for LintLevelReason {
530 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
531 match self {
532 LintLevelReason::Default => write!(f, "by default"),
533 LintLevelReason::Package => write!(f, "in `[lints]`"),
534 }
535 }
536}
537
538impl LintLevelReason {
539 fn is_user_specified(&self) -> bool {
540 match self {
541 LintLevelReason::Default => false,
542 LintLevelReason::Package => true,
543 }
544 }
545}
546
547fn level_priority(
548 name: &str,
549 default_level: LintLevel,
550 pkg_lints: &TomlToolLints,
551) -> (LintLevel, LintLevelReason, i8) {
552 if let Some(defined_level) = pkg_lints.get(name) {
553 (
554 defined_level.level().into(),
555 LintLevelReason::Package,
556 defined_level.priority(),
557 )
558 } else {
559 (default_level, LintLevelReason::Default, 0)
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use itertools::Itertools;
566 use snapbox::ToDebug;
567 use std::collections::HashSet;
568
569 #[test]
570 fn ensure_lint_groups_do_not_default_to_forbid() {
571 let forbid_groups = super::LINT_GROUPS
572 .iter()
573 .filter(|g| matches!(g.default_level, super::LintLevel::Forbid))
574 .collect::<Vec<_>>();
575
576 assert!(
577 forbid_groups.is_empty(),
578 "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
579 {}\n",
580 forbid_groups.iter().map(|g| g.name).join("\n")
581 );
582 }
583
584 #[test]
585 fn ensure_sorted_lints() {
586 let location = std::panic::Location::caller();
588 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
589
590 let actual = super::LINTS
591 .iter()
592 .map(|l| l.name.to_uppercase())
593 .collect::<Vec<_>>();
594
595 let mut expected = actual.clone();
596 expected.sort();
597 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
598 }
599
600 #[test]
601 fn ensure_sorted_lint_groups() {
602 let location = std::panic::Location::caller();
604 println!(
605 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
606 location.file(),
607 );
608 let actual = super::LINT_GROUPS
609 .iter()
610 .map(|l| l.name.to_uppercase())
611 .collect::<Vec<_>>();
612
613 let mut expected = actual.clone();
614 expected.sort();
615 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
616 }
617
618 #[test]
619 fn ensure_updated_lints() {
620 let dir = snapbox::utils::current_dir!().join("rules");
621 let mut expected = HashSet::new();
622 for entry in std::fs::read_dir(&dir).unwrap() {
623 let entry = entry.unwrap();
624 let path = entry.path();
625 if path.ends_with("mod.rs") {
626 continue;
627 }
628 let lint_name = path.file_stem().unwrap().to_string_lossy();
629 assert!(expected.insert(lint_name.into()), "duplicate lint found");
630 }
631
632 let actual = super::LINTS
633 .iter()
634 .map(|l| l.name.to_string())
635 .collect::<HashSet<_>>();
636 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
637
638 let mut need_added = String::new();
639 for name in &diff {
640 need_added.push_str(&format!("{name}\n"));
641 }
642 assert!(
643 diff.is_empty(),
644 "\n`LINTS` did not contain all `Lint`s found in {}\n\
645 Please add the following to `LINTS`:\n\
646 {need_added}",
647 dir.display(),
648 );
649 }
650
651 #[test]
652 fn ensure_updated_lint_groups() {
653 let path = snapbox::utils::current_rs!();
654 let expected = std::fs::read_to_string(&path).unwrap();
655 let expected = expected
656 .lines()
657 .filter_map(|l| {
658 if l.ends_with(": LintGroup = LintGroup {") {
659 Some(
660 l.chars()
661 .skip(6)
662 .take_while(|c| *c != ':')
663 .collect::<String>(),
664 )
665 } else {
666 None
667 }
668 })
669 .collect::<HashSet<_>>();
670 let actual = super::LINT_GROUPS
671 .iter()
672 .map(|l| l.name.to_uppercase())
673 .collect::<HashSet<_>>();
674 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
675
676 let mut need_added = String::new();
677 for name in &diff {
678 need_added.push_str(&format!("{}\n", name));
679 }
680 assert!(
681 diff.is_empty(),
682 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
683 Please add the following to `LINT_GROUPS`:\n\
684 {}",
685 path.display(),
686 need_added
687 );
688 }
689}