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