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 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    // `warn`
68    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    // `allow`
85    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
130/// Version required for specifying `[lints.cargo]`
131///
132/// Before this, it was an error.  No on-by-default lint should fire before this time without
133/// another way of disabling it.
134static 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
195/// This lint group is only to be used for testing purposes
196const 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            // Only relevant for default lints
250            .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        // This will be printed out if the fields are not sorted.
320        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        // This will be printed out if the fields are not sorted.
336        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                // diagnostic
447                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}