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