1use crate::core::{Edition, Feature, Features, MaybePackage, Package};
2use crate::{CargoResult, GlobalContext};
3
4use annotate_snippets::AnnotationKind;
5use annotate_snippets::Level;
6use annotate_snippets::Snippet;
7use cargo_util_schemas::manifest::TomlLintLevel;
8use cargo_util_schemas::manifest::TomlToolLints;
9use pathdiff::diff_paths;
10
11use std::borrow::Cow;
12use std::fmt::Display;
13use std::ops::Range;
14use std::path::Path;
15
16pub mod rules;
17pub use rules::LINTS;
18
19const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
20
21pub enum ManifestFor<'a> {
23 Package(&'a Package),
25 Workspace(&'a MaybePackage),
27}
28
29impl ManifestFor<'_> {
30 fn lint_level(&self, pkg_lints: &TomlToolLints, lint: Lint) -> (LintLevel, LintLevelReason) {
31 lint.level(pkg_lints, self.edition(), self.unstable_features())
32 }
33
34 pub fn contents(&self) -> &str {
35 match self {
36 ManifestFor::Package(p) => p.manifest().contents(),
37 ManifestFor::Workspace(p) => p.contents(),
38 }
39 }
40
41 pub fn document(&self) -> &toml::Spanned<toml::de::DeTable<'static>> {
42 match self {
43 ManifestFor::Package(p) => p.manifest().document(),
44 ManifestFor::Workspace(p) => p.document(),
45 }
46 }
47
48 pub fn edition(&self) -> Edition {
49 match self {
50 ManifestFor::Package(p) => p.manifest().edition(),
51 ManifestFor::Workspace(p) => p.edition(),
52 }
53 }
54
55 pub fn unstable_features(&self) -> &Features {
56 match self {
57 ManifestFor::Package(p) => p.manifest().unstable_features(),
58 ManifestFor::Workspace(p) => p.unstable_features(),
59 }
60 }
61}
62
63impl<'a> From<&'a Package> for ManifestFor<'a> {
64 fn from(value: &'a Package) -> ManifestFor<'a> {
65 ManifestFor::Package(value)
66 }
67}
68
69impl<'a> From<&'a MaybePackage> for ManifestFor<'a> {
70 fn from(value: &'a MaybePackage) -> ManifestFor<'a> {
71 ManifestFor::Workspace(value)
72 }
73}
74
75pub fn analyze_cargo_lints_table(
76 manifest: ManifestFor<'_>,
77 manifest_path: &Path,
78 cargo_lints: &TomlToolLints,
79 error_count: &mut usize,
80 gctx: &GlobalContext,
81) -> CargoResult<()> {
82 let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
83 let mut unknown_lints = Vec::new();
84 for lint_name in cargo_lints.keys().map(|name| name) {
85 let Some((name, default_level, edition_lint_opts, feature_gate)) =
86 find_lint_or_group(lint_name)
87 else {
88 unknown_lints.push(lint_name);
89 continue;
90 };
91
92 let (_, reason, _) = level_priority(
93 name,
94 *default_level,
95 *edition_lint_opts,
96 cargo_lints,
97 manifest.edition(),
98 );
99
100 if !reason.is_user_specified() {
102 continue;
103 }
104
105 if let Some(feature_gate) = feature_gate
107 && !manifest.unstable_features().is_enabled(feature_gate)
108 {
109 report_feature_not_enabled(
110 name,
111 feature_gate,
112 &manifest,
113 &manifest_path,
114 error_count,
115 gctx,
116 )?;
117 }
118 }
119
120 rules::output_unknown_lints(
121 unknown_lints,
122 &manifest,
123 &manifest_path,
124 cargo_lints,
125 error_count,
126 gctx,
127 )?;
128
129 Ok(())
130}
131
132fn find_lint_or_group<'a>(
133 name: &str,
134) -> Option<(
135 &'static str,
136 &LintLevel,
137 &Option<(Edition, LintLevel)>,
138 &Option<&'static Feature>,
139)> {
140 if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
141 Some((
142 lint.name,
143 &lint.default_level,
144 &lint.edition_lint_opts,
145 &lint.feature_gate,
146 ))
147 } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
148 Some((
149 group.name,
150 &group.default_level,
151 &group.edition_lint_opts,
152 &group.feature_gate,
153 ))
154 } else {
155 None
156 }
157}
158
159fn report_feature_not_enabled(
160 lint_name: &str,
161 feature_gate: &Feature,
162 manifest: &ManifestFor<'_>,
163 manifest_path: &str,
164 error_count: &mut usize,
165 gctx: &GlobalContext,
166) -> CargoResult<()> {
167 let document = manifest.document();
168 let contents = manifest.contents();
169 let dash_feature_name = feature_gate.name().replace("_", "-");
170 let title = format!("use of unstable lint `{}`", lint_name);
171 let label = format!(
172 "this is behind `{}`, which is not enabled",
173 dash_feature_name
174 );
175 let help = format!(
176 "consider adding `cargo-features = [\"{}\"]` to the top of the manifest",
177 dash_feature_name
178 );
179
180 let key_path = match manifest {
181 ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
182 ManifestFor::Workspace(_) => &["workspace", "lints", "cargo", lint_name][..],
183 };
184 let Some(span) = get_key_value_span(document, key_path) else {
185 return Ok(());
187 };
188
189 let report = [Level::ERROR
190 .primary_title(title)
191 .element(
192 Snippet::source(contents)
193 .path(manifest_path)
194 .annotation(AnnotationKind::Primary.span(span.key).label(label)),
195 )
196 .element(Level::HELP.message(help))];
197
198 *error_count += 1;
199 gctx.shell().print_report(&report, true)?;
200
201 Ok(())
202}
203
204#[derive(Clone)]
205pub struct TomlSpan {
206 pub key: Range<usize>,
207 pub value: Range<usize>,
208}
209
210pub fn get_key_value<'doc>(
211 document: &'doc toml::Spanned<toml::de::DeTable<'static>>,
212 path: &[&str],
213) -> Option<(
214 &'doc toml::Spanned<Cow<'doc, str>>,
215 &'doc toml::Spanned<toml::de::DeValue<'static>>,
216)> {
217 let mut table = document.get_ref();
218 let mut iter = path.into_iter().peekable();
219 while let Some(key) = iter.next() {
220 let key_s: &str = key.as_ref();
221 let (key, item) = table.get_key_value(key_s)?;
222 if iter.peek().is_none() {
223 return Some((key, item));
224 }
225 if let Some(next_table) = item.get_ref().as_table() {
226 table = next_table;
227 }
228 if iter.peek().is_some() {
229 if let Some(array) = item.get_ref().as_array() {
230 let next = iter.next().unwrap();
231 return array.iter().find_map(|item| match item.get_ref() {
232 toml::de::DeValue::String(s) if s == next => Some((key, item)),
233 _ => None,
234 });
235 }
236 }
237 }
238 None
239}
240
241pub fn get_key_value_span(
242 document: &toml::Spanned<toml::de::DeTable<'static>>,
243 path: &[&str],
244) -> Option<TomlSpan> {
245 get_key_value(document, path).map(|(k, v)| TomlSpan {
246 key: k.span(),
247 value: v.span(),
248 })
249}
250
251pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
254 diff_paths(path, gctx.cwd())
255 .unwrap_or_else(|| path.to_path_buf())
256 .display()
257 .to_string()
258}
259
260#[derive(Copy, Clone, Debug)]
261pub struct LintGroup {
262 pub name: &'static str,
263 pub default_level: LintLevel,
264 pub desc: &'static str,
265 pub edition_lint_opts: Option<(Edition, LintLevel)>,
266 pub feature_gate: Option<&'static Feature>,
267}
268
269const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
271 name: "test_dummy_unstable",
272 desc: "test_dummy_unstable is meant to only be used in tests",
273 default_level: LintLevel::Allow,
274 edition_lint_opts: None,
275 feature_gate: Some(Feature::test_dummy_unstable()),
276};
277
278#[derive(Copy, Clone, Debug)]
279pub struct Lint {
280 pub name: &'static str,
281 pub desc: &'static str,
282 pub groups: &'static [LintGroup],
283 pub default_level: LintLevel,
284 pub edition_lint_opts: Option<(Edition, LintLevel)>,
285 pub feature_gate: Option<&'static Feature>,
286 pub docs: Option<&'static str>,
290}
291
292impl Lint {
293 pub fn level(
294 &self,
295 pkg_lints: &TomlToolLints,
296 edition: Edition,
297 unstable_features: &Features,
298 ) -> (LintLevel, LintLevelReason) {
299 if self
302 .feature_gate
303 .is_some_and(|f| !unstable_features.is_enabled(f))
304 {
305 return (LintLevel::Allow, LintLevelReason::Default);
306 }
307
308 self.groups
309 .iter()
310 .map(|g| {
311 (
312 g.name,
313 level_priority(
314 g.name,
315 g.default_level,
316 g.edition_lint_opts,
317 pkg_lints,
318 edition,
319 ),
320 )
321 })
322 .chain(std::iter::once((
323 self.name,
324 level_priority(
325 self.name,
326 self.default_level,
327 self.edition_lint_opts,
328 pkg_lints,
329 edition,
330 ),
331 )))
332 .max_by_key(|(n, (l, _, p))| (l == &LintLevel::Forbid, *p, std::cmp::Reverse(*n)))
333 .map(|(_, (l, r, _))| (l, r))
334 .unwrap()
335 }
336
337 fn emitted_source(&self, lint_level: LintLevel, reason: LintLevelReason) -> String {
338 format!("`cargo::{}` is set to `{lint_level}` {reason}", self.name,)
339 }
340}
341
342#[derive(Copy, Clone, Debug, PartialEq)]
343pub enum LintLevel {
344 Allow,
345 Warn,
346 Deny,
347 Forbid,
348}
349
350impl Display for LintLevel {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 match self {
353 LintLevel::Allow => write!(f, "allow"),
354 LintLevel::Warn => write!(f, "warn"),
355 LintLevel::Deny => write!(f, "deny"),
356 LintLevel::Forbid => write!(f, "forbid"),
357 }
358 }
359}
360
361impl LintLevel {
362 pub fn is_error(&self) -> bool {
363 self == &LintLevel::Forbid || self == &LintLevel::Deny
364 }
365
366 pub fn to_diagnostic_level(self) -> Level<'static> {
367 match self {
368 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
369 LintLevel::Warn => Level::WARNING,
370 LintLevel::Deny => Level::ERROR,
371 LintLevel::Forbid => Level::ERROR,
372 }
373 }
374
375 fn force(self) -> bool {
376 match self {
377 Self::Allow => false,
378 Self::Warn => true,
379 Self::Deny => true,
380 Self::Forbid => true,
381 }
382 }
383}
384
385impl From<TomlLintLevel> for LintLevel {
386 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
387 match toml_lint_level {
388 TomlLintLevel::Allow => LintLevel::Allow,
389 TomlLintLevel::Warn => LintLevel::Warn,
390 TomlLintLevel::Deny => LintLevel::Deny,
391 TomlLintLevel::Forbid => LintLevel::Forbid,
392 }
393 }
394}
395
396#[derive(Copy, Clone, Debug, PartialEq, Eq)]
397pub enum LintLevelReason {
398 Default,
399 Edition(Edition),
400 Package,
401}
402
403impl Display for LintLevelReason {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 match self {
406 LintLevelReason::Default => write!(f, "by default"),
407 LintLevelReason::Edition(edition) => write!(f, "in edition {}", edition),
408 LintLevelReason::Package => write!(f, "in `[lints]`"),
409 }
410 }
411}
412
413impl LintLevelReason {
414 fn is_user_specified(&self) -> bool {
415 match self {
416 LintLevelReason::Default => false,
417 LintLevelReason::Edition(_) => false,
418 LintLevelReason::Package => true,
419 }
420 }
421}
422
423fn level_priority(
424 name: &str,
425 default_level: LintLevel,
426 edition_lint_opts: Option<(Edition, LintLevel)>,
427 pkg_lints: &TomlToolLints,
428 edition: Edition,
429) -> (LintLevel, LintLevelReason, i8) {
430 let (unspecified_level, reason) = if let Some(level) = edition_lint_opts
431 .filter(|(e, _)| edition >= *e)
432 .map(|(_, l)| l)
433 {
434 (level, LintLevelReason::Edition(edition))
435 } else {
436 (default_level, LintLevelReason::Default)
437 };
438
439 if unspecified_level == LintLevel::Forbid {
441 return (unspecified_level, reason, 0);
442 }
443
444 if let Some(defined_level) = pkg_lints.get(name) {
445 (
446 defined_level.level().into(),
447 LintLevelReason::Package,
448 defined_level.priority(),
449 )
450 } else {
451 (unspecified_level, reason, 0)
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use itertools::Itertools;
458 use snapbox::ToDebug;
459 use std::collections::HashSet;
460
461 #[test]
462 fn ensure_sorted_lints() {
463 let location = std::panic::Location::caller();
465 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
466
467 let actual = super::LINTS
468 .iter()
469 .map(|l| l.name.to_uppercase())
470 .collect::<Vec<_>>();
471
472 let mut expected = actual.clone();
473 expected.sort();
474 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
475 }
476
477 #[test]
478 fn ensure_sorted_lint_groups() {
479 let location = std::panic::Location::caller();
481 println!(
482 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
483 location.file(),
484 );
485 let actual = super::LINT_GROUPS
486 .iter()
487 .map(|l| l.name.to_uppercase())
488 .collect::<Vec<_>>();
489
490 let mut expected = actual.clone();
491 expected.sort();
492 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
493 }
494
495 #[test]
496 fn ensure_updated_lints() {
497 let dir = snapbox::utils::current_dir!().join("rules");
498 let mut expected = HashSet::new();
499 for entry in std::fs::read_dir(&dir).unwrap() {
500 let entry = entry.unwrap();
501 let path = entry.path();
502 if path.ends_with("mod.rs") {
503 continue;
504 }
505 let lint_name = path.file_stem().unwrap().to_string_lossy();
506 assert!(expected.insert(lint_name.into()), "duplicate lint found");
507 }
508
509 let actual = super::LINTS
510 .iter()
511 .map(|l| l.name.to_string())
512 .collect::<HashSet<_>>();
513 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
514
515 let mut need_added = String::new();
516 for name in &diff {
517 need_added.push_str(&format!("{name}\n"));
518 }
519 assert!(
520 diff.is_empty(),
521 "\n`LINTS` did not contain all `Lint`s found in {}\n\
522 Please add the following to `LINTS`:\n\
523 {need_added}",
524 dir.display(),
525 );
526 }
527
528 #[test]
529 fn ensure_updated_lint_groups() {
530 let path = snapbox::utils::current_rs!();
531 let expected = std::fs::read_to_string(&path).unwrap();
532 let expected = expected
533 .lines()
534 .filter_map(|l| {
535 if l.ends_with(": LintGroup = LintGroup {") {
536 Some(
537 l.chars()
538 .skip(6)
539 .take_while(|c| *c != ':')
540 .collect::<String>(),
541 )
542 } else {
543 None
544 }
545 })
546 .collect::<HashSet<_>>();
547 let actual = super::LINT_GROUPS
548 .iter()
549 .map(|l| l.name.to_uppercase())
550 .collect::<HashSet<_>>();
551 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
552
553 let mut need_added = String::new();
554 for name in &diff {
555 need_added.push_str(&format!("{}\n", name));
556 }
557 assert!(
558 diff.is_empty(),
559 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
560 Please add the following to `LINT_GROUPS`:\n\
561 {}",
562 path.display(),
563 need_added
564 );
565 }
566}