150 lines
4.9 KiB
Rust
150 lines
4.9 KiB
Rust
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);
|
|
});
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|