Skip to main content

cargo/diagnostics/rules/
mod.rs

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