Skip to main content

cargo/diagnostics/rules/
mod.rs

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    // `warn`
64    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    // `allow`
81    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
126/// Version required for specifying `[lints.cargo]`
127///
128/// Before this, it was an error.  No on-by-default lint should fire before this time without
129/// another way of disabling it.
130static 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
191/// This lint group is only to be used for testing purposes
192const 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            // Only relevant for default lints
246            .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        // This will be printed out if the fields are not sorted.
316        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        // This will be printed out if the fields are not sorted.
332        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                // diagnostic
443                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}