1mod blanket_hint_mostly_unused;
2mod deferred_parse_diagnostics;
3mod im_a_teapot;
4mod manual_readme;
5mod min_opt_level_hint;
6mod missing_lints_features;
7mod missing_lints_inheritance;
8mod mostly_unused_hint;
9mod non_kebab_case_bins;
10mod non_kebab_case_features;
11mod non_kebab_case_packages;
12mod non_snake_case_features;
13mod non_snake_case_packages;
14mod redundant_homepage;
15mod text_direction_codepoint_in_comment;
16mod text_direction_codepoint_in_literal;
17mod unknown_lints;
18pub mod unused_dependencies;
19mod unused_workspace_dependencies;
20mod unused_workspace_package_fields;
21
22use super::LintGroup;
23use super::LintLevel;
24use super::passes::ParsePassRule;
25use crate::workspace::Feature;
26
27pub const PARSE_PASS_RULES: &[ParsePassRule<'static>] = &[
28 ParsePassRule::DiagnosticManifest {
29 rule: deferred_parse_diagnostics::diagnose_manifest,
30 },
31 ParsePassRule::DiagnosticManifest {
32 rule: missing_lints_features::diagnose_manifest,
33 },
34 ParsePassRule::DiagnosticPackage {
35 rule: min_opt_level_hint::diagnose_package,
36 },
37 ParsePassRule::DiagnosticWorkspace {
38 rule: mostly_unused_hint::diagnose_workspace,
39 },
40 ParsePassRule::DiagnosticPackage {
41 rule: mostly_unused_hint::diagnose_package,
42 },
43 ParsePassRule::LintManifest {
44 rule: text_direction_codepoint_in_comment::lint_manifest,
45 lint: text_direction_codepoint_in_comment::LINT,
46 },
47 ParsePassRule::LintManifest {
48 rule: text_direction_codepoint_in_literal::lint_manifest,
49 lint: text_direction_codepoint_in_literal::LINT,
50 },
51 ParsePassRule::LintManifest {
52 rule: unknown_lints::lint_manifest,
53 lint: unknown_lints::LINT,
54 },
55 ParsePassRule::LintWorkspace {
56 rule: blanket_hint_mostly_unused::lint_workspace,
57 lint: blanket_hint_mostly_unused::LINT,
58 },
59 ParsePassRule::LintWorkspace {
60 rule: unused_workspace_dependencies::lint_workspace,
61 lint: unused_workspace_dependencies::LINT,
62 },
63 ParsePassRule::LintWorkspace {
64 rule: unused_workspace_package_fields::lint_workspace,
65 lint: unused_workspace_package_fields::LINT,
66 },
67 ParsePassRule::LintPackage {
69 rule: missing_lints_inheritance::lint_package,
70 lint: missing_lints_inheritance::LINT,
71 },
72 ParsePassRule::LintPackage {
73 rule: redundant_homepage::lint_package,
74 lint: redundant_homepage::LINT,
75 },
76 ParsePassRule::LintPackage {
77 rule: unused_dependencies::lint_package,
78 lint: unused_dependencies::LINT,
79 },
80 ParsePassRule::LintPackage {
81 rule: im_a_teapot::lint_package,
82 lint: im_a_teapot::LINT,
83 },
84 ParsePassRule::LintPackage {
86 rule: manual_readme::lint_package,
87 lint: manual_readme::LINT,
88 },
89 ParsePassRule::LintPackage {
90 rule: non_kebab_case_bins::lint_package,
91 lint: non_kebab_case_bins::LINT,
92 },
93 ParsePassRule::LintPackage {
94 rule: non_kebab_case_features::lint_package,
95 lint: non_kebab_case_features::LINT,
96 },
97 ParsePassRule::LintPackage {
98 rule: non_kebab_case_packages::lint_package,
99 lint: non_kebab_case_packages::LINT,
100 },
101 ParsePassRule::LintPackage {
102 rule: non_snake_case_features::lint_package,
103 lint: non_snake_case_features::LINT,
104 },
105 ParsePassRule::LintPackage {
106 rule: non_snake_case_packages::lint_package,
107 lint: non_snake_case_packages::LINT,
108 },
109];
110
111pub static LINTS: &[&crate::diagnostics::Lint] = &[
112 blanket_hint_mostly_unused::LINT,
113 im_a_teapot::LINT,
114 manual_readme::LINT,
115 missing_lints_inheritance::LINT,
116 non_kebab_case_bins::LINT,
117 non_kebab_case_features::LINT,
118 non_kebab_case_packages::LINT,
119 non_snake_case_features::LINT,
120 non_snake_case_packages::LINT,
121 redundant_homepage::LINT,
122 text_direction_codepoint_in_comment::LINT,
123 text_direction_codepoint_in_literal::LINT,
124 unknown_lints::LINT,
125 unused_dependencies::LINT,
126 unused_workspace_dependencies::LINT,
127 unused_workspace_package_fields::LINT,
128];
129
130static CARGO_LINTS_MSRV: cargo_util_schemas::manifest::RustVersion =
135 cargo_util_schemas::manifest::RustVersion::new(1, 79, 0);
136
137pub static LINT_GROUPS: &[LintGroup] = &[
138 DEFAULT,
139 CORRECTNESS,
140 STYLE,
141 SUSPICIOUS,
142 PEDANTIC,
143 RESTRICTION,
144 TEST_DUMMY_UNSTABLE,
145];
146
147const DEFAULT: LintGroup = LintGroup {
148 name: "default",
149 desc: "all lints that are on by default (correctness, suspicious, style, complexity, perf)",
150 default_level: LintLevel::Warn,
151 feature_gate: None,
152 hidden: false,
153};
154
155const CORRECTNESS: LintGroup = LintGroup {
156 name: "correctness",
157 desc: "code that is outright wrong or useless",
158 default_level: LintLevel::Deny,
159 feature_gate: None,
160 hidden: false,
161};
162
163const PEDANTIC: LintGroup = LintGroup {
164 name: "pedantic",
165 desc: "lints which are rather strict or have occasional false positives",
166 default_level: LintLevel::Allow,
167 feature_gate: None,
168 hidden: false,
169};
170
171const RESTRICTION: LintGroup = LintGroup {
172 name: "restriction",
173 desc: "lints which prevent the use of Cargo features",
174 default_level: LintLevel::Allow,
175 feature_gate: None,
176 hidden: false,
177};
178
179const STYLE: LintGroup = LintGroup {
180 name: "style",
181 desc: "code that should be written in a more idiomatic way",
182 default_level: LintLevel::Warn,
183 feature_gate: None,
184 hidden: false,
185};
186
187const SUSPICIOUS: LintGroup = LintGroup {
188 name: "suspicious",
189 desc: "code that is most likely wrong or useless",
190 default_level: LintLevel::Warn,
191 feature_gate: None,
192 hidden: false,
193};
194
195const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
197 name: "test_dummy_unstable",
198 desc: "test_dummy_unstable is meant to only be used in tests",
199 default_level: LintLevel::Allow,
200 feature_gate: Some(crate::workspace::Feature::test_dummy_unstable()),
201 hidden: true,
202};
203
204fn find_lint_or_group<'a>(
205 name: &str,
206) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> {
207 if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
208 Some((
209 lint.name,
210 &lint.primary_group.default_level,
211 &lint.feature_gate,
212 ))
213 } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
214 Some((group.name, &group.default_level, &group.feature_gate))
215 } else {
216 None
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use crate::util::data_structures::HashSet;
223 use crate::util::data_structures::IndexMap;
224 use itertools::Itertools;
225 use snapbox::ToDebug;
226 use std::cmp::Reverse;
227
228 use super::*;
229
230 #[test]
231 fn ensure_lint_groups_do_not_default_to_forbid() {
232 let forbid_groups = LINT_GROUPS
233 .iter()
234 .filter(|g| matches!(g.default_level, LintLevel::Forbid))
235 .collect::<Vec<_>>();
236
237 assert!(
238 forbid_groups.is_empty(),
239 "\n`LintGroup`s should never default to `forbid`, but the following do:\n\
240 {}\n",
241 forbid_groups.iter().map(|g| g.name).join("\n")
242 );
243 }
244
245 #[test]
246 fn ensure_visible_lint_msrv() {
247 let invalid_msrvs = LINTS
248 .iter()
249 .filter(|l| !matches!(l.primary_group.default_level, LintLevel::Allow))
251 .filter(|l| l.msrv.map(|v| v < CARGO_LINTS_MSRV).unwrap_or(false))
252 .map(|l| l.name)
253 .join(", ");
254 assert!(
255 invalid_msrvs.is_empty(),
256 "{invalid_msrvs} need `msrv` set so users can use `[lints.cargo]` to disable them"
257 );
258 }
259
260 #[test]
261 fn ensure_docs_sections() {
262 let expected_sections_restriction = &[
263 "### What it does",
264 "### Why restrict this?",
265 "### Drawbacks",
266 "### Example",
267 ];
268 let expected_sections = &[
269 "### What it does",
270 "### Why is this bad?",
271 "### Drawbacks",
272 "### Example",
273 ];
274 for lint in LINTS {
275 dbg!(lint.name);
276 let mut sections = IndexMap::default();
277 let mut title = "";
278 let mut body = Vec::new();
279 let Some(docs) = lint.docs else {
280 continue;
281 };
282 for line in docs.trim().lines() {
283 if line.starts_with("#") {
284 if !title.is_empty() || !body.is_empty() {
285 let old = sections.insert(title, body);
286 assert!(old.is_none(), "duplicate title: `{title:?}`");
287 }
288 title = line;
289 body = Vec::new();
290 } else {
291 body.push(line);
292 }
293 }
294 if !title.is_empty() || !body.is_empty() {
295 let old = sections.insert(title, body);
296 assert!(old.is_none(), "duplicate title: `{title:?}`");
297 }
298
299 let mut expected = Vec::new();
300 let expected_sections = match lint.primary_group.name {
301 "restriction" => expected_sections_restriction,
302 _ => expected_sections,
303 };
304 for section in expected_sections {
305 let body = match sections.get(section) {
306 Some(body) => body,
307 None => continue,
308 };
309 expected.push(*section);
310 expected.extend(body.iter().copied());
311 }
312 let expected = expected.join("\n");
313 snapbox::assert_data_eq!(docs.trim(), expected);
314 }
315 }
316
317 #[test]
318 fn ensure_sorted_lints() {
319 let location = std::panic::Location::caller();
321 println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),);
322
323 let actual = LINTS
324 .iter()
325 .map(|l| l.name.to_uppercase())
326 .collect::<Vec<_>>();
327
328 let mut expected = actual.clone();
329 expected.sort();
330 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
331 }
332
333 #[test]
334 fn ensure_sorted_lint_groups() {
335 let location = std::panic::Location::caller();
337 println!(
338 "\nTo fix this test, sort `LINT_GROUPS` in {}\n",
339 location.file(),
340 );
341 let actual = LINT_GROUPS
342 .iter()
343 .map(|l| {
344 (
345 l.name != "default",
346 Reverse(l.default_level),
347 l.name.to_uppercase(),
348 )
349 })
350 .collect::<Vec<_>>();
351
352 let mut expected = actual.clone();
353 expected.sort();
354 snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug());
355 }
356
357 #[test]
358 fn ensure_sorted_parse_pass_rules() {
359 let actual = parse_pass_rule_names(PARSE_PASS_RULES);
360 let mut ordered_parse_pass = PARSE_PASS_RULES.to_vec();
361 ordered_parse_pass.sort_by_key(|rule| {
362 let (lint, scope) = match rule {
363 ParsePassRule::DiagnosticManifest { .. } => {
364 let scope = 0;
365 (None, scope)
366 }
367 ParsePassRule::LintManifest { lint, .. } => {
368 let scope = 0;
369 (Some(lint), scope)
370 }
371 ParsePassRule::DiagnosticWorkspace { .. } => {
372 let scope = 1;
373 (None, scope)
374 }
375 ParsePassRule::LintWorkspace { lint, .. } => {
376 let scope = 1;
377 (Some(lint), scope)
378 }
379 ParsePassRule::DiagnosticPackage { .. } => {
380 let scope = 2;
381 (None, scope)
382 }
383 ParsePassRule::LintPackage { lint, .. } => {
384 let scope = 2;
385 (Some(lint), scope)
386 }
387 };
388 let is_lint = lint.is_some();
389 let level = lint.map(|l| std::cmp::Reverse(l.primary_group.default_level));
390 let name = lint.map(|l| l.name);
391 (is_lint, scope, level, name)
392 });
393 let expected = parse_pass_rule_names(&ordered_parse_pass);
394
395 println!("`PARSE_PASS_RULES` sort order:");
396 snapbox::assert_data_eq!(actual.join("\n"), expected.join("\n"));
397 }
398
399 #[test]
400 fn ensure_parse_passed_in_lints() {
401 let parse_pass_lint_names =
402 HashSet::from_iter(parse_pass_rule_names(PARSE_PASS_RULES).into_iter());
403 let lint_names = LINTS.iter().map(|l| l.name).collect::<HashSet<_>>();
404 let diff = parse_pass_lint_names
405 .difference(&lint_names)
406 .sorted()
407 .collect::<Vec<_>>();
408 let mut need_added = String::new();
409 for name in &diff {
410 need_added.push_str(&format!("{name}\n"));
411 }
412 assert!(
413 diff.is_empty(),
414 "\n`LINTS` did not contain all `Lint`s found in `PARSE_PASS_RULES`\n\
415 Please add the following to `LINTS`:\n\
416 {need_added}",
417 );
418 }
419
420 fn parse_pass_rule_names(rules: &[ParsePassRule<'_>]) -> Vec<&'static str> {
421 rules
422 .iter()
423 .filter_map(|rule| match rule {
424 ParsePassRule::DiagnosticManifest { .. }
425 | ParsePassRule::DiagnosticWorkspace { .. }
426 | ParsePassRule::DiagnosticPackage { .. } => None,
427 ParsePassRule::LintManifest { lint, .. }
428 | ParsePassRule::LintWorkspace { lint, .. }
429 | ParsePassRule::LintPackage { lint, .. } => Some(lint.name),
430 })
431 .collect()
432 }
433
434 #[test]
435 fn ensure_updated_lints() {
436 let dir = snapbox::utils::current_dir!();
437 let mut expected = HashSet::default();
438 for entry in std::fs::read_dir(&dir).unwrap() {
439 let entry = entry.unwrap();
440 let path = entry.path();
441 if path.ends_with("mod.rs") {
442 continue;
443 }
444 let content = std::fs::read_to_string(&path).unwrap();
445 if !content.contains("LINT") {
446 continue;
448 }
449 let lint_name = path.file_stem().unwrap().to_string_lossy();
450 assert!(expected.insert(lint_name.into()), "duplicate lint found");
451 }
452
453 let actual = LINTS
454 .iter()
455 .map(|l| l.name.to_string())
456 .collect::<HashSet<_>>();
457 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
458
459 let mut need_added = String::new();
460 for name in &diff {
461 need_added.push_str(&format!("{name}\n"));
462 }
463 assert!(
464 diff.is_empty(),
465 "\n`LINTS` did not contain all `Lint`s found in {}\n\
466 Please add the following to `LINTS`:\n\
467 {need_added}",
468 dir.display(),
469 );
470 }
471
472 #[test]
473 fn ensure_updated_lint_groups() {
474 let path = snapbox::utils::current_rs!();
475 let expected = std::fs::read_to_string(&path).unwrap();
476 let expected = expected
477 .lines()
478 .filter_map(|l| {
479 if l.ends_with(": LintGroup = LintGroup {") {
480 Some(
481 l.chars()
482 .skip(6)
483 .take_while(|c| *c != ':')
484 .collect::<String>(),
485 )
486 } else {
487 None
488 }
489 })
490 .collect::<HashSet<_>>();
491 let actual = LINT_GROUPS
492 .iter()
493 .map(|l| l.name.to_uppercase())
494 .collect::<HashSet<_>>();
495 let diff = expected.difference(&actual).sorted().collect::<Vec<_>>();
496
497 let mut need_added = String::new();
498 for name in &diff {
499 need_added.push_str(&format!("{}\n", name));
500 }
501 assert!(
502 diff.is_empty(),
503 "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\
504 Please add the following to `LINT_GROUPS`:\n\
505 {}",
506 path.display(),
507 need_added
508 );
509 }
510}