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