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