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