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