ioeric created this revision.
ioeric added a reviewer: klimek.
ioeric added subscribers: djasper, cfe-commits.
Herald added a subscriber: mgorny.

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.


https://reviews.llvm.org/D27054

Files:
  include/clang/Tooling/Refactoring/EditList.h
  lib/Tooling/CMakeLists.txt
  lib/Tooling/Refactoring/CMakeLists.txt
  lib/Tooling/Refactoring/EditList.cpp
  unittests/Tooling/CMakeLists.txt
  unittests/Tooling/RefactoringTest.cpp

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<Replacement> ExpectedExisting,
-    llvm::Optional<Replacement> ExpectedNew) {
+static bool checkReplacementError(llvm::Error &&Error,
+                                  replacement_error ExpectedErr,
+                                  llvm::Optional<Replacement> ExpectedExisting,
+                                  llvm::Optional<Replacement> 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");
+  EXPECT_TRUE(!Err);
+  Err =
+      Edit.insertBefore(Context.Sources, DefaultLoc.getLocWithOffset(10), "bb");
+  EXPECT_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");
+  EXPECT_TRUE(!Err);
+  Err = ExpectedEdit.insertBefore(Context.Sources,
+                                  DefaultLoc.getLocWithOffset(10), "bb");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_TRUE(!Err);
+
+  // Invalid location.
+  Err = Edit.insertBefore(Context.Sources, SourceLocation(), "a");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_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");
+  EXPECT_TRUE(!Err);
+
+  // Invalid location.
+  Err = Edit.insertAfter(Context.Sources, SourceLocation(), "b");
+  EXPECT_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
Index: unittests/Tooling/CMakeLists.txt
===================================================================
--- unittests/Tooling/CMakeLists.txt
+++ unittests/Tooling/CMakeLists.txt
@@ -38,4 +38,5 @@
   clangRewrite
   clangTooling
   clangToolingCore
+  clangToolingRefactor
   )
Index: lib/Tooling/Refactoring/EditList.cpp
===================================================================
--- /dev/null
+++ lib/Tooling/Refactoring/EditList.cpp
@@ -0,0 +1,167 @@
+//===--- 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 <string>
+
+LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(std::string)
+LLVM_YAML_IS_SEQUENCE_VECTOR(clang::tooling::EditList)
+
+namespace llvm {
+namespace yaml {
+/// \brief Specialized MappingTraits to describe how an EditList is
+/// (de)serialized.
+template <> struct MappingTraits<clang::tooling::EditList> {
+  /// \brief Helper to (de)serialize an EditList since we don't have direct
+  /// access to its data members.
+  struct NormalizedEditList {
+    NormalizedEditList(const IO &) {}
+
+    NormalizedEditList(const 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));
+    }
+
+    clang::tooling::EditList denormalize(const IO &) {
+      clang::tooling::Replacements Rs;
+      for (const auto &R : Replaces) {
+        llvm::Error Err = Rs.add(R);
+        if (Err)
+          llvm_unreachable(
+              "Failed to add replacement when Converting YAML to EditList.");
+        llvm::consumeError(std::move(Err));
+      }
+      return clang::tooling::EditList(Key, FilePath, Error, InsertedHeaders,
+                                      RemovedHeaders, Rs);
+    }
+
+    std::string Key;
+    std::string FilePath;
+    std::string Error;
+    std::vector<std::string> InsertedHeaders;
+    std::vector<std::string> RemovedHeaders;
+    std::vector<clang::tooling::Replacement> Replaces;
+  };
+  static void mapping(IO &Io,
+                      clang::tooling::EditList &Doc) {
+    MappingNormalization<NormalizedEditList, clang::tooling::EditList>
+    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<FileID, unsigned> 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<std::string> InsertedHeaders,
+                   std::vector<std::string> 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) {
+  EditList Edit;
+  llvm::yaml::Input YAML(YAMLContent);
+  YAML >> Edit;
+  return Edit;
+}
+
+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<ReplacementError>(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<ReplacementError>(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: 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/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: include/clang/Tooling/Refactoring/EditList.h
===================================================================
--- /dev/null
+++ include/clang/Tooling/Refactoring/EditList.h
@@ -0,0 +1,128 @@
+//===--- 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);
+
+  EditList(std::string Key, std::string FilePath, std::string Error,
+           std::vector<std::string> InsertedHeaders,
+           std::vector<std::string> RemovedHeaders,
+           clang::tooling::Replacements Replaces);
+
+  /// \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 position information 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<std::string> getInsertedHeaders() const {
+    return InsertedHeaders;
+  }
+  llvm::ArrayRef<std::string> getRemovedHeaders() const {
+    return RemovedHeaders;
+  }
+
+private:
+  EditList() {}
+
+  std::string Key;
+  std::string FilePath;
+  std::string Error;
+  std::vector<std::string> InsertedHeaders;
+  std::vector<std::string> RemovedHeaders;
+  tooling::Replacements Replaces;
+};
+
+} // end namespace tooling
+} // end namespace clang
+
+#endif // LLVM_CLANG_TOOLING_REFACTOR_EDITLIST_H
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to