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