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 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    // `warn`
57    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    // `allow`
82    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
119/// Version required for specifying `[lints.cargo]`
120///
121/// Before this, it was an error.  No on-by-default lint should fire before this time without
122/// another way of disabling it.
123static 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
184/// This lint group is only to be used for testing purposes
185const 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            // Only relevant for default lints
239            .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        // This will be printed out if the fields are not sorted.
309        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        // This will be printed out if the fields are not sorted.
325        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                // diagnostic
436                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}