| mod blanket_hint_mostly_unused; | |
| mod deferred_parse_diagnostics; | |
| mod im_a_teapot; | |
| mod manual_readme; | |
| mod min_opt_level_hint; | |
| mod missing_lints_features; | |
| mod missing_lints_inheritance; | |
| mod mostly_unused_hint; | |
| mod non_kebab_case_bins; | |
| mod non_kebab_case_features; | |
| mod non_kebab_case_packages; | |
| mod non_snake_case_features; | |
| mod non_snake_case_packages; | |
| mod redundant_homepage; | |
| mod text_direction_codepoint_in_comment; | |
| mod text_direction_codepoint_in_literal; | |
| mod unknown_lints; | |
| pub mod unused_dependencies; | |
| mod unused_workspace_dependencies; | |
| mod unused_workspace_package_fields; | |
| use super::LintGroup; | |
| use super::LintLevel; | |
| use super::passes::ParsePassRule; | |
| use crate::workspace::Feature; | |
| pub const PARSE_PASS_RULES: &[ParsePassRule<'static>] = &[ | |
| ParsePassRule::DiagnosticManifest { | |
| rule: deferred_parse_diagnostics::diagnose_manifest, | |
| }, | |
| ParsePassRule::DiagnosticManifest { | |
| rule: missing_lints_features::diagnose_manifest, | |
| }, | |
| ParsePassRule::DiagnosticPackage { | |
| rule: min_opt_level_hint::diagnose_package, | |
| }, | |
| ParsePassRule::DiagnosticWorkspace { | |
| rule: mostly_unused_hint::diagnose_workspace, | |
| }, | |
| ParsePassRule::DiagnosticPackage { | |
| rule: mostly_unused_hint::diagnose_package, | |
| }, | |
| ParsePassRule::LintManifest { | |
| rule: text_direction_codepoint_in_comment::lint_manifest, | |
| lint: text_direction_codepoint_in_comment::LINT, | |
| }, | |
| ParsePassRule::LintManifest { | |
| rule: text_direction_codepoint_in_literal::lint_manifest, | |
| lint: text_direction_codepoint_in_literal::LINT, | |
| }, | |
| ParsePassRule::LintManifest { | |
| rule: unknown_lints::lint_manifest, | |
| lint: unknown_lints::LINT, | |
| }, | |
| ParsePassRule::LintWorkspace { | |
| rule: blanket_hint_mostly_unused::lint_workspace, | |
| lint: blanket_hint_mostly_unused::LINT, | |
| }, | |
| ParsePassRule::LintWorkspace { | |
| rule: unused_workspace_dependencies::lint_workspace, | |
| lint: unused_workspace_dependencies::LINT, | |
| }, | |
| ParsePassRule::LintWorkspace { | |
| rule: unused_workspace_package_fields::lint_workspace, | |
| lint: unused_workspace_package_fields::LINT, | |
| }, | |
| // `warn` | |
| ParsePassRule::LintPackage { | |
| rule: missing_lints_inheritance::lint_package, | |
| lint: missing_lints_inheritance::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: redundant_homepage::lint_package, | |
| lint: redundant_homepage::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: unused_dependencies::lint_package, | |
| lint: unused_dependencies::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: im_a_teapot::lint_package, | |
| lint: im_a_teapot::LINT, | |
| }, | |
| // `allow` | |
| ParsePassRule::LintPackage { | |
| rule: manual_readme::lint_package, | |
| lint: manual_readme::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: non_kebab_case_bins::lint_package, | |
| lint: non_kebab_case_bins::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: non_kebab_case_features::lint_package, | |
| lint: non_kebab_case_features::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: non_kebab_case_packages::lint_package, | |
| lint: non_kebab_case_packages::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: non_snake_case_features::lint_package, | |
| lint: non_snake_case_features::LINT, | |
| }, | |
| ParsePassRule::LintPackage { | |
| rule: non_snake_case_packages::lint_package, | |
| lint: non_snake_case_packages::LINT, | |
| }, | |
| ]; | |
| pub static LINTS: &[&crate::diagnostics::Lint] = &[ | |
| blanket_hint_mostly_unused::LINT, | |
| im_a_teapot::LINT, | |
| manual_readme::LINT, | |
| missing_lints_inheritance::LINT, | |
| non_kebab_case_bins::LINT, | |
| non_kebab_case_features::LINT, | |
| non_kebab_case_packages::LINT, | |
| non_snake_case_features::LINT, | |
| non_snake_case_packages::LINT, | |
| redundant_homepage::LINT, | |
| text_direction_codepoint_in_comment::LINT, | |
| text_direction_codepoint_in_literal::LINT, | |
| unknown_lints::LINT, | |
| unused_dependencies::LINT, | |
| unused_workspace_dependencies::LINT, | |
| unused_workspace_package_fields::LINT, | |
| ]; | |
| /// Version required for specifying `[lints.cargo]` | |
| /// | |
| /// Before this, it was an error. No on-by-default lint should fire before this time without | |
| /// another way of disabling it. | |
| static CARGO_LINTS_MSRV: cargo_util_schemas::manifest::RustVersion = | |
| cargo_util_schemas::manifest::RustVersion::new(1, 79, 0); | |
| pub static LINT_GROUPS: &[LintGroup] = &[ | |
| DEFAULT, | |
| CORRECTNESS, | |
| STYLE, | |
| SUSPICIOUS, | |
| PEDANTIC, | |
| RESTRICTION, | |
| TEST_DUMMY_UNSTABLE, | |
| ]; | |
| const DEFAULT: LintGroup = LintGroup { | |
| name: "default", | |
| desc: "all lints that are on by default (correctness, suspicious, style, complexity, perf)", | |
| default_level: LintLevel::Warn, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| const CORRECTNESS: LintGroup = LintGroup { | |
| name: "correctness", | |
| desc: "code that is outright wrong or useless", | |
| default_level: LintLevel::Deny, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| const PEDANTIC: LintGroup = LintGroup { | |
| name: "pedantic", | |
| desc: "lints which are rather strict or have occasional false positives", | |
| default_level: LintLevel::Allow, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| const RESTRICTION: LintGroup = LintGroup { | |
| name: "restriction", | |
| desc: "lints which prevent the use of Cargo features", | |
| default_level: LintLevel::Allow, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| const STYLE: LintGroup = LintGroup { | |
| name: "style", | |
| desc: "code that should be written in a more idiomatic way", | |
| default_level: LintLevel::Warn, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| const SUSPICIOUS: LintGroup = LintGroup { | |
| name: "suspicious", | |
| desc: "code that is most likely wrong or useless", | |
| default_level: LintLevel::Warn, | |
| feature_gate: None, | |
| hidden: false, | |
| }; | |
| /// This lint group is only to be used for testing purposes | |
| const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup { | |
| name: "test_dummy_unstable", | |
| desc: "test_dummy_unstable is meant to only be used in tests", | |
| default_level: LintLevel::Allow, | |
| feature_gate: Some(crate::workspace::Feature::test_dummy_unstable()), | |
| hidden: true, | |
| }; | |
| fn find_lint_or_group<'a>( | |
| name: &str, | |
| ) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> { | |
| if let Some(lint) = LINTS.iter().find(|l| l.name == name) { | |
| Some(( | |
| lint.name, | |
| &lint.primary_group.default_level, | |
| &lint.feature_gate, | |
| )) | |
| } else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) { | |
| Some((group.name, &group.default_level, &group.feature_gate)) | |
| } else { | |
| None | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use cargo_util::collections::HashSet; | |
| use cargo_util::collections::IndexMap; | |
| use itertools::Itertools; | |
| use snapbox::ToDebug; | |
| use std::cmp::Reverse; | |
| use super::*; | |
| #[test] | |
| fn ensure_lint_groups_do_not_default_to_forbid() { | |
| let forbid_groups = LINT_GROUPS | |
| .iter() | |
| .filter(|g| matches!(g.default_level, LintLevel::Forbid)) | |
| .collect::<Vec<_>>(); | |
| assert!( | |
| forbid_groups.is_empty(), | |
| "\n`LintGroup`s should never default to `forbid`, but the following do:\n\ | |
| {}\n", | |
| forbid_groups.iter().map(|g| g.name).join("\n") | |
| ); | |
| } | |
| #[test] | |
| fn ensure_visible_lint_msrv() { | |
| let invalid_msrvs = LINTS | |
| .iter() | |
| // Only relevant for default lints | |
| .filter(|l| !matches!(l.primary_group.default_level, LintLevel::Allow)) | |
| .filter(|l| l.msrv.map(|v| v < CARGO_LINTS_MSRV).unwrap_or(false)) | |
| .map(|l| l.name) | |
| .join(", "); | |
| assert!( | |
| invalid_msrvs.is_empty(), | |
| "{invalid_msrvs} need `msrv` set so users can use `[lints.cargo]` to disable them" | |
| ); | |
| } | |
| #[test] | |
| fn ensure_docs_sections() { | |
| let expected_sections_restriction = &[ | |
| "### What it does", | |
| "### Why restrict this?", | |
| "### Drawbacks", | |
| "### Example", | |
| ]; | |
| let expected_sections = &[ | |
| "### What it does", | |
| "### Why is this bad?", | |
| "### Drawbacks", | |
| "### Example", | |
| ]; | |
| for lint in LINTS { | |
| dbg!(lint.name); | |
| let mut sections = IndexMap::default(); | |
| let mut title = ""; | |
| let mut body = Vec::new(); | |
| let Some(docs) = lint.docs else { | |
| continue; | |
| }; | |
| for line in docs.trim().lines() { | |
| if line.starts_with("#") { | |
| if !title.is_empty() || !body.is_empty() { | |
| let old = sections.insert(title, body); | |
| assert!(old.is_none(), "duplicate title: `{title:?}`"); | |
| } | |
| title = line; | |
| body = Vec::new(); | |
| } else { | |
| body.push(line); | |
| } | |
| } | |
| if !title.is_empty() || !body.is_empty() { | |
| let old = sections.insert(title, body); | |
| assert!(old.is_none(), "duplicate title: `{title:?}`"); | |
| } | |
| let mut expected = Vec::new(); | |
| let expected_sections = match lint.primary_group.name { | |
| "restriction" => expected_sections_restriction, | |
| _ => expected_sections, | |
| }; | |
| for section in expected_sections { | |
| let body = match sections.get(section) { | |
| Some(body) => body, | |
| None => continue, | |
| }; | |
| expected.push(*section); | |
| expected.extend(body.iter().copied()); | |
| } | |
| let expected = expected.join("\n"); | |
| snapbox::assert_data_eq!(docs.trim(), expected); | |
| } | |
| } | |
| #[test] | |
| fn ensure_sorted_lints() { | |
| // This will be printed out if the fields are not sorted. | |
| let location = std::panic::Location::caller(); | |
| println!("\nTo fix this test, sort `LINTS` in {}\n", location.file(),); | |
| let actual = LINTS | |
| .iter() | |
| .map(|l| l.name.to_uppercase()) | |
| .collect::<Vec<_>>(); | |
| let mut expected = actual.clone(); | |
| expected.sort(); | |
| snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug()); | |
| } | |
| #[test] | |
| fn ensure_sorted_lint_groups() { | |
| // This will be printed out if the fields are not sorted. | |
| let location = std::panic::Location::caller(); | |
| println!( | |
| "\nTo fix this test, sort `LINT_GROUPS` in {}\n", | |
| location.file(), | |
| ); | |
| let actual = LINT_GROUPS | |
| .iter() | |
| .map(|l| { | |
| ( | |
| l.name != "default", | |
| Reverse(l.default_level), | |
| l.name.to_uppercase(), | |
| ) | |
| }) | |
| .collect::<Vec<_>>(); | |
| let mut expected = actual.clone(); | |
| expected.sort(); | |
| snapbox::assert_data_eq!(actual.to_debug(), expected.to_debug()); | |
| } | |
| #[test] | |
| fn ensure_sorted_parse_pass_rules() { | |
| let actual = parse_pass_rule_names(PARSE_PASS_RULES); | |
| let mut ordered_parse_pass = PARSE_PASS_RULES.to_vec(); | |
| ordered_parse_pass.sort_by_key(|rule| { | |
| let (lint, scope) = match rule { | |
| ParsePassRule::DiagnosticManifest { .. } => { | |
| let scope = 0; | |
| (None, scope) | |
| } | |
| ParsePassRule::LintManifest { lint, .. } => { | |
| let scope = 0; | |
| (Some(lint), scope) | |
| } | |
| ParsePassRule::DiagnosticWorkspace { .. } => { | |
| let scope = 1; | |
| (None, scope) | |
| } | |
| ParsePassRule::LintWorkspace { lint, .. } => { | |
| let scope = 1; | |
| (Some(lint), scope) | |
| } | |
| ParsePassRule::DiagnosticPackage { .. } => { | |
| let scope = 2; | |
| (None, scope) | |
| } | |
| ParsePassRule::LintPackage { lint, .. } => { | |
| let scope = 2; | |
| (Some(lint), scope) | |
| } | |
| }; | |
| let is_lint = lint.is_some(); | |
| let level = lint.map(|l| std::cmp::Reverse(l.primary_group.default_level)); | |
| let name = lint.map(|l| l.name); | |
| (is_lint, scope, level, name) | |
| }); | |
| let expected = parse_pass_rule_names(&ordered_parse_pass); | |
| println!("`PARSE_PASS_RULES` sort order:"); | |
| snapbox::assert_data_eq!(actual.join("\n"), expected.join("\n")); | |
| } | |
| #[test] | |
| fn ensure_parse_passed_in_lints() { | |
| let parse_pass_lint_names = | |
| HashSet::from_iter(parse_pass_rule_names(PARSE_PASS_RULES).into_iter()); | |
| let lint_names = LINTS.iter().map(|l| l.name).collect::<HashSet<_>>(); | |
| let diff = parse_pass_lint_names | |
| .difference(&lint_names) | |
| .sorted() | |
| .collect::<Vec<_>>(); | |
| let mut need_added = String::new(); | |
| for name in &diff { | |
| need_added.push_str(&format!("{name}\n")); | |
| } | |
| assert!( | |
| diff.is_empty(), | |
| "\n`LINTS` did not contain all `Lint`s found in `PARSE_PASS_RULES`\n\ | |
| Please add the following to `LINTS`:\n\ | |
| {need_added}", | |
| ); | |
| } | |
| fn parse_pass_rule_names(rules: &[ParsePassRule<'_>]) -> Vec<&'static str> { | |
| rules | |
| .iter() | |
| .filter_map(|rule| match rule { | |
| ParsePassRule::DiagnosticManifest { .. } | |
| | ParsePassRule::DiagnosticWorkspace { .. } | |
| | ParsePassRule::DiagnosticPackage { .. } => None, | |
| ParsePassRule::LintManifest { lint, .. } | |
| | ParsePassRule::LintWorkspace { lint, .. } | |
| | ParsePassRule::LintPackage { lint, .. } => Some(lint.name), | |
| }) | |
| .collect() | |
| } | |
| #[test] | |
| fn ensure_updated_lints() { | |
| let dir = snapbox::utils::current_dir!(); | |
| let mut expected = HashSet::default(); | |
| for entry in std::fs::read_dir(&dir).unwrap() { | |
| let entry = entry.unwrap(); | |
| let path = entry.path(); | |
| if path.ends_with("mod.rs") { | |
| continue; | |
| } | |
| let content = std::fs::read_to_string(&path).unwrap(); | |
| if !content.contains("LINT") { | |
| // diagnostic | |
| continue; | |
| } | |
| let lint_name = path.file_stem().unwrap().to_string_lossy(); | |
| assert!(expected.insert(lint_name.into()), "duplicate lint found"); | |
| } | |
| let actual = LINTS | |
| .iter() | |
| .map(|l| l.name.to_string()) | |
| .collect::<HashSet<_>>(); | |
| let diff = expected.difference(&actual).sorted().collect::<Vec<_>>(); | |
| let mut need_added = String::new(); | |
| for name in &diff { | |
| need_added.push_str(&format!("{name}\n")); | |
| } | |
| assert!( | |
| diff.is_empty(), | |
| "\n`LINTS` did not contain all `Lint`s found in {}\n\ | |
| Please add the following to `LINTS`:\n\ | |
| {need_added}", | |
| dir.display(), | |
| ); | |
| } | |
| #[test] | |
| fn ensure_updated_lint_groups() { | |
| let path = snapbox::utils::current_rs!(); | |
| let expected = std::fs::read_to_string(&path).unwrap(); | |
| let expected = expected | |
| .lines() | |
| .filter_map(|l| { | |
| if l.ends_with(": LintGroup = LintGroup {") { | |
| Some( | |
| l.chars() | |
| .skip(6) | |
| .take_while(|c| *c != ':') | |
| .collect::<String>(), | |
| ) | |
| } else { | |
| None | |
| } | |
| }) | |
| .collect::<HashSet<_>>(); | |
| let actual = LINT_GROUPS | |
| .iter() | |
| .map(|l| l.name.to_uppercase()) | |
| .collect::<HashSet<_>>(); | |
| let diff = expected.difference(&actual).sorted().collect::<Vec<_>>(); | |
| let mut need_added = String::new(); | |
| for name in &diff { | |
| need_added.push_str(&format!("{}\n", name)); | |
| } | |
| assert!( | |
| diff.is_empty(), | |
| "\n`LINT_GROUPS` did not contain all `LintGroup`s found in {}\n\ | |
| Please add the following to `LINT_GROUPS`:\n\ | |
| {}", | |
| path.display(), | |
| need_added | |
| ); | |
| } | |
| } |