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