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