]>
Commit | Line | Data |
---|---|---|
4b012472 | 1 | use clippy_utils::diagnostics::span_lint_hir_and_then; |
f20569fa | 2 | use itertools::Itertools; |
f20569fa | 3 | use rustc_errors::Applicability; |
4b012472 | 4 | use rustc_hir::{HirId, Item, ItemKind}; |
17df50a5 | 5 | use rustc_lint::{LateContext, LateLintPass, LintContext}; |
f20569fa | 6 | use rustc_middle::lint::in_external_macro; |
4b012472 | 7 | use rustc_session::impl_lint_pass; |
f20569fa XL |
8 | use rustc_span::symbol::Ident; |
9 | ||
10 | declare_clippy_lint! { | |
94222f64 XL |
11 | /// ### What it does |
12 | /// Checks for fully capitalized names and optionally names containing a capitalized acronym. | |
f20569fa | 13 | /// |
94222f64 XL |
14 | /// ### Why is this bad? |
15 | /// In CamelCase, acronyms count as one word. | |
f20569fa XL |
16 | /// See [naming conventions](https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case) |
17 | /// for more. | |
18 | /// | |
19 | /// By default, the lint only triggers on fully-capitalized names. | |
20 | /// You can use the `upper-case-acronyms-aggressive: true` config option to enable linting | |
21 | /// on all camel case names | |
22 | /// | |
94222f64 XL |
23 | /// ### Known problems |
24 | /// When two acronyms are contiguous, the lint can't tell where | |
f20569fa XL |
25 | /// the first acronym ends and the second starts, so it suggests to lowercase all of |
26 | /// the letters in the second acronym. | |
27 | /// | |
94222f64 | 28 | /// ### Example |
ed00b5ec | 29 | /// ```no_run |
f20569fa XL |
30 | /// struct HTTPResponse; |
31 | /// ``` | |
32 | /// Use instead: | |
ed00b5ec | 33 | /// ```no_run |
f20569fa XL |
34 | /// struct HttpResponse; |
35 | /// ``` | |
a2a8927a | 36 | #[clippy::version = "1.51.0"] |
f20569fa XL |
37 | pub UPPER_CASE_ACRONYMS, |
38 | style, | |
39 | "capitalized acronyms are against the naming convention" | |
40 | } | |
41 | ||
42 | #[derive(Default)] | |
43 | pub struct UpperCaseAcronyms { | |
17df50a5 | 44 | avoid_breaking_exported_api: bool, |
f20569fa XL |
45 | upper_case_acronyms_aggressive: bool, |
46 | } | |
47 | ||
48 | impl UpperCaseAcronyms { | |
17df50a5 | 49 | pub fn new(avoid_breaking_exported_api: bool, aggressive: bool) -> Self { |
f20569fa | 50 | Self { |
17df50a5 | 51 | avoid_breaking_exported_api, |
f20569fa XL |
52 | upper_case_acronyms_aggressive: aggressive, |
53 | } | |
54 | } | |
55 | } | |
56 | ||
57 | impl_lint_pass!(UpperCaseAcronyms => [UPPER_CASE_ACRONYMS]); | |
58 | ||
59 | fn correct_ident(ident: &str) -> String { | |
60 | let ident = ident.chars().rev().collect::<String>(); | |
61 | let fragments = ident | |
62 | .split_inclusive(|x: char| !x.is_ascii_lowercase()) | |
63 | .rev() | |
64 | .map(|x| x.chars().rev().collect::<String>()); | |
65 | ||
66 | let mut ident = fragments.clone().next().unwrap(); | |
67 | for (ref prev, ref curr) in fragments.tuple_windows() { | |
fe692bf9 | 68 | if <[&String; 2]>::from((prev, curr)) |
f20569fa XL |
69 | .iter() |
70 | .all(|s| s.len() == 1 && s.chars().next().unwrap().is_ascii_uppercase()) | |
71 | { | |
72 | ident.push_str(&curr.to_ascii_lowercase()); | |
73 | } else { | |
74 | ident.push_str(curr); | |
75 | } | |
76 | } | |
77 | ident | |
78 | } | |
79 | ||
4b012472 | 80 | fn check_ident(cx: &LateContext<'_>, ident: &Ident, hir_id: HirId, be_aggressive: bool) { |
f20569fa | 81 | let span = ident.span; |
a2a8927a | 82 | let ident = ident.as_str(); |
f20569fa XL |
83 | let corrected = correct_ident(ident); |
84 | // warn if we have pure-uppercase idents | |
85 | // assume that two-letter words are some kind of valid abbreviation like FP for false positive | |
86 | // (and don't warn) | |
87 | if (ident.chars().all(|c| c.is_ascii_uppercase()) && ident.len() > 2) | |
cdc7bbd5 | 88 | // otherwise, warn if we have SOmeTHING lIKE THIs but only warn with the aggressive |
f20569fa | 89 | // upper-case-acronyms-aggressive config option enabled |
a2a8927a | 90 | || (be_aggressive && ident != corrected) |
f20569fa | 91 | { |
4b012472 | 92 | span_lint_hir_and_then( |
f20569fa XL |
93 | cx, |
94 | UPPER_CASE_ACRONYMS, | |
4b012472 | 95 | hir_id, |
f20569fa | 96 | span, |
e8be2606 | 97 | format!("name `{ident}` contains a capitalized acronym"), |
4b012472 FG |
98 | |diag| { |
99 | diag.span_suggestion( | |
100 | span, | |
101 | "consider making the acronym lowercase, except the initial letter", | |
102 | corrected, | |
103 | Applicability::MaybeIncorrect, | |
104 | ); | |
105 | }, | |
17df50a5 | 106 | ); |
f20569fa XL |
107 | } |
108 | } | |
109 | ||
17df50a5 XL |
110 | impl LateLintPass<'_> for UpperCaseAcronyms { |
111 | fn check_item(&mut self, cx: &LateContext<'_>, it: &Item<'_>) { | |
cdc7bbd5 | 112 | // do not lint public items or in macros |
17df50a5 | 113 | if in_external_macro(cx.sess(), it.span) |
2b03887a | 114 | || (self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(it.owner_id.def_id)) |
17df50a5 XL |
115 | { |
116 | return; | |
117 | } | |
118 | match it.kind { | |
119 | ItemKind::TyAlias(..) | ItemKind::Struct(..) | ItemKind::Trait(..) => { | |
4b012472 | 120 | check_ident(cx, &it.ident, it.hir_id(), self.upper_case_acronyms_aggressive); |
17df50a5 XL |
121 | }, |
122 | ItemKind::Enum(ref enumdef, _) => { | |
4b012472 | 123 | check_ident(cx, &it.ident, it.hir_id(), self.upper_case_acronyms_aggressive); |
5e7ed085 | 124 | // check enum variants separately because again we only want to lint on private enums and |
cdc7bbd5 | 125 | // the fn check_variant does not know about the vis of the enum of its variants |
4b012472 FG |
126 | enumdef.variants.iter().for_each(|variant| { |
127 | check_ident(cx, &variant.ident, variant.hir_id, self.upper_case_acronyms_aggressive); | |
128 | }); | |
17df50a5 XL |
129 | }, |
130 | _ => {}, | |
f20569fa XL |
131 | } |
132 | } | |
f20569fa | 133 | } |