[clang-refactor] Apply source replacements

This commit actually brings clang-refactor to a usable state as it can now
apply the refactoring changes to source files.
The -selection option is now also fully supported.

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

llvm-svn: 315738
This commit is contained in:
Alex Lorenz 2017-10-13 19:42:05 +00:00
parent ac40140e24
commit 57e060b309
4 changed files with 155 additions and 6 deletions

View File

@ -51,6 +51,51 @@ public:
}
};
/// A source range that has been parsed on the command line.
struct ParsedSourceRange {
std::string FileName;
/// The starting location of the range. The first element is the line and
/// the second element is the column.
std::pair<unsigned, unsigned> Begin;
/// The ending location of the range. The first element is the line and the
/// second element is the column.
std::pair<unsigned, unsigned> End;
/// Returns a parsed source range from a string or None if the string is
/// invalid.
///
/// These source string has the following format:
///
/// file:start_line:start_column[-end_line:end_column]
///
/// If the end line and column are omitted, the starting line and columns
/// are used as the end values.
static Optional<ParsedSourceRange> fromString(StringRef Str) {
std::pair<StringRef, StringRef> RangeSplit;
// Avoid splitting '-' when there's no end line & column as '-' might be
// part of the filename.
if (Str.count(':') > 2)
RangeSplit = Str.rsplit('-');
else
RangeSplit = {Str, ""};
auto Begin = ParsedSourceLocation::FromString(RangeSplit.first);
if (Begin.FileName.empty())
return None;
unsigned EndLine, EndColumn;
if (RangeSplit.second.empty()) {
EndLine = Begin.Line;
EndColumn = Begin.Column;
} else {
std::pair<StringRef, StringRef> Split = RangeSplit.second.rsplit(':');
if (Split.first.getAsInteger(10, EndLine) ||
Split.second.getAsInteger(10, EndColumn))
return None;
}
return ParsedSourceRange{std::move(Begin.FileName),
{Begin.Line, Begin.Column},
{EndLine, EndColumn}};
}
};
}
namespace llvm {

View File

@ -0,0 +1,11 @@
// RUN: rm -f %t.cp.cpp
// RUN: cp %s %t.cp.cpp
// RUN: clang-refactor local-rename -selection=%t.cp.cpp:9:7 -new-name=test %t.cp.cpp --
// RUN: grep -v CHECK %t.cp.cpp | FileCheck %t.cp.cpp
// RUN: cp %s %t.cp.cpp
// RUN: clang-refactor local-rename -selection=%t.cp.cpp:9:7-9:15 -new-name=test %t.cp.cpp --
// RUN: grep -v CHECK %t.cp.cpp | FileCheck %t.cp.cpp
class RenameMe {
// CHECK: class test {
};

View File

@ -0,0 +1,15 @@
// RUN: rm -f %t.cp.c
// RUN: cp %s %t.cp.c
// RUN: clang-refactor local-rename -selection=%t.cp.c:6:5 -new-name=test -v %t.cp.c -- | FileCheck --check-prefix=CHECK1 %s
// RUN: clang-refactor local-rename -selection=%t.cp.c:6:5-6:9 -new-name=test -v %t.cp.c -- | FileCheck --check-prefix=CHECK2 %s
int test;
// CHECK1: invoking action 'local-rename':
// CHECK1-NEXT: -selection={{.*}}.cp.c:6:5 -> {{.*}}.cp.c:6:5
// CHECK2: invoking action 'local-rename':
// CHECK2-NEXT: -selection={{.*}}.cp.c:6:5 -> {{.*}}.cp.c:6:9
// RUN: not clang-refactor local-rename -selection=%s:6:5 -new-name=test -v %t.cp.c -- 2>&1 | FileCheck --check-prefix=CHECK-FILE-ERR %s
// CHECK-FILE-ERR: given file is not in the target TU

View File

@ -14,6 +14,7 @@
//===----------------------------------------------------------------------===//
#include "TestSupport.h"
#include "clang/Frontend/CommandLineSourceLoc.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Refactoring.h"
@ -54,7 +55,7 @@ public:
/// Prints any additional state associated with the selection argument to
/// the given output stream.
virtual void print(raw_ostream &OS) = 0;
virtual void print(raw_ostream &OS) {}
/// Returns a replacement refactoring result consumer (if any) that should
/// consume the results of a refactoring operation.
@ -99,6 +100,41 @@ private:
TestSelectionRangesInFile TestSelections;
};
/// Stores the parsed -selection=filename:line:column[-line:column] option.
class SourceRangeSelectionArgument final : public SourceSelectionArgument {
public:
SourceRangeSelectionArgument(ParsedSourceRange Range)
: Range(std::move(Range)) {}
bool forAllRanges(const SourceManager &SM,
llvm::function_ref<void(SourceRange R)> Callback) override {
const FileEntry *FE = SM.getFileManager().getFile(Range.FileName);
FileID FID = FE ? SM.translateFile(FE) : FileID();
if (!FE || FID.isInvalid()) {
llvm::errs() << "error: -selection=" << Range.FileName
<< ":... : given file is not in the target TU\n";
return true;
}
SourceLocation Start = SM.getMacroArgExpandedLocation(
SM.translateLineCol(FID, Range.Begin.first, Range.Begin.second));
SourceLocation End = SM.getMacroArgExpandedLocation(
SM.translateLineCol(FID, Range.End.first, Range.End.second));
if (Start.isInvalid() || End.isInvalid()) {
llvm::errs() << "error: -selection=" << Range.FileName << ':'
<< Range.Begin.first << ':' << Range.Begin.second << '-'
<< Range.End.first << ':' << Range.End.second
<< " : invalid source location\n";
return true;
}
Callback(SourceRange(Start, End));
return false;
}
private:
ParsedSourceRange Range;
};
std::unique_ptr<SourceSelectionArgument>
SourceSelectionArgument::fromString(StringRef Value) {
if (Value.startswith("test:")) {
@ -110,10 +146,12 @@ SourceSelectionArgument::fromString(StringRef Value) {
return llvm::make_unique<TestSourceSelectionArgument>(
std::move(*ParsedTestSelection));
}
// FIXME: Support true selection ranges.
Optional<ParsedSourceRange> Range = ParsedSourceRange::fromString(Value);
if (Range)
return llvm::make_unique<SourceRangeSelectionArgument>(std::move(*Range));
llvm::errs() << "error: '-selection' option must be specified using "
"<file>:<line>:<column> or "
"<file>:<line>:<column>-<line>:<column> format";
"<file>:<line>:<column>-<line>:<column> format\n";
return nullptr;
}
@ -268,11 +306,18 @@ private:
class ClangRefactorConsumer : public RefactoringResultConsumer {
public:
void handleError(llvm::Error Err) {
void handleError(llvm::Error Err) override {
llvm::errs() << llvm::toString(std::move(Err)) << "\n";
}
// FIXME: Consume atomic changes and apply them to files.
void handle(AtomicChanges Changes) override {
SourceChanges.insert(SourceChanges.begin(), Changes.begin(), Changes.end());
}
const AtomicChanges &getSourceChanges() const { return SourceChanges; }
private:
AtomicChanges SourceChanges;
};
class ClangRefactorTool {
@ -352,6 +397,39 @@ public:
}
}
bool applySourceChanges(const AtomicChanges &Replacements) {
std::set<std::string> Files;
for (const auto &Change : Replacements)
Files.insert(Change.getFilePath());
// FIXME: Add automatic formatting support as well.
tooling::ApplyChangesSpec Spec;
// FIXME: We should probably cleanup the result by default as well.
Spec.Cleanup = false;
for (const auto &File : Files) {
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> BufferErr =
llvm::MemoryBuffer::getFile(File);
if (!BufferErr) {
llvm::errs() << "error: failed to open " << File << " for rewriting\n";
return true;
}
auto Result = tooling::applyAtomicChanges(File, (*BufferErr)->getBuffer(),
Replacements, Spec);
if (!Result) {
llvm::errs() << toString(Result.takeError());
return true;
}
std::error_code EC;
llvm::raw_fd_ostream OS(File, EC, llvm::sys::fs::F_Text);
if (EC) {
llvm::errs() << EC.message() << "\n";
return true;
}
OS << *Result;
}
return false;
}
bool invokeAction(RefactoringActionSubcommand &Subcommand,
const CompilationDatabase &DB,
ArrayRef<std::string> Sources) {
@ -423,7 +501,7 @@ public:
// FIXME (Alex L): Implement non-selection based invocation path.
}))
return true;
return HasFailed;
return HasFailed || applySourceChanges(Consumer.getSourceChanges());
}
};