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