1 | //===--- MakeMemberFunctionConstCheck.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 "MakeMemberFunctionConstCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/AST/ParentMapContext.h" |
12 | #include "clang/AST/RecursiveASTVisitor.h" |
13 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
14 | #include "clang/Lex/Lexer.h" |
15 | |
16 | using namespace clang::ast_matchers; |
17 | |
18 | namespace clang::tidy::readability { |
19 | |
20 | AST_MATCHER(CXXMethodDecl, isStatic) { return Node.isStatic(); } |
21 | |
22 | AST_MATCHER(CXXMethodDecl, hasTrivialBody) { return Node.hasTrivialBody(); } |
23 | |
24 | AST_MATCHER(CXXRecordDecl, hasAnyDependentBases) { |
25 | return Node.hasAnyDependentBases(); |
26 | } |
27 | |
28 | AST_MATCHER(CXXMethodDecl, isTemplate) { |
29 | return Node.getTemplatedKind() != FunctionDecl::TK_NonTemplate; |
30 | } |
31 | |
32 | AST_MATCHER(CXXMethodDecl, isDependentContext) { |
33 | return Node.isDependentContext(); |
34 | } |
35 | |
36 | AST_MATCHER(CXXMethodDecl, isInsideMacroDefinition) { |
37 | const ASTContext &Ctxt = Finder->getASTContext(); |
38 | return clang::Lexer::makeFileCharRange( |
39 | Range: clang::CharSourceRange::getCharRange( |
40 | Node.getTypeSourceInfo()->getTypeLoc().getSourceRange()), |
41 | SM: Ctxt.getSourceManager(), LangOpts: Ctxt.getLangOpts()) |
42 | .isInvalid(); |
43 | } |
44 | |
45 | AST_MATCHER_P(CXXMethodDecl, hasCanonicalDecl, |
46 | ast_matchers::internal::Matcher<CXXMethodDecl>, InnerMatcher) { |
47 | return InnerMatcher.matches(Node: *Node.getCanonicalDecl(), Finder, Builder); |
48 | } |
49 | |
50 | enum UsageKind { Unused, Const, NonConst }; |
51 | |
52 | class FindUsageOfThis : public RecursiveASTVisitor<FindUsageOfThis> { |
53 | ASTContext &Ctxt; |
54 | |
55 | public: |
56 | FindUsageOfThis(ASTContext &Ctxt) : Ctxt(Ctxt) {} |
57 | UsageKind Usage = Unused; |
58 | |
59 | template <class T> const T *getParent(const Expr *E) { |
60 | DynTypedNodeList Parents = Ctxt.getParents(Node: *E); |
61 | if (Parents.size() != 1) |
62 | return nullptr; |
63 | |
64 | return Parents.begin()->get<T>(); |
65 | } |
66 | |
67 | const Expr *getParentExprIgnoreParens(const Expr *E) { |
68 | const Expr *Parent = getParent<Expr>(E); |
69 | while (isa_and_nonnull<ParenExpr>(Val: Parent)) |
70 | Parent = getParent<Expr>(E: Parent); |
71 | return Parent; |
72 | } |
73 | |
74 | bool VisitUnresolvedMemberExpr(const UnresolvedMemberExpr *) { |
75 | // An UnresolvedMemberExpr might resolve to a non-const non-static |
76 | // member function. |
77 | Usage = NonConst; |
78 | return false; // Stop traversal. |
79 | } |
80 | |
81 | bool VisitCXXConstCastExpr(const CXXConstCastExpr *) { |
82 | // Workaround to support the pattern |
83 | // class C { |
84 | // const S *get() const; |
85 | // S* get() { |
86 | // return const_cast<S*>(const_cast<const C*>(this)->get()); |
87 | // } |
88 | // }; |
89 | // Here, we don't want to make the second 'get' const even though |
90 | // it only calls a const member function on this. |
91 | Usage = NonConst; |
92 | return false; // Stop traversal. |
93 | } |
94 | |
95 | // Our AST is |
96 | // `-ImplicitCastExpr |
97 | // (possibly `-UnaryOperator Deref) |
98 | // `-CXXThisExpr 'S *' this |
99 | bool visitUser(const ImplicitCastExpr *Cast) { |
100 | if (Cast->getCastKind() != CK_NoOp) |
101 | return false; // Stop traversal. |
102 | |
103 | // Only allow NoOp cast to 'const S' or 'const S *'. |
104 | QualType QT = Cast->getType(); |
105 | if (QT->isPointerType()) |
106 | QT = QT->getPointeeType(); |
107 | |
108 | if (!QT.isConstQualified()) |
109 | return false; // Stop traversal. |
110 | |
111 | const auto *Parent = getParent<Stmt>(Cast); |
112 | if (!Parent) |
113 | return false; // Stop traversal. |
114 | |
115 | if (isa<ReturnStmt>(Parent)) |
116 | return true; // return (const S*)this; |
117 | |
118 | if (isa<CallExpr>(Parent)) |
119 | return true; // use((const S*)this); |
120 | |
121 | // ((const S*)this)->Member |
122 | if (const auto *Member = dyn_cast<MemberExpr>(Parent)) |
123 | return visitUser(Member, /*OnConstObject=*/true); |
124 | |
125 | return false; // Stop traversal. |
126 | } |
127 | |
128 | // If OnConstObject is true, then this is a MemberExpr using |
129 | // a constant this, i.e. 'const S' or 'const S *'. |
130 | bool visitUser(const MemberExpr *Member, bool OnConstObject) { |
131 | if (Member->isBoundMemberFunction(Ctxt)) { |
132 | if (!OnConstObject || Member->getFoundDecl().getAccess() != AS_public) { |
133 | // Non-public non-static member functions might not preserve the |
134 | // logical constness. E.g. in |
135 | // class C { |
136 | // int &data() const; |
137 | // public: |
138 | // int &get() { return data(); } |
139 | // }; |
140 | // get() uses a private const method, but must not be made const |
141 | // itself. |
142 | return false; // Stop traversal. |
143 | } |
144 | // Using a public non-static const member function. |
145 | return true; |
146 | } |
147 | |
148 | const auto *Parent = getParentExprIgnoreParens(Member); |
149 | |
150 | if (const auto *Cast = dyn_cast_or_null<ImplicitCastExpr>(Parent)) { |
151 | // A read access to a member is safe when the member either |
152 | // 1) has builtin type (a 'const int' cannot be modified), |
153 | // 2) or it's a public member (the pointee of a public 'int * const' can |
154 | // can be modified by any user of the class). |
155 | if (Member->getFoundDecl().getAccess() != AS_public && |
156 | !Cast->getType()->isBuiltinType()) |
157 | return false; |
158 | |
159 | if (Cast->getCastKind() == CK_LValueToRValue) |
160 | return true; |
161 | |
162 | if (Cast->getCastKind() == CK_NoOp && Cast->getType().isConstQualified()) |
163 | return true; |
164 | } |
165 | |
166 | if (const auto *M = dyn_cast_or_null<MemberExpr>(Parent)) |
167 | return visitUser(M, /*OnConstObject=*/false); |
168 | |
169 | return false; // Stop traversal. |
170 | } |
171 | |
172 | bool VisitCXXThisExpr(const CXXThisExpr *E) { |
173 | Usage = Const; |
174 | |
175 | const auto *Parent = getParentExprIgnoreParens(E); |
176 | |
177 | // Look through deref of this. |
178 | if (const auto *UnOp = dyn_cast_or_null<UnaryOperator>(Parent)) { |
179 | if (UnOp->getOpcode() == UO_Deref) { |
180 | Parent = getParentExprIgnoreParens(E: UnOp); |
181 | } |
182 | } |
183 | |
184 | // It's okay to |
185 | // return (const S*)this; |
186 | // use((const S*)this); |
187 | // ((const S*)this)->f() |
188 | // when 'f' is a public member function. |
189 | if (const auto *Cast = dyn_cast_or_null<ImplicitCastExpr>(Parent)) { |
190 | if (visitUser(Cast)) |
191 | return true; |
192 | |
193 | // And it's also okay to |
194 | // (const T)(S->t) |
195 | // (LValueToRValue)(S->t) |
196 | // when 't' is either of builtin type or a public member. |
197 | } else if (const auto *Member = dyn_cast_or_null<MemberExpr>(Parent)) { |
198 | if (visitUser(Member, /*OnConstObject=*/false)) |
199 | return true; |
200 | } |
201 | |
202 | // Unknown user of this. |
203 | Usage = NonConst; |
204 | return false; // Stop traversal. |
205 | } |
206 | }; |
207 | |
208 | AST_MATCHER(CXXMethodDecl, usesThisAsConst) { |
209 | FindUsageOfThis UsageOfThis(Finder->getASTContext()); |
210 | |
211 | // TraverseStmt does not modify its argument. |
212 | UsageOfThis.TraverseStmt(S: const_cast<Stmt *>(Node.getBody())); |
213 | |
214 | return UsageOfThis.Usage == Const; |
215 | } |
216 | |
217 | void MakeMemberFunctionConstCheck::registerMatchers(MatchFinder *Finder) { |
218 | Finder->addMatcher( |
219 | NodeMatch: traverse( |
220 | TK: TK_AsIs, |
221 | InnerMatcher: cxxMethodDecl( |
222 | isDefinition(), isUserProvided(), |
223 | unless(anyOf( |
224 | isExpansionInSystemHeader(), isVirtual(), isConst(), |
225 | isStatic(), hasTrivialBody(), cxxConstructorDecl(), |
226 | cxxDestructorDecl(), isTemplate(), isDependentContext(), |
227 | ofClass(InnerMatcher: anyOf(isLambda(), |
228 | hasAnyDependentBases()) // Method might become |
229 | // virtual depending on |
230 | // template base class. |
231 | ), |
232 | isInsideMacroDefinition(), |
233 | hasCanonicalDecl(InnerMatcher: isInsideMacroDefinition()))), |
234 | usesThisAsConst()) |
235 | .bind(ID: "x" )), |
236 | Action: this); |
237 | } |
238 | |
239 | static SourceLocation getConstInsertionPoint(const CXXMethodDecl *M) { |
240 | TypeSourceInfo *TSI = M->getTypeSourceInfo(); |
241 | if (!TSI) |
242 | return {}; |
243 | |
244 | auto FTL = TSI->getTypeLoc().IgnoreParens().getAs<FunctionTypeLoc>(); |
245 | if (!FTL) |
246 | return {}; |
247 | |
248 | return FTL.getRParenLoc().getLocWithOffset(1); |
249 | } |
250 | |
251 | void MakeMemberFunctionConstCheck::check( |
252 | const MatchFinder::MatchResult &Result) { |
253 | const auto *Definition = Result.Nodes.getNodeAs<CXXMethodDecl>(ID: "x" ); |
254 | |
255 | const auto *Declaration = Definition->getCanonicalDecl(); |
256 | |
257 | auto Diag = diag(Definition->getLocation(), "method %0 can be made const" ) |
258 | << Definition |
259 | << FixItHint::CreateInsertion(InsertionLoc: getConstInsertionPoint(M: Definition), |
260 | Code: " const" ); |
261 | if (Declaration != Definition) { |
262 | Diag << FixItHint::CreateInsertion(InsertionLoc: getConstInsertionPoint(M: Declaration), |
263 | Code: " const" ); |
264 | } |
265 | } |
266 | |
267 | } // namespace clang::tidy::readability |
268 | |