Index: include/clang/Tooling/Refactoring/EditList.h =================================================================== --- /dev/null +++ include/clang/Tooling/Refactoring/EditList.h @@ -0,0 +1,129 @@ +//===--- EditList.h - Refactoring edits -------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file defines EditList which is used to create a set of source eidts, +// e.g. replacements and header insertions. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLING_REFACTOR_EDITLIST_H +#define LLVM_CLANG_TOOLING_REFACTOR_EDITLIST_H + +#include "clang/Basic/SourceManager.h" +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +namespace clang { +namespace tooling { + +/// \brief An edit list is used to create and group a set of source edits, e.g. +/// replacements or header insertions. Edits in an EditList should be related, +/// e.g. replacements for the same type reference and the corresponding header +/// insertion/deletion. +/// +/// An EditList is uniquely identified by a key position and will either be +/// fully applied or not applied at all. The key position should be the location +/// of the key syntactical element that is being changed, e.g. the call to a +/// refactored method. +/// +/// Calling setError on an EditList stores the error message and marks it as +/// bad, i.e. none of its source edits will be applied. +class EditList { +public: + /// \brief Creates an edit list for a key position. + EditList(const SourceManager &SM, SourceLocation KeyPosition); + + /// \brief Returns the edit as a YAML string. + std::string toYAMLString(); + + /// \brief Converts a YAML-encoded edit to EditList. + static EditList convertFromYAML(llvm::StringRef YAMLContent); + + /// \brief Returns the key of this edit, which is a concatenation of the file + /// name and offset of the key position. + std::string getKey() const { return Key; } + + /// \brief Returns the path of the file containing this edit. + std::string getFilePath() const { return FilePath; } + + /// \brief If this edit could not be created successfully, e.g. because of + /// conflicts among replacements, use this to set an error description. + /// Thereby, places that cannot be fixed automatically can be gathered when + /// applying edits. + void setError(llvm::StringRef Error) { this->Error = Error; } + + /// \brief Returns whether an error has been set on this list. + bool hasError() const { return !Error.empty(); } + + /// \brief Returns the error message or an empty string if it does not exist. + std::string getError() const { return Error; } + + /// \brief Adds a replacement \p R to the edit. + /// \returns An llvm::Error carrying ReplacementError on error. + llvm::Error addReplacement(const Replacement &R); + + /// \brief Merges `Rs` into the existing replacements with `Rs` referring to + /// the code after all existing replacements are applied. This enables + /// conflict resolving. + void mergeReplacements(const clang::tooling::Replacements &Rs); + + /// \brief Adds a replacement that inserts \p Text at \p Loc. If this + /// insertion conflicts with an existing insertion (at the same position), + /// this will be inserted before the existing insertion. If the conflicting + /// replacement is not an insertion, an error is returned. + /// + /// \returns An llvm::Error carrying ReplacementError on error. + llvm::Error insertBefore(const SourceManager &SM, SourceLocation Loc, + llvm::StringRef Text); + + /// \brief Same as `InsertBefore` except this inserts after the conflicting + /// insertion. + llvm::Error insertAfter(const SourceManager &SM, clang::SourceLocation Loc, + llvm::StringRef Text); + + /// \brief Adds a header into the file that contains the key position. + /// Header can be in angle brackets or double quotation marks. By default + /// (header is not quoted), header will be surrounded with double quotes. + void addHeader(llvm::StringRef Header); + + /// \brief Removes a header from the file that contains the key position. + void removeHeader(llvm::StringRef Header); + + /// \brief Returns a const reference to existing replacements. + const Replacements &getReplacements() const { return Replaces; } + + llvm::ArrayRef getInsertedHeaders() const { + return InsertedHeaders; + } + llvm::ArrayRef getRemovedHeaders() const { + return RemovedHeaders; + } + +private: + EditList() {} + + EditList(std::string Key, std::string FilePath, std::string Error, + std::vector InsertedHeaders, + std::vector RemovedHeaders, + clang::tooling::Replacements Replaces); + + // A concatenation of the file name and the offset of the key position. + std::string Key; + std::string FilePath; + std::string Error; + std::vector InsertedHeaders; + std::vector RemovedHeaders; + tooling::Replacements Replaces; +}; + +} // end namespace tooling +} // end namespace clang + +#endif // LLVM_CLANG_TOOLING_REFACTOR_EDITLIST_H Index: lib/Tooling/CMakeLists.txt =================================================================== --- lib/Tooling/CMakeLists.txt +++ lib/Tooling/CMakeLists.txt @@ -4,6 +4,7 @@ ) add_subdirectory(Core) +add_subdirectory(Refactoring) add_clang_library(clangTooling ArgumentsAdjusters.cpp Index: lib/Tooling/Refactoring/CMakeLists.txt =================================================================== --- /dev/null +++ lib/Tooling/Refactoring/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LLVM_LINK_COMPONENTS + Option + Support + ) + +add_clang_library(clangToolingRefactor + EditList.cpp + + LINK_LIBS + clangBasic + clangToolingCore + ) Index: lib/Tooling/Refactoring/EditList.cpp =================================================================== --- /dev/null +++ lib/Tooling/Refactoring/EditList.cpp @@ -0,0 +1,192 @@ +//===--- EditList.cpp - EditList implementation -----------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "clang/Tooling/Refactoring/EditList.h" +#include "clang/Tooling/ReplacementsYaml.h" +#include "llvm/Support/YAMLTraits.h" +#include + +LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(std::string) +LLVM_YAML_IS_SEQUENCE_VECTOR(clang::tooling::EditList) + +namespace { +/// \brief Helper to (de)serialize an EditList since we don't have direct +/// access to its data members. +/// Data members of a normalized editlist can be directly mapped from/to YAML +/// string. +struct NormalizedEditList { + NormalizedEditList() = default; + + NormalizedEditList(const llvm::yaml::IO &) {} + + // This converts EditList's internal implementation of the replacements set + // to a vector of replacements. + NormalizedEditList(const llvm::yaml::IO &, const clang::tooling::EditList &E) + : Key(E.getKey()), FilePath(E.getFilePath()), Error(E.getError()), + InsertedHeaders(E.getInsertedHeaders()), + RemovedHeaders(E.getRemovedHeaders()) { + std::copy(E.getReplacements().begin(), E.getReplacements().end(), + std::back_inserter(Replaces)); + } + + // This is not expected to be called but needed for template instantiation. + clang::tooling::EditList denormalize(const llvm::yaml::IO &) { + llvm_unreachable("Do not convert YAML to EditList directly with '>>'. " + "Use EditList::convertFromYAML instead."); + } + std::string Key; + std::string FilePath; + std::string Error; + std::vector InsertedHeaders; + std::vector RemovedHeaders; + std::vector Replaces; +}; +} // anonymous namespace + +namespace llvm { +namespace yaml { + +/// \brief Specialized MappingTraits to describe how an EditList is +/// (de)serialized. +template <> struct MappingTraits { + static void mapping(IO &Io, NormalizedEditList &Doc) { + Io.mapRequired("Key", Doc.Key); + Io.mapRequired("FilePath", Doc.FilePath); + Io.mapRequired("Error", Doc.Error); + Io.mapRequired("InsertedHeaders", Doc.InsertedHeaders); + Io.mapRequired("RemovedHeaders", Doc.RemovedHeaders); + Io.mapRequired("Replacements", Doc.Replaces); + } +}; + +/// \brief Specialized MappingTraits to describe how an EditList is +/// (de)serialized. +template <> struct MappingTraits { + static void mapping(IO &Io, + clang::tooling::EditList &Doc) { + MappingNormalization + Keys(Io, Doc); + Io.mapRequired("Key", Keys->Key); + Io.mapRequired("FilePath", Keys->FilePath); + Io.mapRequired("Error", Keys->Error); + Io.mapRequired("InsertedHeaders", Keys->InsertedHeaders); + Io.mapRequired("RemovedHeaders", Keys->RemovedHeaders); + Io.mapRequired("Replacements", Keys->Replaces); + } +}; + +} // end namespace yaml +} // end namespace llvm + +namespace clang { +namespace tooling { + +EditList::EditList(const SourceManager &SM, SourceLocation KeyPosition) { + const FullSourceLoc FullKeyPosition(KeyPosition, SM); + std::pair FileIDAndOffset = + FullKeyPosition.getSpellingLoc().getDecomposedLoc(); + const FileEntry *FE = SM.getFileEntryForID(FileIDAndOffset.first); + assert(FE && "Cannot create EditList with invalid location."); + FilePath = FE->getName(); + Key = FilePath + ":" + std::to_string(FileIDAndOffset.second); +} + +EditList::EditList(std::string Key, std::string FilePath, std::string Error, + std::vector InsertedHeaders, + std::vector RemovedHeaders, + clang::tooling::Replacements Replaces) + : Key(std::move(Key)), FilePath(std::move(FilePath)), + Error(std::move(Error)), InsertedHeaders(std::move(InsertedHeaders)), + RemovedHeaders(std::move(RemovedHeaders)), Replaces(std::move(Replaces)) { +} + +std::string EditList::toYAMLString() { + std::string YamlContent; + llvm::raw_string_ostream YamlContentStream(YamlContent); + + llvm::yaml::Output YAML(YamlContentStream); + YAML << *this; + YamlContentStream.flush(); + return YamlContent; +} + +EditList EditList::convertFromYAML(llvm::StringRef YAMLContent) { + NormalizedEditList NE; + llvm::yaml::Input YAML(YAMLContent); + YAML >> NE; + EditList E(NE.Key, NE.FilePath, NE.Error, NE.InsertedHeaders, + NE.RemovedHeaders, tooling::Replacements()); + for (const auto &R : NE.Replaces) { + llvm::Error Err = E.addReplacement(R); + if (Err) + llvm_unreachable( + "Failed to add replacement when Converting YAML to EditList."); + llvm::consumeError(std::move(Err)); + } + return E; +} + +llvm::Error EditList::addReplacement(const Replacement &R) { + return Replaces.add(R); +} + +void EditList::mergeReplacements(const Replacements &Rs) { + Replaces = Replaces.merge(Rs); +} + +llvm::Error EditList::insertBefore(const SourceManager &SM, SourceLocation Loc, + llvm::StringRef Text) { + if (Text.empty()) return llvm::Error::success(); + Replacement R(SM, Loc, 0, Text); + llvm::Error Err = Replaces.add(R); + if (Err) { + return llvm::handleErrors( + std::move(Err), [&](const ReplacementError &RE) -> llvm::Error { + if (RE.get() != replacement_error::insert_conflict) + return llvm::make_error(RE); + unsigned NewOffset = + Replaces.getShiftedCodePosition(R.getOffset()) - + RE.getExistingReplacement()->getReplacementText().size(); + Replacement NewR(R.getFilePath(), NewOffset, 0, Text); + mergeReplacements(Replacements(NewR)); + return llvm::Error::success(); + }); + } + return llvm::Error::success(); +} + +llvm::Error EditList::insertAfter(const SourceManager &SM, SourceLocation Loc, + llvm::StringRef Text) { + if (Text.empty()) return llvm::Error::success(); + Replacement R(SM, Loc, 0, Text); + llvm::Error Err = Replaces.add(R); + if (Err) { + return llvm::handleErrors( + std::move(Err), [&](const ReplacementError &RE) -> llvm::Error { + if (RE.get() != replacement_error::insert_conflict) + return llvm::make_error(RE); + unsigned NewOffset = Replaces.getShiftedCodePosition(R.getOffset()); + Replacement NewR(R.getFilePath(), NewOffset, 0, Text); + mergeReplacements(Replacements(NewR)); + return llvm::Error::success(); + }); + } + return llvm::Error::success(); +} + +void EditList::addHeader(llvm::StringRef Header) { + InsertedHeaders.push_back(Header); +} + +void EditList::removeHeader(llvm::StringRef Header) { + RemovedHeaders.push_back(Header); +} + +} // end namespace tooling +} // end namespace clang Index: unittests/Tooling/CMakeLists.txt =================================================================== --- unittests/Tooling/CMakeLists.txt +++ unittests/Tooling/CMakeLists.txt @@ -38,4 +38,5 @@ clangRewrite clangTooling clangToolingCore + clangToolingRefactor ) Index: unittests/Tooling/RefactoringTest.cpp =================================================================== --- unittests/Tooling/RefactoringTest.cpp +++ unittests/Tooling/RefactoringTest.cpp @@ -26,6 +26,7 @@ #include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/Rewrite/Core/Rewriter.h" #include "clang/Tooling/Refactoring.h" +#include "clang/Tooling/Refactoring/EditList.h" #include "clang/Tooling/Tooling.h" #include "llvm/ADT/SmallString.h" #include "gtest/gtest.h" @@ -102,10 +103,10 @@ // Checks that an llvm::Error instance contains a ReplacementError with expected // error code, expected new replacement, and expected existing replacement. -static bool checkReplacementError( - llvm::Error&& Error, replacement_error ExpectedErr, - llvm::Optional ExpectedExisting, - llvm::Optional ExpectedNew) { +static bool checkReplacementError(llvm::Error &&Error, + replacement_error ExpectedErr, + llvm::Optional ExpectedExisting, + llvm::Optional ExpectedNew) { if (!Error) { llvm::errs() << "Error is a success."; return false; @@ -1089,5 +1090,183 @@ EXPECT_TRUE(FileToReplaces.empty()); } +class EditListTest : public ::testing::Test { + protected: + void setUp() { + DefaultFileID = Context.createInMemoryFile("input.cpp", DefaultCode); + DefaultLoc = Context.Sources.getLocForStartOfFile(DefaultFileID) + .getLocWithOffset(20); + assert(DefaultLoc.isValid() && "Default location must be valid."); + } + + RewriterTestContext Context; + std::string DefaultCode = std::string(100, 'a'); + unsigned DefaultOffset = 20; + SourceLocation DefaultLoc; + FileID DefaultFileID; +}; + +TEST_F(EditListTest, EditListToYAML) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertBefore(Context.Sources, DefaultLoc, "aa"); + ASSERT_TRUE(!Err); + Err = + Edit.insertBefore(Context.Sources, DefaultLoc.getLocWithOffset(10), "bb"); + ASSERT_TRUE(!Err); + Edit.addHeader("a.h"); + Edit.removeHeader("b.h"); + std::string YAMLString = Edit.toYAMLString(); + + // NOTE: If this test starts to fail for no obvious reason, check whitespace. + ASSERT_STREQ("---\n" + "Key: 'input.cpp:20'\n" + "FilePath: input.cpp\n" + "Error: ''\n" + "InsertedHeaders: [ a.h ]\n" + "RemovedHeaders: [ b.h ]\n" + "Replacements: \n" // Extra whitespace here! + " - FilePath: input.cpp\n" + " Offset: 20\n" + " Length: 0\n" + " ReplacementText: aa\n" + " - FilePath: input.cpp\n" + " Offset: 30\n" + " Length: 0\n" + " ReplacementText: bb\n" + "...\n", + YAMLString.c_str()); +} + +TEST_F(EditListTest, YAMLToEditList) { + setUp(); + std::string YamlContent = "---\n" + "Key: 'input.cpp:20'\n" + "FilePath: input.cpp\n" + "Error: 'ok'\n" + "InsertedHeaders: [ a.h ]\n" + "RemovedHeaders: [ b.h ]\n" + "Replacements: \n" // Extra whitespace here! + " - FilePath: input.cpp\n" + " Offset: 20\n" + " Length: 0\n" + " ReplacementText: aa\n" + " - FilePath: input.cpp\n" + " Offset: 30\n" + " Length: 0\n" + " ReplacementText: bb\n" + "...\n"; + EditList ExpectedEdit(Context.Sources, DefaultLoc); + llvm::Error Err = + ExpectedEdit.insertBefore(Context.Sources, DefaultLoc, "aa"); + ASSERT_TRUE(!Err); + Err = ExpectedEdit.insertBefore(Context.Sources, + DefaultLoc.getLocWithOffset(10), "bb"); + ASSERT_TRUE(!Err); + + ExpectedEdit.addHeader("a.h"); + ExpectedEdit.removeHeader("b.h"); + ExpectedEdit.setError("ok"); + + EditList ActualEdit = EditList::convertFromYAML(YamlContent); + EXPECT_EQ(ExpectedEdit.getKey(), ActualEdit.getKey()); + EXPECT_EQ(ExpectedEdit.getFilePath(), ActualEdit.getFilePath()); + EXPECT_EQ(ExpectedEdit.getError(), ActualEdit.getError()); + EXPECT_EQ(ExpectedEdit.getInsertedHeaders(), ActualEdit.getInsertedHeaders()); + EXPECT_EQ(ExpectedEdit.getRemovedHeaders(), ActualEdit.getRemovedHeaders()); + EXPECT_EQ(ExpectedEdit.getReplacements().size(), + ActualEdit.getReplacements().size()); + EXPECT_EQ(2u, ActualEdit.getReplacements().size()); + EXPECT_EQ(*ExpectedEdit.getReplacements().begin(), + *ActualEdit.getReplacements().begin()); + EXPECT_EQ(*(++ExpectedEdit.getReplacements().begin()), + *(++ActualEdit.getReplacements().begin())); +} + +TEST_F(EditListTest, CheckKeyAndKeyFile) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + EXPECT_EQ("input.cpp:20", Edit.getKey()); + EXPECT_EQ("input.cpp", Edit.getFilePath()); +} + +TEST_F(EditListTest, InsertBefore) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertBefore(Context.Sources, DefaultLoc, "aa"); + ASSERT_TRUE(!Err); + EXPECT_EQ(Edit.getReplacements().size(), 1u); + EXPECT_EQ(*Edit.getReplacements().begin(), + Replacement(Context.Sources, DefaultLoc, 0, "aa")); + Err = Edit.insertBefore(Context.Sources, DefaultLoc, "b"); + ASSERT_TRUE(!Err); + EXPECT_EQ(Edit.getReplacements().size(), 1u); + EXPECT_EQ(*Edit.getReplacements().begin(), + Replacement(Context.Sources, DefaultLoc, 0, "baa")); +} + +TEST_F(EditListTest, InsertAfter) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertAfter(Context.Sources, DefaultLoc, "aa"); + ASSERT_TRUE(!Err); + EXPECT_EQ(Edit.getReplacements().size(), 1u); + EXPECT_EQ(*Edit.getReplacements().begin(), + Replacement(Context.Sources, DefaultLoc, 0, "aa")); + Err = Edit.insertAfter(Context.Sources, DefaultLoc, "b"); + ASSERT_TRUE(!Err); + EXPECT_EQ(Edit.getReplacements().size(), 1u); + EXPECT_EQ(*Edit.getReplacements().begin(), + Replacement(Context.Sources, DefaultLoc, 0, "aab")); +} + +TEST_F(EditListTest, InsertBeforeWithInvalidLocation) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertBefore(Context.Sources, DefaultLoc, "a"); + ASSERT_TRUE(!Err); + + // Invalid location. + Err = Edit.insertBefore(Context.Sources, SourceLocation(), "a"); + ASSERT_TRUE((bool)Err); + EXPECT_TRUE(checkReplacementError( + std::move(Err), replacement_error::wrong_file_path, + Replacement(Context.Sources, DefaultLoc, 0, "a"), + Replacement(Context.Sources, SourceLocation(), 0, "a"))); + +} + +TEST_F(EditListTest, InsertBeforeToWrongFile) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertBefore(Context.Sources, DefaultLoc, "a"); + ASSERT_TRUE(!Err); + + // Inserting at a different file. + FileID NewID = Context.createInMemoryFile("extra.cpp", DefaultCode); + SourceLocation NewLoc = Context.Sources.getLocForStartOfFile(NewID); + Err = Edit.insertBefore(Context.Sources, NewLoc, "b"); + ASSERT_TRUE((bool)Err); + EXPECT_TRUE( + checkReplacementError(std::move(Err), replacement_error::wrong_file_path, + Replacement(Context.Sources, DefaultLoc, 0, "a"), + Replacement(Context.Sources, NewLoc, 0, "b"))); +} + +TEST_F(EditListTest, InsertAfterWithInvalidLocation) { + setUp(); + EditList Edit(Context.Sources, DefaultLoc); + llvm::Error Err = Edit.insertAfter(Context.Sources, DefaultLoc, "a"); + ASSERT_TRUE(!Err); + + // Invalid location. + Err = Edit.insertAfter(Context.Sources, SourceLocation(), "b"); + ASSERT_TRUE((bool)Err); + EXPECT_TRUE(checkReplacementError( + std::move(Err), replacement_error::wrong_file_path, + Replacement(Context.Sources, DefaultLoc, 0, "a"), + Replacement(Context.Sources, SourceLocation(), 0, "b"))); +} + } // end namespace tooling } // end namespace clang