use clippy_utils::diagnostics::span_lint_hir_and_then; use core::mem::replace; use rustc_errors::Applicability; use rustc_hir::{HirId, Item, ItemKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_session::impl_lint_pass; use rustc_span::symbol::Ident; declare_clippy_lint! { /// ### What it does /// Checks for fully capitalized names and optionally names containing a capitalized acronym. /// /// ### Why is this bad? /// In CamelCase, acronyms count as one word. /// See [naming conventions](https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case) /// for more. /// /// By default, the lint only triggers on fully-capitalized names. /// You can use the `upper-case-acronyms-aggressive: true` config option to enable linting /// on all camel case names /// /// ### Known problems /// When two acronyms are contiguous, the lint can't tell where /// the first acronym ends and the second starts, so it suggests to lowercase all of /// the letters in the second acronym. /// /// ### Example /// ```no_run /// struct HTTPResponse; /// ``` /// Use instead: /// ```no_run /// struct HttpResponse; /// ``` #[clippy::version = "1.51.0"] pub UPPER_CASE_ACRONYMS, style, "capitalized acronyms are against the naming convention" } #[derive(Default)] pub struct UpperCaseAcronyms { avoid_breaking_exported_api: bool, upper_case_acronyms_aggressive: bool, } impl UpperCaseAcronyms { pub fn new(avoid_breaking_exported_api: bool, aggressive: bool) -> Self { Self { avoid_breaking_exported_api, upper_case_acronyms_aggressive: aggressive, } } } impl_lint_pass!(UpperCaseAcronyms => [UPPER_CASE_ACRONYMS]); fn contains_acronym(s: &str) -> bool { let mut count = 0; for c in s.chars() { if c.is_ascii_uppercase() { count += 1; if count == 3 { return true; } } else { count = 0; } } count == 2 } fn check_ident(cx: &LateContext<'_>, ident: &Ident, hir_id: HirId, be_aggressive: bool) { let s = ident.as_str(); // By default, only warn for upper case identifiers with at least 3 characters. let replacement = if s.len() > 2 && s.bytes().all(|c| c.is_ascii_uppercase()) { let mut r = String::with_capacity(s.len()); let mut s = s.chars(); r.push(s.next().unwrap()); r.extend(s.map(|c| c.to_ascii_lowercase())); r } else if be_aggressive // Only lint if the ident starts with an upper case character. && let unprefixed = s.trim_start_matches('_') && unprefixed.starts_with(|c: char| c.is_ascii_uppercase()) && contains_acronym(unprefixed) { let mut r = String::with_capacity(s.len()); let mut s = s.chars(); let mut prev_upper = false; while let Some(c) = s.next() { r.push( if replace(&mut prev_upper, c.is_ascii_uppercase()) && s.clone().next().map_or(true, |c| c.is_ascii_uppercase()) { c.to_ascii_lowercase() } else { c }, ); } r } else { return; }; span_lint_hir_and_then( cx, UPPER_CASE_ACRONYMS, hir_id, ident.span, format!("name `{ident}` contains a capitalized acronym"), |diag| { diag.span_suggestion( ident.span, "consider making the acronym lowercase, except the initial letter", replacement, Applicability::MaybeIncorrect, ); }, ); } impl LateLintPass<'_> for UpperCaseAcronyms { fn check_item(&mut self, cx: &LateContext<'_>, it: &Item<'_>) { // do not lint public items or in macros if in_external_macro(cx.sess(), it.span) || (self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(it.owner_id.def_id)) { return; } match it.kind { ItemKind::TyAlias(..) | ItemKind::Struct(..) | ItemKind::Trait(..) => { check_ident(cx, &it.ident, it.hir_id(), self.upper_case_acronyms_aggressive); }, ItemKind::Enum(ref enumdef, _) => { check_ident(cx, &it.ident, it.hir_id(), self.upper_case_acronyms_aggressive); // check enum variants separately because again we only want to lint on private enums and // the fn check_variant does not know about the vis of the enum of its variants enumdef.variants.iter().for_each(|variant| { check_ident(cx, &variant.ident, variant.hir_id, self.upper_case_acronyms_aggressive); }); }, _ => {}, } } }