summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/upper_case_acronyms.rs
blob: 1d2d3eb12e1147fd71a1bb14191e77f9da6972b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
    /// ```rust
    /// struct HTTPResponse;
    /// ```
    /// Use instead:
    /// ```rust
    /// 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 [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));
            },
            _ => {},
        }
    }
}