diff --git a/clang-tools-extra/clangd/ParsedAST.h b/clang-tools-extra/clangd/ParsedAST.h --- a/clang-tools-extra/clangd/ParsedAST.h +++ b/clang-tools-extra/clangd/ParsedAST.h @@ -41,6 +41,7 @@ #include namespace clang { +class Sema; namespace clangd { class HeuristicResolver; class SymbolIndex; @@ -68,6 +69,8 @@ ASTContext &getASTContext(); const ASTContext &getASTContext() const; + Sema &getSema(); + Preprocessor &getPreprocessor(); std::shared_ptr getPreprocessorPtr(); const Preprocessor &getPreprocessor() const; diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp --- a/clang-tools-extra/clangd/ParsedAST.cpp +++ b/clang-tools-extra/clangd/ParsedAST.cpp @@ -558,6 +558,8 @@ return Clang->getASTContext(); } +Sema &ParsedAST::getSema() { return Clang->getSema(); } + Preprocessor &ParsedAST::getPreprocessor() { return Clang->getPreprocessor(); } std::shared_ptr ParsedAST::getPreprocessorPtr() { diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt --- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt +++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -25,6 +25,7 @@ PopulateSwitch.cpp RawStringLiteral.cpp RemoveUsingNamespace.cpp + SpecialMembers.cpp SwapIfBranches.cpp LINK_LIBS diff --git a/clang-tools-extra/clangd/refactor/tweaks/SpecialMembers.cpp b/clang-tools-extra/clangd/refactor/tweaks/SpecialMembers.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/SpecialMembers.cpp @@ -0,0 +1,196 @@ +//===--- SpecialMembers.cpp - Generate C++ special member functions -------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "ParsedAST.h" +#include "refactor/Tweak.h" +#include "support/Logger.h" +#include "clang/AST/DeclCXX.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Sema/Sema.h" +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Error.h" + +namespace clang { +namespace clangd { +namespace { + +// Returns code to declare missing copy/move constructors/assignment operators. +// They will be deleted or defaulted to match the class's current state. +std::string buildSpecialMemberDeclarations(const CXXRecordDecl &Class) { + struct Members { + const CXXMethodDecl *Copy = nullptr; + const CXXMethodDecl *Move = nullptr; + } Ctor, Assign; + + for (const auto &M : Class.methods()) { + if (M->isCopyAssignmentOperator()) + Assign.Copy = M; + else if (M->isMoveAssignmentOperator()) + Assign.Move = M; + if (const auto *C = llvm::dyn_cast(M)) { + if (C->isCopyConstructor()) + Ctor.Copy = C; + else if (C->isMoveConstructor()) + Ctor.Move = C; + } + } + + std::string S; + llvm::raw_string_ostream OS(S); + + auto PrintMember = [&](const CXXMethodDecl *D, const char *MemberPattern, + const char *ParmPattern) { + if (D && !D->isImplicit()) + return; + bool Delete = !D || D->isDeleted(); + OS << llvm::formatv( + "{0} = {1};\n", + llvm::formatv(MemberPattern, Class.getName(), + llvm::formatv(ParmPattern, Class.getName())), + Delete ? "delete" : "default"); + }; + auto PrintMembers = [&](const Members &M, const char *MemberPattern) { + PrintMember(M.Copy, MemberPattern, /*ParmPattern=*/"const {0}&"); + PrintMember(M.Move, MemberPattern, /*ParmPattern=*/"{0}&&"); + }; + PrintMembers(Ctor, /*MemberPattern=*/"{0}({1})"); + PrintMembers(Assign, /*MemberPattern=*/"{0} &operator=({1})"); + + return S; +} + +// Chooses where in the class definition to insert constructors/operators. +// If there is any public constructor, we insert after the last one. +// Otherwise we insert at the top of the first specified public section. +// If no public section is specified in a struct, insert at the top. +// If no public section is specified in a class, set NeedsPublic. +SourceLocation chooseSpecialMemberInsertionPoint(const CXXRecordDecl &Class, + bool &NeedsPublic) { + SourceLocation InFirstPublic; + SourceLocation AfterLastPublicCtor; + SourceLocation FirstDecl; + SourceLocation RBrace = Class.getBraceRange().getEnd(); + + // We want a slightly odd loop here, pairing each declaration with + // the location *after* it. + auto SawDecl = [&](const Decl *D, SourceLocation NextLoc) { + if (FirstDecl.isInvalid()) + FirstDecl = D->getSourceRange().getBegin(); + if (InFirstPublic.isInvalid()) { + if (auto *ASD = llvm::dyn_cast(D)) { + if (ASD->getAccess() == AS_public) + InFirstPublic = NextLoc; + } + } + if (llvm::isa(D) && D->getAccess() == AS_public) + AfterLastPublicCtor = NextLoc; + }; + + const Decl *Prev = nullptr; + for (const auto *D : Class.decls()) { + if (D->isImplicit()) + continue; + if (Prev) + SawDecl(Prev, D->getSourceRange().getBegin()); + Prev = D; + } + if (Prev) + SawDecl(Prev, RBrace); + + // We've gathered all the locations, pick the best insertion point. + if (AfterLastPublicCtor.isValid()) + return AfterLastPublicCtor; + if (InFirstPublic.isValid()) + return InFirstPublic; + if (Class.getTagKind() == TTK_Class) { + NeedsPublic = true; + // Insert at the end, to avoid inserting a second access specifier. + return RBrace; + } + return FirstDecl.isValid() ? FirstDecl : RBrace; +} + +// A tweak that adds missing declarations of copy & move constructors. +// +// e.g. given `struct S{};`, produces: +// struct S { +// S(const S&) = default; +// S(S&&) = default; +// S &operator=(const S&) = default; +// S &operator=(S&&) = default; +// }; +// +// Added members are defaulted or deleted to approximately preserve semantics. +// (May not be a strict no-op when they were not implicitly declared). +// +// Having these spelled out is useful: +// - to understand the implicit behavior +// - to avoid relying on the implicit behavior +// - as a baseline for explicit modification +class DeclareCopyMove : public Tweak { +public: + const char *id() const override final; + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } + std::string title() const override { + return llvm::formatv("declare implicit {0} members", + NeedCopy ? NeedMove ? "copy/move" : "copy" : "move"); + } + + bool prepare(const Selection &Inputs) override { + // This tweak relies on =default and =delete. + if (!Inputs.AST->getLangOpts().CPlusPlus11) + return false; + + // Trigger only on class definitions. + if (auto *N = Inputs.ASTSelection.commonAncestor()) + Class = const_cast(N->ASTNode.get()); + if (!Class || !Class->isThisDeclarationADefinition()) + return false; + + // Tweak is only available if some members are missing. + NeedCopy = !Class->hasUserDeclaredCopyConstructor() || + !Class->hasUserDeclaredCopyAssignment(); + NeedMove = !Class->hasUserDeclaredMoveAssignment() || + !Class->hasUserDeclaredMoveConstructor(); + return NeedCopy || NeedMove; + } + + Expected apply(const Selection &Inputs) override { + // We need to ensure implicit special members exist so we can tell whether + // they should be =default or =delete. + Inputs.AST->getSema().ForceDeclarationOfImplicitMembers(Class); + std::string Code = buildSpecialMemberDeclarations(*Class); + + bool NeedsPublic = false; + SourceLocation InsertBefore = + chooseSpecialMemberInsertionPoint(*Class, NeedsPublic); + if (NeedsPublic) + Code = "public:\n" + Code; + + const auto &SM = Inputs.AST->getSourceManager(); + if (!SM.isInMainFile(InsertBefore)) + return error("Class body not in the main file! {0}", + InsertBefore.printToString(SM)); + return Effect::mainFileEdit( + Inputs.AST->getSourceManager(), + tooling::Replacements(tooling::Replacement(SM, InsertBefore, 0, Code))); + } + +private: + bool NeedCopy, NeedMove; + CXXRecordDecl *Class = nullptr; +}; +REGISTER_TWEAK(DeclareCopyMove) + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/tweaks/SpecialMembersTests.cpp b/clang-tools-extra/clangd/unittests/tweaks/SpecialMembersTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/tweaks/SpecialMembersTests.cpp @@ -0,0 +1,47 @@ +//===-- SpecialMembersTests.cpp -------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "TweakTesting.h" +#include "gmock/gmock-matchers.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TWEAK_TEST(SpecialMembers); + +TEST_F(SpecialMembersTest, Test) { + EXPECT_AVAILABLE("struct ^S {};"); + EXPECT_UNAVAILABLE("struct S { ^ };"); + EXPECT_UNAVAILABLE("union ^U {};"); + EXPECT_AVAILABLE("struct ^S { S(const S&); S(S&&); };"); + EXPECT_UNAVAILABLE("struct ^S {" + "S(const S&); S(S&&);" + "S &operator=(S&&); S &operator=(const S&);" + "};"); + + const char *Output = R"cpp(struct S{S(const S &) = default; + S(S &&) = default; + S &operator=(const S &) = default; + S &operator=(S &&) = default; +};)cpp"; + EXPECT_EQ(apply("struct ^S{};"), Output); + + Output = R"cpp(struct S{S(const S &) = default; +S(S &&) = default; +S &operator=(const S &) = delete; +S &operator=(S &&) = delete; +int& ref;};)cpp"; + EXPECT_EQ(apply("struct ^S{int& ref;};"), Output); +} + +} // namespace +} // namespace clangd +} // namespace clang