| mod blanket_hint_mostly_unused; |
| mod deferred_parse_diagnostics; |
| mod im_a_teapot; |
| mod missing_lints_features; |
| mod missing_lints_inheritance; |
| 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 redundant_readme; |
| 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::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: non_kebab_case_bins::lint_package, |
| lint: non_kebab_case_bins::LINT, |
| }, |
| ParsePassRule::LintPackage { |
| rule: redundant_homepage::lint_package, |
| lint: redundant_homepage::LINT, |
| }, |
| ParsePassRule::LintPackage { |
| rule: redundant_readme::lint_package, |
| lint: redundant_readme::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: 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, |
| 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, |
| redundant_readme::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, |
| COMPLEXITY, |
| PERF, |
| STYLE, |
| SUSPICIOUS, |
| NURSERY, |
| 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 COMPLEXITY: LintGroup = LintGroup { |
| name: "complexity", |
| desc: "code that does something simple but in a complex way", |
| 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 NURSERY: LintGroup = LintGroup { |
| name: "nursery", |
| desc: "new lints that are still under development", |
| default_level: LintLevel::Allow, |
| 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 PERF: LintGroup = LintGroup { |
| name: "perf", |
| desc: "code that can be written to run faster", |
| default_level: LintLevel::Warn, |
| 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 crate::util::data_structures::HashSet; |
| use crate::util::data_structures::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 |
| ); |
| } |
| } |