1 | //===--- EnumInitialValueCheck.cpp - clang-tidy ---------------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "EnumInitialValueCheck.h" |
10 | #include "../utils/LexerUtils.h" |
11 | #include "clang/AST/Decl.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/ASTMatchers/ASTMatchers.h" |
14 | #include "clang/Basic/Diagnostic.h" |
15 | #include "clang/Basic/SourceLocation.h" |
16 | #include "llvm/ADT/STLExtras.h" |
17 | #include "llvm/ADT/SmallString.h" |
18 | |
19 | using namespace clang::ast_matchers; |
20 | |
21 | namespace clang::tidy::readability { |
22 | |
23 | static bool isNoneEnumeratorsInitialized(const EnumDecl &Node) { |
24 | return llvm::all_of(Range: Node.enumerators(), P: [](const EnumConstantDecl *ECD) { |
25 | return ECD->getInitExpr() == nullptr; |
26 | }); |
27 | } |
28 | |
29 | static bool isOnlyFirstEnumeratorInitialized(const EnumDecl &Node) { |
30 | bool IsFirst = true; |
31 | for (const EnumConstantDecl *ECD : Node.enumerators()) { |
32 | if ((IsFirst && ECD->getInitExpr() == nullptr) || |
33 | (!IsFirst && ECD->getInitExpr() != nullptr)) |
34 | return false; |
35 | IsFirst = false; |
36 | } |
37 | return !IsFirst; |
38 | } |
39 | |
40 | static bool areAllEnumeratorsInitialized(const EnumDecl &Node) { |
41 | return llvm::all_of(Range: Node.enumerators(), P: [](const EnumConstantDecl *ECD) { |
42 | return ECD->getInitExpr() != nullptr; |
43 | }); |
44 | } |
45 | |
46 | /// Check if \p Enumerator is initialized with a (potentially negated) \c |
47 | /// IntegerLiteral. |
48 | static bool isInitializedByLiteral(const EnumConstantDecl *Enumerator) { |
49 | const Expr *const Init = Enumerator->getInitExpr(); |
50 | if (!Init) |
51 | return false; |
52 | return Init->isIntegerConstantExpr(Ctx: Enumerator->getASTContext()); |
53 | } |
54 | |
55 | static void cleanInitialValue(DiagnosticBuilder &Diag, |
56 | const EnumConstantDecl *ECD, |
57 | const SourceManager &SM, |
58 | const LangOptions &LangOpts) { |
59 | const SourceRange InitExprRange = ECD->getInitExpr()->getSourceRange(); |
60 | if (InitExprRange.isInvalid() || InitExprRange.getBegin().isMacroID() || |
61 | InitExprRange.getEnd().isMacroID()) |
62 | return; |
63 | std::optional<Token> EqualToken = utils::lexer::findNextTokenSkippingComments( |
64 | Start: ECD->getLocation(), SM, LangOpts); |
65 | if (!EqualToken.has_value() || |
66 | EqualToken.value().getKind() != tok::TokenKind::equal) |
67 | return; |
68 | const SourceLocation EqualLoc{EqualToken->getLocation()}; |
69 | if (EqualLoc.isInvalid() || EqualLoc.isMacroID()) |
70 | return; |
71 | Diag << FixItHint::CreateRemoval(RemoveRange: EqualLoc) |
72 | << FixItHint::CreateRemoval(RemoveRange: InitExprRange); |
73 | } |
74 | |
75 | namespace { |
76 | |
77 | AST_MATCHER(EnumDecl, isMacro) { |
78 | SourceLocation Loc = Node.getBeginLoc(); |
79 | return Loc.isMacroID(); |
80 | } |
81 | |
82 | AST_MATCHER(EnumDecl, hasConsistentInitialValues) { |
83 | return isNoneEnumeratorsInitialized(Node) || |
84 | isOnlyFirstEnumeratorInitialized(Node) || |
85 | areAllEnumeratorsInitialized(Node); |
86 | } |
87 | |
88 | AST_MATCHER(EnumDecl, hasZeroInitialValueForFirstEnumerator) { |
89 | const EnumDecl::enumerator_range Enumerators = Node.enumerators(); |
90 | if (Enumerators.empty()) |
91 | return false; |
92 | const EnumConstantDecl *ECD = *Enumerators.begin(); |
93 | return isOnlyFirstEnumeratorInitialized(Node) && |
94 | isInitializedByLiteral(Enumerator: ECD) && ECD->getInitVal().isZero(); |
95 | } |
96 | |
97 | /// Excludes bitfields because enumerators initialized with the result of a |
98 | /// bitwise operator on enumeration values or any other expr that is not a |
99 | /// potentially negative integer literal. |
100 | /// Enumerations where it is not directly clear if they are used with |
101 | /// bitmask, evident when enumerators are only initialized with (potentially |
102 | /// negative) integer literals, are ignored. This is also the case when all |
103 | /// enumerators are powers of two (e.g., 0, 1, 2). |
104 | AST_MATCHER(EnumDecl, hasSequentialInitialValues) { |
105 | const EnumDecl::enumerator_range Enumerators = Node.enumerators(); |
106 | if (Enumerators.empty()) |
107 | return false; |
108 | const EnumConstantDecl *const FirstEnumerator = *Node.enumerator_begin(); |
109 | llvm::APSInt PrevValue = FirstEnumerator->getInitVal(); |
110 | if (!isInitializedByLiteral(Enumerator: FirstEnumerator)) |
111 | return false; |
112 | bool AllEnumeratorsArePowersOfTwo = true; |
113 | for (const EnumConstantDecl *Enumerator : llvm::drop_begin(RangeOrContainer: Enumerators)) { |
114 | const llvm::APSInt NewValue = Enumerator->getInitVal(); |
115 | if (NewValue != ++PrevValue) |
116 | return false; |
117 | if (!isInitializedByLiteral(Enumerator)) |
118 | return false; |
119 | PrevValue = NewValue; |
120 | AllEnumeratorsArePowersOfTwo &= NewValue.isPowerOf2(); |
121 | } |
122 | return !AllEnumeratorsArePowersOfTwo; |
123 | } |
124 | |
125 | std::string getName(const EnumDecl *Decl) { |
126 | if (!Decl->getDeclName()) |
127 | return "<unnamed>" ; |
128 | |
129 | return Decl->getQualifiedNameAsString(); |
130 | } |
131 | |
132 | } // namespace |
133 | |
134 | EnumInitialValueCheck::EnumInitialValueCheck(StringRef Name, |
135 | ClangTidyContext *Context) |
136 | : ClangTidyCheck(Name, Context), |
137 | AllowExplicitZeroFirstInitialValue( |
138 | Options.get(LocalName: "AllowExplicitZeroFirstInitialValue" , Default: true)), |
139 | AllowExplicitSequentialInitialValues( |
140 | Options.get(LocalName: "AllowExplicitSequentialInitialValues" , Default: true)) {} |
141 | |
142 | void EnumInitialValueCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
143 | Options.store(Options&: Opts, LocalName: "AllowExplicitZeroFirstInitialValue" , |
144 | Value: AllowExplicitZeroFirstInitialValue); |
145 | Options.store(Options&: Opts, LocalName: "AllowExplicitSequentialInitialValues" , |
146 | Value: AllowExplicitSequentialInitialValues); |
147 | } |
148 | |
149 | void EnumInitialValueCheck::registerMatchers(MatchFinder *Finder) { |
150 | Finder->addMatcher(NodeMatch: enumDecl(isDefinition(), unless(isMacro()), |
151 | unless(hasConsistentInitialValues())) |
152 | .bind(ID: "inconsistent" ), |
153 | Action: this); |
154 | if (!AllowExplicitZeroFirstInitialValue) |
155 | Finder->addMatcher( |
156 | NodeMatch: enumDecl(isDefinition(), hasZeroInitialValueForFirstEnumerator()) |
157 | .bind(ID: "zero_first" ), |
158 | Action: this); |
159 | if (!AllowExplicitSequentialInitialValues) |
160 | Finder->addMatcher(NodeMatch: enumDecl(isDefinition(), unless(isMacro()), |
161 | hasSequentialInitialValues()) |
162 | .bind(ID: "sequential" ), |
163 | Action: this); |
164 | } |
165 | |
166 | void EnumInitialValueCheck::check(const MatchFinder::MatchResult &Result) { |
167 | if (const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>(ID: "inconsistent" )) { |
168 | DiagnosticBuilder Diag = |
169 | diag( |
170 | Enum->getBeginLoc(), |
171 | "initial values in enum '%0' are not consistent, consider explicit " |
172 | "initialization of all, none or only the first enumerator" ) |
173 | << getName(Decl: Enum); |
174 | for (const EnumConstantDecl *ECD : Enum->enumerators()) |
175 | if (ECD->getInitExpr() == nullptr) { |
176 | const SourceLocation EndLoc = Lexer::getLocForEndOfToken( |
177 | Loc: ECD->getLocation(), Offset: 0, SM: *Result.SourceManager, LangOpts: getLangOpts()); |
178 | if (EndLoc.isMacroID()) |
179 | continue; |
180 | llvm::SmallString<8> Str{" = " }; |
181 | ECD->getInitVal().toString(Str); |
182 | Diag << FixItHint::CreateInsertion(InsertionLoc: EndLoc, Code: Str); |
183 | } |
184 | return; |
185 | } |
186 | |
187 | if (const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>(ID: "zero_first" )) { |
188 | const EnumConstantDecl *ECD = *Enum->enumerator_begin(); |
189 | const SourceLocation Loc = ECD->getLocation(); |
190 | if (Loc.isInvalid() || Loc.isMacroID()) |
191 | return; |
192 | DiagnosticBuilder Diag = diag(Loc, Description: "zero initial value for the first " |
193 | "enumerator in '%0' can be disregarded" ) |
194 | << getName(Decl: Enum); |
195 | cleanInitialValue(Diag, ECD, SM: *Result.SourceManager, LangOpts: getLangOpts()); |
196 | return; |
197 | } |
198 | if (const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>(ID: "sequential" )) { |
199 | DiagnosticBuilder Diag = |
200 | diag(Enum->getBeginLoc(), |
201 | "sequential initial value in '%0' can be ignored" ) |
202 | << getName(Decl: Enum); |
203 | for (const EnumConstantDecl *ECD : llvm::drop_begin(RangeOrContainer: Enum->enumerators())) |
204 | cleanInitialValue(Diag, ECD, SM: *Result.SourceManager, LangOpts: getLangOpts()); |
205 | return; |
206 | } |
207 | } |
208 | |
209 | } // namespace clang::tidy::readability |
210 | |