clang-tools 24.0.0git
VirtualClassDestructorCheck.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
10#include "../utils/LexerUtils.h"
11#include "clang/AST/ASTContext.h"
12#include "clang/ASTMatchers/ASTMatchFinder.h"
13#include "clang/Lex/Lexer.h"
14#include <optional>
15#include <string>
16
17using namespace clang::ast_matchers;
18
20
21namespace {
22
23AST_MATCHER(CXXRecordDecl, hasPublicVirtualOrProtectedNonVirtualDestructor) {
24 // We need to call Node.getDestructor() instead of matching a
25 // CXXDestructorDecl. Otherwise, tests will fail for class templates, since
26 // the primary template (not the specialization) always gets a non-virtual
27 // CXXDestructorDecl in the AST. https://bugs.llvm.org/show_bug.cgi?id=51912
28 const CXXDestructorDecl *Destructor = Node.getDestructor();
29 if (!Destructor)
30 return false;
31
32 return (((Destructor->getAccess() == AccessSpecifier::AS_public) &&
33 Destructor->isVirtual()) ||
34 ((Destructor->getAccess() == AccessSpecifier::AS_protected) &&
35 !Destructor->isVirtual()));
36}
37
38} // namespace
39
41 ast_matchers::internal::Matcher<CXXRecordDecl> InheritsVirtualMethod =
42 hasAnyBase(hasType(cxxRecordDecl(has(cxxMethodDecl(isVirtual())))));
43
44 Finder->addMatcher(
45 cxxRecordDecl(
46 anyOf(has(cxxMethodDecl(isVirtual())), InheritsVirtualMethod),
47 unless(isFinal()),
48 unless(hasPublicVirtualOrProtectedNonVirtualDestructor()))
49 .bind("ProblematicClassOrStruct"),
50 this);
51}
52
53static std::optional<CharSourceRange>
54getVirtualKeywordRange(const CXXDestructorDecl &Destructor,
55 const SourceManager &SM, const LangOptions &LangOpts) {
56 if (Destructor.getLocation().isMacroID())
57 return std::nullopt;
58
59 const SourceLocation VirtualBeginLoc = Destructor.getBeginLoc();
60 const SourceLocation VirtualBeginSpellingLoc =
61 SM.getSpellingLoc(Destructor.getBeginLoc());
62 const SourceLocation VirtualEndLoc = VirtualBeginSpellingLoc.getLocWithOffset(
63 Lexer::MeasureTokenLength(VirtualBeginSpellingLoc, SM, LangOpts));
64
65 /// Range ends with \c StartOfNextToken so that any whitespace after \c
66 /// virtual is included.
67 std::optional<Token> NextToken =
68 utils::lexer::findNextTokenSkippingComments(VirtualEndLoc, SM, LangOpts);
69 if (!NextToken)
70 return std::nullopt;
71 const SourceLocation StartOfNextToken = NextToken->getLocation();
72
73 return CharSourceRange::getCharRange(VirtualBeginLoc, StartOfNextToken);
74}
75
76static const AccessSpecDecl *
77getPublicASDecl(const CXXRecordDecl &StructOrClass) {
78 for (DeclContext::specific_decl_iterator<AccessSpecDecl>
79 AS{StructOrClass.decls_begin()},
80 ASEnd{StructOrClass.decls_end()};
81 AS != ASEnd; ++AS) {
82 const AccessSpecDecl *ASDecl = *AS;
83 if (ASDecl->getAccess() == AccessSpecifier::AS_public)
84 return ASDecl;
85 }
86
87 return nullptr;
88}
89
90static FixItHint
91generateUserDeclaredDestructor(const CXXRecordDecl &StructOrClass,
92 const SourceManager &SourceManager) {
93 std::string DestructorString;
94 SourceLocation Loc;
95 bool AppendLineBreak = false;
96
97 const AccessSpecDecl *AccessSpecDecl = getPublicASDecl(StructOrClass);
98
99 if (!AccessSpecDecl) {
100 if (StructOrClass.isClass()) {
101 Loc = StructOrClass.getEndLoc();
102 DestructorString = "public:";
103 AppendLineBreak = true;
104 } else {
105 Loc = StructOrClass.getBraceRange().getBegin().getLocWithOffset(1);
106 }
107 } else {
108 Loc = AccessSpecDecl->getEndLoc().getLocWithOffset(1);
109 }
110
111 DestructorString = (llvm::Twine(DestructorString) + "\nvirtual ~" +
112 StructOrClass.getName().str() + "() = default;" +
113 (AppendLineBreak ? "\n" : ""))
114 .str();
115
116 return FixItHint::CreateInsertion(Loc, DestructorString);
117}
118
119static std::string getSourceText(const CXXDestructorDecl &Destructor) {
120 std::string SourceText;
121 llvm::raw_string_ostream DestructorStream(SourceText);
122 Destructor.print(DestructorStream);
123 return SourceText;
124}
125
126static std::string eraseKeyword(std::string &DestructorString,
127 const std::string &Keyword) {
128 const size_t KeywordIndex = DestructorString.find(Keyword);
129 if (KeywordIndex != std::string::npos)
130 DestructorString.erase(KeywordIndex, Keyword.length());
131 return DestructorString;
132}
133
135 const std::string &Visibility, const CXXDestructorDecl &Destructor,
136 const SourceManager &SM, const LangOptions &LangOpts) {
137 std::string DestructorString =
138 (llvm::Twine() + Visibility + ":\n" +
139 (Visibility == "public" && !Destructor.isVirtual() ? "virtual " : ""))
140 .str();
141
142 std::string OriginalDestructor = getSourceText(Destructor);
143 if (Visibility == "protected" && Destructor.isVirtualAsWritten())
144 OriginalDestructor = eraseKeyword(OriginalDestructor, "virtual ");
145
146 DestructorString =
147 (llvm::Twine(DestructorString) + OriginalDestructor +
148 (Destructor.isExplicitlyDefaulted() ? ";\n" : "") + "private:")
149 .str();
150
151 /// Semicolons ending an explicitly defaulted destructor have to be deleted.
152 /// Otherwise, the left-over semicolon trails the \c private: access
153 /// specifier.
154 SourceLocation EndLocation;
155 if (Destructor.isExplicitlyDefaulted())
156 EndLocation =
157 utils::lexer::findNextTerminator(Destructor.getEndLoc(), SM, LangOpts)
158 .getLocWithOffset(1);
159 else
160 EndLocation = Destructor.getEndLoc().getLocWithOffset(1);
161
162 const auto OriginalDestructorRange =
163 CharSourceRange::getCharRange(Destructor.getBeginLoc(), EndLocation);
164 return FixItHint::CreateReplacement(OriginalDestructorRange,
165 DestructorString);
166}
167
169 const MatchFinder::MatchResult &Result) {
170 const auto *MatchedClassOrStruct =
171 Result.Nodes.getNodeAs<CXXRecordDecl>("ProblematicClassOrStruct");
172
173 const CXXDestructorDecl *Destructor = MatchedClassOrStruct->getDestructor();
174 if (!Destructor)
175 return;
176
177 const bool HasUserDeclaredDtor =
178 MatchedClassOrStruct->hasUserDeclaredDestructor();
179
180 const SourceLocation DiagLoc = HasUserDeclaredDtor
181 ? Destructor->getLocation()
182 : MatchedClassOrStruct->getLocation();
183
184 if (Destructor->getAccess() == AccessSpecifier::AS_private) {
185 diag(DiagLoc, "destructor of %0 is private and prevents using the type")
186 << MatchedClassOrStruct;
187 diag(DiagLoc,
188 /*Description=*/"make it public and virtual", DiagnosticIDs::Note)
190 "public", *Destructor, *Result.SourceManager, getLangOpts());
191 diag(DiagLoc,
192 /*Description=*/"make it protected", DiagnosticIDs::Note)
194 "protected", *Destructor, *Result.SourceManager, getLangOpts());
195
196 return;
197 }
198
199 // Implicit destructors are public and non-virtual for classes and structs.
200 bool ProtectedAndVirtual = false;
201 FixItHint Fix;
202
203 if (HasUserDeclaredDtor) {
204 if (Destructor->getAccess() == AccessSpecifier::AS_public) {
205 Fix = FixItHint::CreateInsertion(Destructor->getLocation(), "virtual ");
206 } else if (Destructor->getAccess() == AccessSpecifier::AS_protected) {
207 ProtectedAndVirtual = true;
208 if (const auto MaybeRange =
209 getVirtualKeywordRange(*Destructor, *Result.SourceManager,
210 Result.Context->getLangOpts()))
211 Fix = FixItHint::CreateRemoval(*MaybeRange);
212 }
213 } else {
214 Fix = generateUserDeclaredDestructor(*MatchedClassOrStruct,
215 *Result.SourceManager);
216 }
217
218 diag(DiagLoc,
219 "destructor of %0 is %select{public and non-virtual|protected and "
220 "virtual}1")
221 << MatchedClassOrStruct << ProtectedAndVirtual;
222 diag(DiagLoc,
223 "make it %select{public and virtual|protected and non-virtual}0",
224 DiagnosticIDs::Note)
225 << ProtectedAndVirtual << Fix;
226}
227
228} // namespace clang::tidy::cppcoreguidelines
static cl::opt< bool > Fix("fix", desc(R"( Apply suggested fixes. Without -fix-errors clang-tidy will bail out if any compilation errors were found. )"), cl::init(false), cl::cat(ClangTidyCategory))
void registerMatchers(ast_matchers::MatchFinder *Finder) override
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
static std::string eraseKeyword(std::string &DestructorString, const std::string &Keyword)
static const AccessSpecDecl * getPublicASDecl(const CXXRecordDecl &StructOrClass)
static FixItHint changePrivateDestructorVisibilityTo(const std::string &Visibility, const CXXDestructorDecl &Destructor, const SourceManager &SM, const LangOptions &LangOpts)
static std::string getSourceText(const CXXDestructorDecl &Destructor)
static std::optional< CharSourceRange > getVirtualKeywordRange(const CXXDestructorDecl &Destructor, const SourceManager &SM, const LangOptions &LangOpts)
static FixItHint generateUserDeclaredDestructor(const CXXRecordDecl &StructOrClass, const SourceManager &SourceManager)
AST_MATCHER(BinaryOperator, isRelationalOperator)
SourceLocation findNextTerminator(SourceLocation Start, const SourceManager &SM, const LangOptions &LangOpts)
std::optional< Token > findNextTokenSkippingComments(SourceLocation Start, const SourceManager &SM, const LangOptions &LangOpts)
Definition LexerUtils.h:106