blob: de6a75b79fcfd196d2c21a3687dafcfb87b3d910 [file] [log] [blame]
use clippy_utils::diagnostics::span_lint_and_sugg;
use itertools::Itertools;
use rustc_errors::Applicability;
use rustc_hir::{Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_session::{declare_tool_lint, 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 correct_ident(ident: &str) -> String {
let ident = ident.chars().rev().collect::<String>();
let fragments = ident
.split_inclusive(|x: char| !x.is_ascii_lowercase())
.rev()
.map(|x| x.chars().rev().collect::<String>());
let mut ident = fragments.clone().next().unwrap();
for (ref prev, ref curr) in fragments.tuple_windows() {
if <[&String; 2]>::from((prev, curr))
.iter()
.all(|s| s.len() == 1 && s.chars().next().unwrap().is_ascii_uppercase())
{
ident.push_str(&curr.to_ascii_lowercase());
} else {
ident.push_str(curr);
}
}
ident
}
fn check_ident(cx: &LateContext<'_>, ident: &Ident, be_aggressive: bool) {
let span = ident.span;
let ident = ident.as_str();
let corrected = correct_ident(ident);
// warn if we have pure-uppercase idents
// assume that two-letter words are some kind of valid abbreviation like FP for false positive
// (and don't warn)
if (ident.chars().all(|c| c.is_ascii_uppercase()) && ident.len() > 2)
// otherwise, warn if we have SOmeTHING lIKE THIs but only warn with the aggressive
// upper-case-acronyms-aggressive config option enabled
|| (be_aggressive && ident != corrected)
{
span_lint_and_sugg(
cx,
UPPER_CASE_ACRONYMS,
span,
&format!("name `{ident}` contains a capitalized acronym"),
"consider making the acronym lowercase, except the initial letter",
corrected,
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, self.upper_case_acronyms_aggressive);
},
ItemKind::Enum(ref enumdef, _) => {
check_ident(cx, &it.ident, 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, self.upper_case_acronyms_aggressive));
},
_ => {},
}
}
}