1 | //===--- SuspiciousEnumUsageCheck.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 "SuspiciousEnumUsageCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
12 | #include <algorithm> |
13 | |
14 | using namespace clang::ast_matchers; |
15 | |
16 | namespace clang::tidy::bugprone { |
17 | |
18 | static const char DifferentEnumErrorMessage[] = |
19 | "enum values are from different enum types" ; |
20 | |
21 | static const char BitmaskErrorMessage[] = |
22 | "enum type seems like a bitmask (contains mostly " |
23 | "power-of-2 literals), but this literal is not a " |
24 | "power-of-2" ; |
25 | |
26 | static const char BitmaskVarErrorMessage[] = |
27 | "enum type seems like a bitmask (contains mostly " |
28 | "power-of-2 literals) but %plural{1:a literal is|:some literals are}0 not " |
29 | "power-of-2" ; |
30 | |
31 | static const char BitmaskNoteMessage[] = "used here as a bitmask" ; |
32 | |
33 | /// Stores a min and a max value which describe an interval. |
34 | struct ValueRange { |
35 | llvm::APSInt MinVal; |
36 | llvm::APSInt MaxVal; |
37 | |
38 | ValueRange(const EnumDecl *EnumDec) { |
39 | const auto MinMaxVal = std::minmax_element( |
40 | first: EnumDec->enumerator_begin(), last: EnumDec->enumerator_end(), |
41 | comp: [](const EnumConstantDecl *E1, const EnumConstantDecl *E2) { |
42 | return llvm::APSInt::compareValues(I1: E1->getInitVal(), |
43 | I2: E2->getInitVal()) < 0; |
44 | }); |
45 | MinVal = MinMaxVal.first->getInitVal(); |
46 | MaxVal = MinMaxVal.second->getInitVal(); |
47 | } |
48 | }; |
49 | |
50 | /// Return the number of EnumConstantDecls in an EnumDecl. |
51 | static int enumLength(const EnumDecl *EnumDec) { |
52 | return std::distance(first: EnumDec->enumerator_begin(), last: EnumDec->enumerator_end()); |
53 | } |
54 | |
55 | static bool hasDisjointValueRange(const EnumDecl *Enum1, |
56 | const EnumDecl *Enum2) { |
57 | ValueRange Range1(Enum1), Range2(Enum2); |
58 | return llvm::APSInt::compareValues(I1: Range1.MaxVal, I2: Range2.MinVal) < 0 || |
59 | llvm::APSInt::compareValues(I1: Range2.MaxVal, I2: Range1.MinVal) < 0; |
60 | } |
61 | |
62 | static bool isNonPowerOf2NorNullLiteral(const EnumConstantDecl *EnumConst) { |
63 | const llvm::APSInt &Val = EnumConst->getInitVal(); |
64 | if (Val.isPowerOf2() || !Val.getBoolValue()) |
65 | return false; |
66 | const Expr *InitExpr = EnumConst->getInitExpr(); |
67 | if (!InitExpr) |
68 | return true; |
69 | return isa<IntegerLiteral>(Val: InitExpr->IgnoreImpCasts()); |
70 | } |
71 | |
72 | static bool isMaxValAllBitSetLiteral(const EnumDecl *EnumDec) { |
73 | auto EnumConst = std::max_element( |
74 | first: EnumDec->enumerator_begin(), last: EnumDec->enumerator_end(), |
75 | comp: [](const EnumConstantDecl *E1, const EnumConstantDecl *E2) { |
76 | return E1->getInitVal() < E2->getInitVal(); |
77 | }); |
78 | |
79 | if (const Expr *InitExpr = EnumConst->getInitExpr()) { |
80 | return EnumConst->getInitVal().countr_one() == |
81 | EnumConst->getInitVal().getActiveBits() && |
82 | isa<IntegerLiteral>(Val: InitExpr->IgnoreImpCasts()); |
83 | } |
84 | return false; |
85 | } |
86 | |
87 | static int countNonPowOfTwoLiteralNum(const EnumDecl *EnumDec) { |
88 | return llvm::count_if(Range: EnumDec->enumerators(), P: isNonPowerOf2NorNullLiteral); |
89 | } |
90 | |
91 | /// Check if there is one or two enumerators that are not a power of 2 and are |
92 | /// initialized by a literal in the enum type, and that the enumeration contains |
93 | /// enough elements to reasonably act as a bitmask. Exclude the case where the |
94 | /// last enumerator is the sum of the lesser values (and initialized by a |
95 | /// literal) or when it could contain consecutive values. |
96 | static bool isPossiblyBitMask(const EnumDecl *EnumDec) { |
97 | ValueRange VR(EnumDec); |
98 | int EnumLen = enumLength(EnumDec); |
99 | int NonPowOfTwoCounter = countNonPowOfTwoLiteralNum(EnumDec); |
100 | return NonPowOfTwoCounter >= 1 && NonPowOfTwoCounter <= 2 && |
101 | NonPowOfTwoCounter < EnumLen / 2 && |
102 | (VR.MaxVal - VR.MinVal != EnumLen - 1) && |
103 | !(NonPowOfTwoCounter == 1 && isMaxValAllBitSetLiteral(EnumDec)); |
104 | } |
105 | |
106 | SuspiciousEnumUsageCheck::SuspiciousEnumUsageCheck(StringRef Name, |
107 | ClangTidyContext *Context) |
108 | : ClangTidyCheck(Name, Context), |
109 | StrictMode(Options.getLocalOrGlobal(LocalName: "StrictMode" , Default: false)) {} |
110 | |
111 | void SuspiciousEnumUsageCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
112 | Options.store(Options&: Opts, LocalName: "StrictMode" , Value: StrictMode); |
113 | } |
114 | |
115 | void SuspiciousEnumUsageCheck::registerMatchers(MatchFinder *Finder) { |
116 | const auto EnumExpr = [](StringRef RefName, StringRef DeclName) { |
117 | return expr(hasType(InnerMatcher: enumDecl().bind(ID: DeclName))).bind(ID: RefName); |
118 | }; |
119 | |
120 | Finder->addMatcher( |
121 | NodeMatch: binaryOperator( |
122 | hasOperatorName(Name: "|" ), hasLHS(InnerMatcher: hasType(InnerMatcher: enumDecl().bind(ID: "enumDecl" ))), |
123 | hasRHS(InnerMatcher: hasType(InnerMatcher: enumDecl(unless(equalsBoundNode(ID: "enumDecl" ))) |
124 | .bind(ID: "otherEnumDecl" )))) |
125 | .bind(ID: "diffEnumOp" ), |
126 | Action: this); |
127 | |
128 | Finder->addMatcher( |
129 | NodeMatch: binaryOperator(hasAnyOperatorName("+" , "|" ), |
130 | hasLHS(InnerMatcher: EnumExpr("lhsExpr" , "enumDecl" )), |
131 | hasRHS(InnerMatcher: expr(hasType(InnerMatcher: enumDecl(equalsBoundNode(ID: "enumDecl" )))) |
132 | .bind(ID: "rhsExpr" ))), |
133 | Action: this); |
134 | |
135 | Finder->addMatcher( |
136 | NodeMatch: binaryOperator( |
137 | hasAnyOperatorName("+" , "|" ), |
138 | hasOperands(Matcher1: expr(hasType(InnerMatcher: isInteger()), unless(hasType(InnerMatcher: enumDecl()))), |
139 | Matcher2: EnumExpr("enumExpr" , "enumDecl" ))), |
140 | Action: this); |
141 | |
142 | Finder->addMatcher(NodeMatch: binaryOperator(hasAnyOperatorName("|=" , "+=" ), |
143 | hasRHS(InnerMatcher: EnumExpr("enumExpr" , "enumDecl" ))), |
144 | Action: this); |
145 | } |
146 | |
147 | void SuspiciousEnumUsageCheck::checkSuspiciousBitmaskUsage( |
148 | const Expr *NodeExpr, const EnumDecl *EnumDec) { |
149 | const auto *EnumExpr = dyn_cast<DeclRefExpr>(Val: NodeExpr); |
150 | const auto *EnumConst = |
151 | EnumExpr ? dyn_cast<EnumConstantDecl>(Val: EnumExpr->getDecl()) : nullptr; |
152 | |
153 | // Report the parameter if necessary. |
154 | if (!EnumConst) { |
155 | diag(EnumDec->getInnerLocStart(), BitmaskVarErrorMessage) |
156 | << countNonPowOfTwoLiteralNum(EnumDec); |
157 | diag(EnumExpr->getExprLoc(), BitmaskNoteMessage, DiagnosticIDs::Note); |
158 | } else if (isNonPowerOf2NorNullLiteral(EnumConst)) { |
159 | diag(Loc: EnumConst->getSourceRange().getBegin(), Description: BitmaskErrorMessage); |
160 | diag(EnumExpr->getExprLoc(), BitmaskNoteMessage, DiagnosticIDs::Note); |
161 | } |
162 | } |
163 | |
164 | void SuspiciousEnumUsageCheck::check(const MatchFinder::MatchResult &Result) { |
165 | // Case 1: The two enum values come from different types. |
166 | if (const auto *DiffEnumOp = |
167 | Result.Nodes.getNodeAs<BinaryOperator>(ID: "diffEnumOp" )) { |
168 | const auto *EnumDec = Result.Nodes.getNodeAs<EnumDecl>(ID: "enumDecl" ); |
169 | const auto *OtherEnumDec = |
170 | Result.Nodes.getNodeAs<EnumDecl>(ID: "otherEnumDecl" ); |
171 | // Skip when one of the parameters is an empty enum. The |
172 | // hasDisjointValueRange function could not decide the values properly in |
173 | // case of an empty enum. |
174 | if (EnumDec->enumerator_begin() == EnumDec->enumerator_end() || |
175 | OtherEnumDec->enumerator_begin() == OtherEnumDec->enumerator_end()) |
176 | return; |
177 | |
178 | if (!hasDisjointValueRange(Enum1: EnumDec, Enum2: OtherEnumDec)) |
179 | diag(Loc: DiffEnumOp->getOperatorLoc(), Description: DifferentEnumErrorMessage); |
180 | return; |
181 | } |
182 | |
183 | // Case 2 and 3 only checked in strict mode. The checker tries to detect |
184 | // suspicious bitmasks which contains values initialized by non power-of-2 |
185 | // literals. |
186 | if (!StrictMode) |
187 | return; |
188 | const auto *EnumDec = Result.Nodes.getNodeAs<EnumDecl>(ID: "enumDecl" ); |
189 | if (!isPossiblyBitMask(EnumDec)) |
190 | return; |
191 | |
192 | // Case 2: |
193 | // a. Investigating the right hand side of `+=` or `|=` operator. |
194 | // b. When the operator is `|` or `+` but only one of them is an EnumExpr |
195 | if (const auto *EnumExpr = Result.Nodes.getNodeAs<Expr>(ID: "enumExpr" )) { |
196 | checkSuspiciousBitmaskUsage(NodeExpr: EnumExpr, EnumDec); |
197 | return; |
198 | } |
199 | |
200 | // Case 3: |
201 | // '|' or '+' operator where both argument comes from the same enum type |
202 | const auto *LhsExpr = Result.Nodes.getNodeAs<Expr>(ID: "lhsExpr" ); |
203 | checkSuspiciousBitmaskUsage(NodeExpr: LhsExpr, EnumDec); |
204 | |
205 | const auto *RhsExpr = Result.Nodes.getNodeAs<Expr>(ID: "rhsExpr" ); |
206 | checkSuspiciousBitmaskUsage(NodeExpr: RhsExpr, EnumDec); |
207 | } |
208 | |
209 | } // namespace clang::tidy::bugprone |
210 | |