Introducing clang::tooling::AtomicChange for refactoring tools.

Summary:
An AtomicChange is used to create and group a set of source edits, e.g.
replacements or header insertions. Edits in an AtomicChange should be related,
e.g. replacements for the same type reference and the corresponding header
insertion/deletion.

An AtomicChange 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.

Next step: add a tool that applies AtomicChange.

Reviewers: klimek, djasper

Reviewed By: klimek

Subscribers: alexshap, cfe-commits, djasper, mgorny

Differential Revision: https://reviews.llvm.org/D27054

llvm-svn: 296616
This commit is contained in:
Eric Liu 2017-03-01 13:14:01 +00:00
parent 4a45efa431
commit 9e745b7292
6 changed files with 498 additions and 5 deletions

View File

@ -0,0 +1,129 @@
//===--- AtomicChange.h - AtomicChange class --------------------*- 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 AtomicChange which is used to create a set of source
// changes, e.g. replacements and header insertions.
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_CLANG_TOOLING_REFACTOR_ATOMICCHANGE_H
#define LLVM_CLANG_TOOLING_REFACTOR_ATOMICCHANGE_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 atomic change is used to create and group a set of source edits,
/// e.g. replacements or header insertions. Edits in an AtomicChange should be
/// related, e.g. replacements for the same type reference and the corresponding
/// header insertion/deletion.
///
/// An AtomicChange is uniquely identified by a key and will either be fully
/// applied or not applied at all.
///
/// Calling setError on an AtomicChange stores the error message and marks it as
/// bad, i.e. none of its source edits will be applied.
class AtomicChange {
public:
/// \brief Creates an atomic change around \p KeyPosition with the key being a
/// concatenation of the file name and the offset of \p KeyPosition.
/// \p KeyPosition should be the location of the key syntactical element that
/// is being changed, e.g. the call to a refactored method.
AtomicChange(const SourceManager &SM, SourceLocation KeyPosition);
/// \brief Creates an atomic change for \p FilePath with a customized key.
AtomicChange(llvm::StringRef FilePath, llvm::StringRef Key)
: Key(Key), FilePath(FilePath) {}
/// \brief Returns the atomic change as a YAML string.
std::string toYAMLString();
/// \brief Converts a YAML-encoded automic change to AtomicChange.
static AtomicChange convertFromYAML(llvm::StringRef YAMLContent);
/// \brief Returns the key of this change, which is a concatenation of the
/// file name and offset of the key position.
const std::string &getKey() const { return Key; }
/// \brief Returns the path of the file containing this atomic change.
const std::string &getFilePath() const { return FilePath; }
/// \brief If this change 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 changes.
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.
const std::string &getError() const { return Error; }
/// \brief Adds a replacement that replaces range [Loc, Loc+Length) with
/// \p Text.
/// \returns An llvm::Error carrying ReplacementError on error.
llvm::Error replace(const SourceManager &SM, SourceLocation Loc,
unsigned Length, llvm::StringRef Text);
/// \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/after the existing insertion depending on
/// \p InsertAfter. Users should use `replace` with `Length=0` instead if they
/// do not want conflict resolving by default. If the conflicting replacement
/// is not an insertion, an error is returned.
///
/// \returns An llvm::Error carrying ReplacementError on error.
llvm::Error insert(const SourceManager &SM, SourceLocation Loc,
llvm::StringRef Text, bool InsertAfter = true);
/// \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:
AtomicChange() {}
AtomicChange(std::string Key, std::string FilePath, std::string Error,
std::vector<std::string> InsertedHeaders,
std::vector<std::string> RemovedHeaders,
clang::tooling::Replacements Replaces);
// This uniquely identifies an AtomicChange.
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_ATOMICCHANGE_H

View File

@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS
) )
add_subdirectory(Core) add_subdirectory(Core)
add_subdirectory(Refactoring)
add_clang_library(clangTooling add_clang_library(clangTooling
ArgumentsAdjusters.cpp ArgumentsAdjusters.cpp

View File

@ -0,0 +1,167 @@
//===--- AtomicChange.cpp - AtomicChange 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/AtomicChange.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::AtomicChange)
namespace {
/// \brief Helper to (de)serialize an AtomicChange since we don't have direct
/// access to its data members.
/// Data members of a normalized AtomicChange can be directly mapped from/to
/// YAML string.
struct NormalizedAtomicChange {
NormalizedAtomicChange() = default;
NormalizedAtomicChange(const llvm::yaml::IO &) {}
// This converts AtomicChange's internal implementation of the replacements
// set to a vector of replacements.
NormalizedAtomicChange(const llvm::yaml::IO &,
const clang::tooling::AtomicChange &E)
: Key(E.getKey()), FilePath(E.getFilePath()), Error(E.getError()),
InsertedHeaders(E.getInsertedHeaders()),
RemovedHeaders(E.getRemovedHeaders()),
Replaces(E.getReplacements().begin(), E.getReplacements().end()) {}
// This is not expected to be called but needed for template instantiation.
clang::tooling::AtomicChange denormalize(const llvm::yaml::IO &) {
llvm_unreachable("Do not convert YAML to AtomicChange directly with '>>'. "
"Use AtomicChange::convertFromYAML instead.");
}
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;
};
} // anonymous namespace
namespace llvm {
namespace yaml {
/// \brief Specialized MappingTraits to describe how an AtomicChange is
/// (de)serialized.
template <> struct MappingTraits<NormalizedAtomicChange> {
static void mapping(IO &Io, NormalizedAtomicChange &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 AtomicChange is
/// (de)serialized.
template <> struct MappingTraits<clang::tooling::AtomicChange> {
static void mapping(IO &Io, clang::tooling::AtomicChange &Doc) {
MappingNormalization<NormalizedAtomicChange, clang::tooling::AtomicChange>
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 {
AtomicChange::AtomicChange(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 AtomicChange with invalid location.");
FilePath = FE->getName();
Key = FilePath + ":" + std::to_string(FileIDAndOffset.second);
}
AtomicChange::AtomicChange(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 AtomicChange::toYAMLString() {
std::string YamlContent;
llvm::raw_string_ostream YamlContentStream(YamlContent);
llvm::yaml::Output YAML(YamlContentStream);
YAML << *this;
YamlContentStream.flush();
return YamlContent;
}
AtomicChange AtomicChange::convertFromYAML(llvm::StringRef YAMLContent) {
NormalizedAtomicChange NE;
llvm::yaml::Input YAML(YAMLContent);
YAML >> NE;
AtomicChange E(NE.Key, NE.FilePath, NE.Error, NE.InsertedHeaders,
NE.RemovedHeaders, tooling::Replacements());
for (const auto &R : NE.Replaces) {
llvm::Error Err = E.Replaces.add(R);
if (Err)
llvm_unreachable(
"Failed to add replacement when Converting YAML to AtomicChange.");
llvm::consumeError(std::move(Err));
}
return E;
}
llvm::Error AtomicChange::insert(const SourceManager &SM, SourceLocation Loc,
llvm::StringRef Text, bool InsertAfter) {
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());
if (!InsertAfter)
NewOffset -=
RE.getExistingReplacement()->getReplacementText().size();
Replacement NewR(R.getFilePath(), NewOffset, 0, Text);
Replaces = Replaces.merge(Replacements(NewR));
return llvm::Error::success();
});
}
return llvm::Error::success();
}
void AtomicChange::addHeader(llvm::StringRef Header) {
InsertedHeaders.push_back(Header);
}
void AtomicChange::removeHeader(llvm::StringRef Header) {
RemovedHeaders.push_back(Header);
}
} // end namespace tooling
} // end namespace clang

View File

@ -0,0 +1,12 @@
set(LLVM_LINK_COMPONENTS
Option
Support
)
add_clang_library(clangToolingRefactor
AtomicChange.cpp
LINK_LIBS
clangBasic
clangToolingCore
)

View File

@ -13,7 +13,7 @@ endif()
add_clang_unittest(ToolingTests add_clang_unittest(ToolingTests
CommentHandlerTest.cpp CommentHandlerTest.cpp
CompilationDatabaseTest.cpp CompilationDatabaseTest.cpp
FixItTest.cpp FixItTest.cpp
LookupTest.cpp LookupTest.cpp
QualTypeNamesTest.cpp QualTypeNamesTest.cpp
RecursiveASTVisitorTest.cpp RecursiveASTVisitorTest.cpp
@ -38,4 +38,5 @@ target_link_libraries(ToolingTests
clangRewrite clangRewrite
clangTooling clangTooling
clangToolingCore clangToolingCore
clangToolingRefactor
) )

View File

@ -26,6 +26,7 @@
#include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Rewrite/Core/Rewriter.h" #include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Refactoring.h" #include "clang/Tooling/Refactoring.h"
#include "clang/Tooling/Refactoring/AtomicChange.h"
#include "clang/Tooling/Tooling.h" #include "clang/Tooling/Tooling.h"
#include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallString.h"
#include "gtest/gtest.h" #include "gtest/gtest.h"
@ -102,10 +103,10 @@ TEST_F(ReplacementTest, ReturnsInvalidPath) {
// Checks that an llvm::Error instance contains a ReplacementError with expected // Checks that an llvm::Error instance contains a ReplacementError with expected
// error code, expected new replacement, and expected existing replacement. // error code, expected new replacement, and expected existing replacement.
static bool checkReplacementError( static bool checkReplacementError(llvm::Error &&Error,
llvm::Error&& Error, replacement_error ExpectedErr, replacement_error ExpectedErr,
llvm::Optional<Replacement> ExpectedExisting, llvm::Optional<Replacement> ExpectedExisting,
llvm::Optional<Replacement> ExpectedNew) { llvm::Optional<Replacement> ExpectedNew) {
if (!Error) { if (!Error) {
llvm::errs() << "Error is a success."; llvm::errs() << "Error is a success.";
return false; return false;
@ -1089,5 +1090,187 @@ TEST(DeduplicateByFileTest, NonExistingFilePath) {
EXPECT_TRUE(FileToReplaces.empty()); EXPECT_TRUE(FileToReplaces.empty());
} }
class AtomicChangeTest : 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(AtomicChangeTest, AtomicChangeToYAML) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err =
Change.insert(Context.Sources, DefaultLoc, "aa", /*InsertAfter=*/false);
ASSERT_TRUE(!Err);
Err = Change.insert(Context.Sources, DefaultLoc.getLocWithOffset(10), "bb",
/*InsertAfter=*/false);
ASSERT_TRUE(!Err);
Change.addHeader("a.h");
Change.removeHeader("b.h");
std::string YAMLString = Change.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(AtomicChangeTest, YAMLToAtomicChange) {
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";
AtomicChange ExpectedChange(Context.Sources, DefaultLoc);
llvm::Error Err = ExpectedChange.insert(Context.Sources, DefaultLoc, "aa",
/*InsertAfter=*/false);
ASSERT_TRUE(!Err);
Err = ExpectedChange.insert(Context.Sources, DefaultLoc.getLocWithOffset(10),
"bb", /*InsertAfter=*/false);
ASSERT_TRUE(!Err);
ExpectedChange.addHeader("a.h");
ExpectedChange.removeHeader("b.h");
ExpectedChange.setError("ok");
AtomicChange ActualChange = AtomicChange::convertFromYAML(YamlContent);
EXPECT_EQ(ExpectedChange.getKey(), ActualChange.getKey());
EXPECT_EQ(ExpectedChange.getFilePath(), ActualChange.getFilePath());
EXPECT_EQ(ExpectedChange.getError(), ActualChange.getError());
EXPECT_EQ(ExpectedChange.getInsertedHeaders(), ActualChange.getInsertedHeaders());
EXPECT_EQ(ExpectedChange.getRemovedHeaders(), ActualChange.getRemovedHeaders());
EXPECT_EQ(ExpectedChange.getReplacements().size(),
ActualChange.getReplacements().size());
EXPECT_EQ(2u, ActualChange.getReplacements().size());
EXPECT_EQ(*ExpectedChange.getReplacements().begin(),
*ActualChange.getReplacements().begin());
EXPECT_EQ(*(++ExpectedChange.getReplacements().begin()),
*(++ActualChange.getReplacements().begin()));
}
TEST_F(AtomicChangeTest, CheckKeyAndKeyFile) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
EXPECT_EQ("input.cpp:20", Change.getKey());
EXPECT_EQ("input.cpp", Change.getFilePath());
}
TEST_F(AtomicChangeTest, InsertBefore) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err = Change.insert(Context.Sources, DefaultLoc, "aa");
ASSERT_TRUE(!Err);
EXPECT_EQ(Change.getReplacements().size(), 1u);
EXPECT_EQ(*Change.getReplacements().begin(),
Replacement(Context.Sources, DefaultLoc, 0, "aa"));
Err = Change.insert(Context.Sources, DefaultLoc, "b", /*InsertAfter=*/false);
ASSERT_TRUE(!Err);
EXPECT_EQ(Change.getReplacements().size(), 1u);
EXPECT_EQ(*Change.getReplacements().begin(),
Replacement(Context.Sources, DefaultLoc, 0, "baa"));
}
TEST_F(AtomicChangeTest, InsertAfter) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err = Change.insert(Context.Sources, DefaultLoc, "aa");
ASSERT_TRUE(!Err);
EXPECT_EQ(Change.getReplacements().size(), 1u);
EXPECT_EQ(*Change.getReplacements().begin(),
Replacement(Context.Sources, DefaultLoc, 0, "aa"));
Err = Change.insert(Context.Sources, DefaultLoc, "b");
ASSERT_TRUE(!Err);
EXPECT_EQ(Change.getReplacements().size(), 1u);
EXPECT_EQ(*Change.getReplacements().begin(),
Replacement(Context.Sources, DefaultLoc, 0, "aab"));
}
TEST_F(AtomicChangeTest, InsertBeforeWithInvalidLocation) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err =
Change.insert(Context.Sources, DefaultLoc, "a", /*InsertAfter=*/false);
ASSERT_TRUE(!Err);
// Invalid location.
Err = Change.insert(Context.Sources, SourceLocation(), "a",
/*InsertAfter=*/false);
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(AtomicChangeTest, InsertBeforeToWrongFile) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err =
Change.insert(Context.Sources, DefaultLoc, "a", /*InsertAfter=*/false);
ASSERT_TRUE(!Err);
// Inserting at a different file.
FileID NewID = Context.createInMemoryFile("extra.cpp", DefaultCode);
SourceLocation NewLoc = Context.Sources.getLocForStartOfFile(NewID);
Err = Change.insert(Context.Sources, NewLoc, "b", /*InsertAfter=*/false);
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(AtomicChangeTest, InsertAfterWithInvalidLocation) {
setUp();
AtomicChange Change(Context.Sources, DefaultLoc);
llvm::Error Err = Change.insert(Context.Sources, DefaultLoc, "a");
ASSERT_TRUE(!Err);
// Invalid location.
Err = Change.insert(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 tooling
} // end namespace clang } // end namespace clang