61 changes: 61 additions & 0 deletions clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
#include "clang/Lex/Preprocessor.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Refactoring.h"
#include "clang/Tooling/Refactoring/RefactoringAction.h"
#include "clang/Tooling/Refactoring/RefactoringActionRules.h"
#include "clang/Tooling/Refactoring/Rename/USRFinder.h"
#include "clang/Tooling/Refactoring/Rename/USRFindingAction.h"
#include "clang/Tooling/Refactoring/Rename/USRLocFinder.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/STLExtras.h"
Expand All @@ -33,6 +37,63 @@ using namespace llvm;
namespace clang {
namespace tooling {

namespace {

class LocalRename : public RefactoringAction {
public:
StringRef getCommand() const override { return "local-rename"; }

StringRef getDescription() const override {
return "Finds and renames symbols in code with no indexer support";
}

/// Returns a set of refactoring actions rules that are defined by this
/// action.
RefactoringActionRules createActionRules() const override {
using namespace refactoring_action_rules;
RefactoringActionRules Rules;
Rules.push_back(createRefactoringRule(
renameOccurrences, requiredSelection(SymbolSelectionRequirement())));
return Rules;
}

private:
static Expected<AtomicChanges>
renameOccurrences(const RefactoringRuleContext &Context,
const NamedDecl *ND) {
std::vector<std::string> USRs =
getUSRsForDeclaration(ND, Context.getASTContext());
std::string PrevName = ND->getNameAsString();
auto Occurrences = getOccurrencesOfUSRs(
USRs, PrevName, Context.getASTContext().getTranslationUnitDecl());

// FIXME: This is a temporary workaround that's needed until the refactoring
// options are implemented.
StringRef NewName = "Bar";
return createRenameReplacements(
Occurrences, Context.getASTContext().getSourceManager(), NewName);
}

class SymbolSelectionRequirement : public selection::Requirement {
public:
Expected<Optional<const NamedDecl *>>
evaluateSelection(const RefactoringRuleContext &Context,
selection::SourceSelectionRange Selection) const {
const NamedDecl *ND = getNamedDeclAt(Context.getASTContext(),
Selection.getRange().getBegin());
if (!ND)
return None;
return getCanonicalSymbolDeclaration(ND);
}
};
};

} // end anonymous namespace

std::unique_ptr<RefactoringAction> createLocalRenameAction() {
return llvm::make_unique<LocalRename>();
}

Expected<std::vector<AtomicChange>>
createRenameReplacements(const SymbolOccurrences &Occurrences,
const SourceManager &SM,
Expand Down
6 changes: 6 additions & 0 deletions clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ class AdditionalUSRFinder : public RecursiveASTVisitor<AdditionalUSRFinder> {
};
} // namespace

std::vector<std::string> getUSRsForDeclaration(const NamedDecl *ND,
ASTContext &Context) {
AdditionalUSRFinder Finder(ND, Context);
return Finder.Find();
}

class NamedDeclFindingConsumer : public ASTConsumer {
public:
NamedDeclFindingConsumer(ArrayRef<unsigned> SymbolOffsets,
Expand Down
1 change: 1 addition & 0 deletions clang/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ list(APPEND CLANG_TEST_DEPS
clang-offload-bundler
clang-import-test
clang-rename
clang-refactor
clang-diff
)

Expand Down
9 changes: 9 additions & 0 deletions clang/test/Refactor/LocalRename/Field.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// RUN: clang-refactor local-rename -selection=test:%s -no-dbs %s | FileCheck %s

class Baz {
int /*range=*/Foo; // CHECK: int /*range=*/Bar;
public:
Baz();
};

Baz::Baz() : /*range=*/Foo(0) {} // CHECK: Baz::Baz() : /*range=*/Bar(0) {};
6 changes: 6 additions & 0 deletions clang/test/Refactor/tool-common-options.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// RUN: not clang-refactor 2>&1 | FileCheck --check-prefix=MISSING_ACTION %s
// MISSING_ACTION: error: no refactoring action given
// MISSING_ACTION-NEXT: note: the following actions are supported:

// RUN: not clang-refactor local-rename -no-dbs 2>&1 | FileCheck --check-prefix=MISSING_SOURCES %s
// MISSING_SOURCES: error: must provide paths to the source files when '-no-dbs' is used
41 changes: 41 additions & 0 deletions clang/test/Refactor/tool-test-support.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// RUN: clang-refactor local-rename -selection=test:%s -no-dbs -v %s 2>&1 | FileCheck %s

/*range=*/int test;

/*range named=*/int test2;

/*range= +1*/int test3;

/* range = +100 */int test4;

/*range named =+0*/int test5;

// CHECK: Test selection group '':
// CHECK-NEXT: 100-100
// CHECK-NEXT: 153-153
// CHECK-NEXT: 192-192
// CHECK-NEXT: Test selection group 'named':
// CHECK-NEXT: 127-127
// CHECK-NEXT: 213-213

// The following invocations are in the default group:

// CHECK: invoking action 'local-rename':
// CHECK-NEXT: -selection={{.*}}tool-test-support.c:3:11

// CHECK: invoking action 'local-rename':
// CHECK-NEXT: -selection={{.*}}tool-test-support.c:7:15

// CHECK: invoking action 'local-rename':
// CHECK-NEXT: -selection={{.*}}tool-test-support.c:9:29


// The following invocations are in the 'named' group, and they follow
// the default invocation even if some of their ranges occur prior to the
// ranges from the default group because the groups are tested one-by-one:

// CHECK: invoking action 'local-rename':
// CHECK-NEXT: -selection={{.*}}tool-test-support.c:5:17

// CHECK: invoking action 'local-rename':
// CHECK-NEXT: -selection={{.*}}tool-test-support.c:11:20
15 changes: 0 additions & 15 deletions clang/test/clang-rename/Field.cpp

This file was deleted.

1 change: 1 addition & 0 deletions clang/tools/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_clang_subdirectory(clang-offload-bundler)
add_clang_subdirectory(c-index-test)

add_clang_subdirectory(clang-rename)
add_clang_subdirectory(clang-refactor)

if(CLANG_ENABLE_ARCMT)
add_clang_subdirectory(arcmt-test)
Expand Down
20 changes: 20 additions & 0 deletions clang/tools/clang-refactor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
set(LLVM_LINK_COMPONENTS
Option
Support
)

add_clang_executable(clang-refactor
ClangRefactor.cpp
TestSupport.cpp
)

target_link_libraries(clang-refactor
clangBasic
clangFrontend
clangRewrite
clangTooling
clangToolingCore
clangToolingRefactor
)

install(TARGETS clang-refactor RUNTIME DESTINATION bin)
391 changes: 391 additions & 0 deletions clang/tools/clang-refactor/ClangRefactor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,391 @@
//===--- ClangRefactor.cpp - Clang-based refactoring tool -----------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// \brief This file implements a clang-refactor tool that performs various
/// source transformations.
///
//===----------------------------------------------------------------------===//

#include "TestSupport.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Refactoring.h"
#include "clang/Tooling/Refactoring/RefactoringAction.h"
#include "clang/Tooling/Refactoring/Rename/RenamingAction.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/raw_ostream.h"
#include <string>

using namespace clang;
using namespace tooling;
using namespace refactor;
namespace cl = llvm::cl;

namespace opts {

static cl::OptionCategory CommonRefactorOptions("Common refactoring options");

static cl::opt<bool>
NoDatabases("no-dbs",
cl::desc("Ignore external databases including Clang's "
"compilation database and indexer stores"),
cl::cat(CommonRefactorOptions), cl::sub(*cl::AllSubCommands));

static cl::opt<bool> Verbose("v", cl::desc("Use verbose output"),
cl::cat(CommonRefactorOptions),
cl::sub(*cl::AllSubCommands));
} // end namespace opts

namespace {

/// Stores the parsed `-selection` argument.
class SourceSelectionArgument {
public:
virtual ~SourceSelectionArgument() {}

/// Parse the `-selection` argument.
///
/// \returns A valid argument when the parse succedeed, null otherwise.
static std::unique_ptr<SourceSelectionArgument> fromString(StringRef Value);

/// Prints any additional state associated with the selection argument to
/// the given output stream.
virtual void print(raw_ostream &OS) = 0;

/// Returns a replacement refactoring result consumer (if any) that should
/// consume the results of a refactoring operation.
///
/// The replacement refactoring result consumer is used by \c
/// TestSourceSelectionArgument to inject a test-specific result handling
/// logic into the refactoring operation. The test-specific consumer
/// ensures that the individual results in a particular test group are
/// identical.
virtual std::unique_ptr<RefactoringResultConsumer> createCustomConsumer() {
return nullptr;
}

/// Runs the give refactoring function for each specified selection.
///
/// \returns true if an error occurred, false otherwise.
virtual bool
forAllRanges(const SourceManager &SM,
llvm::function_ref<void(SourceRange R)> Callback) = 0;
};

/// Stores the parsed -selection=test:<filename> option.
class TestSourceSelectionArgument final : public SourceSelectionArgument {
public:
TestSourceSelectionArgument(TestSelectionRangesInFile TestSelections)
: TestSelections(std::move(TestSelections)) {}

void print(raw_ostream &OS) override { TestSelections.dump(OS); }

std::unique_ptr<RefactoringResultConsumer> createCustomConsumer() override {
return TestSelections.createConsumer();
}

/// Testing support: invokes the selection action for each selection range in
/// the test file.
bool forAllRanges(const SourceManager &SM,
llvm::function_ref<void(SourceRange R)> Callback) override {
return TestSelections.foreachRange(SM, Callback);
}

private:
TestSelectionRangesInFile TestSelections;
};

std::unique_ptr<SourceSelectionArgument>
SourceSelectionArgument::fromString(StringRef Value) {
if (Value.startswith("test:")) {
StringRef Filename = Value.drop_front(strlen("test:"));
Optional<TestSelectionRangesInFile> ParsedTestSelection =
findTestSelectionRanges(Filename);
if (!ParsedTestSelection)
return nullptr; // A parsing error was already reported.
return llvm::make_unique<TestSourceSelectionArgument>(
std::move(*ParsedTestSelection));
}
// FIXME: Support true selection ranges.
llvm::errs() << "error: '-selection' option must be specified using "
"<file>:<line>:<column> or "
"<file>:<line>:<column>-<line>:<column> format";
return nullptr;
}

/// A subcommand that corresponds to individual refactoring action.
class RefactoringActionSubcommand : public cl::SubCommand {
public:
RefactoringActionSubcommand(std::unique_ptr<RefactoringAction> Action,
RefactoringActionRules ActionRules,
cl::OptionCategory &Category)
: SubCommand(Action->getCommand(), Action->getDescription()),
Action(std::move(Action)), ActionRules(std::move(ActionRules)) {
Sources = llvm::make_unique<cl::list<std::string>>(
cl::Positional, cl::ZeroOrMore, cl::desc("<source0> [... <sourceN>]"),
cl::cat(Category), cl::sub(*this));

// Check if the selection option is supported.
bool HasSelection = false;
for (const auto &Rule : this->ActionRules) {
if ((HasSelection = Rule->hasSelectionRequirement()))
break;
}
if (HasSelection) {
Selection = llvm::make_unique<cl::opt<std::string>>(
"selection",
cl::desc("The selected source range in which the refactoring should "
"be initiated (<file>:<line>:<column>-<line>:<column> or "
"<file>:<line>:<column>)"),
cl::cat(Category), cl::sub(*this));
}
}

~RefactoringActionSubcommand() { unregisterSubCommand(); }

const RefactoringActionRules &getActionRules() const { return ActionRules; }

/// Parses the command-line arguments that are specific to this rule.
///
/// \returns true on error, false otherwise.
bool parseArguments() {
if (Selection) {
ParsedSelection = SourceSelectionArgument::fromString(*Selection);
if (!ParsedSelection)
return true;
}
return false;
}

SourceSelectionArgument *getSelection() const {
assert(Selection && "selection not supported!");
return ParsedSelection.get();
}

ArrayRef<std::string> getSources() const { return *Sources; }

private:
std::unique_ptr<RefactoringAction> Action;
RefactoringActionRules ActionRules;
std::unique_ptr<cl::list<std::string>> Sources;
std::unique_ptr<cl::opt<std::string>> Selection;
std::unique_ptr<SourceSelectionArgument> ParsedSelection;
};

class ClangRefactorConsumer : public RefactoringResultConsumer {
public:
void handleError(llvm::Error Err) {
llvm::errs() << llvm::toString(std::move(Err)) << "\n";
}

// FIXME: Consume atomic changes and apply them to files.
};

class ClangRefactorTool {
public:
std::vector<std::unique_ptr<RefactoringActionSubcommand>> SubCommands;

ClangRefactorTool() {
std::vector<std::unique_ptr<RefactoringAction>> Actions =
createRefactoringActions();

// Actions must have unique command names so that we can map them to one
// subcommand.
llvm::StringSet<> CommandNames;
for (const auto &Action : Actions) {
if (!CommandNames.insert(Action->getCommand()).second) {
llvm::errs() << "duplicate refactoring action command '"
<< Action->getCommand() << "'!";
exit(1);
}
}

// Create subcommands and command-line options.
for (auto &Action : Actions) {
SubCommands.push_back(llvm::make_unique<RefactoringActionSubcommand>(
std::move(Action), Action->createActiveActionRules(),
opts::CommonRefactorOptions));
}
}

using TUCallbackType = llvm::function_ref<void(ASTContext &)>;

/// Parses the translation units that were given to the subcommand using
/// the 'sources' option and invokes the callback for each parsed
/// translation unit.
bool foreachTranslationUnit(RefactoringActionSubcommand &Subcommand,
TUCallbackType Callback) {
std::unique_ptr<CompilationDatabase> Compilations;
if (opts::NoDatabases) {
// FIXME (Alex L): Support compilation options.
Compilations =
llvm::make_unique<clang::tooling::FixedCompilationDatabase>(
".", std::vector<std::string>());
} else {
// FIXME (Alex L): Support compilation database.
llvm::errs() << "compilation databases are not supported yet!\n";
return true;
}

class ToolASTConsumer : public ASTConsumer {
public:
TUCallbackType Callback;
ToolASTConsumer(TUCallbackType Callback) : Callback(Callback) {}

void HandleTranslationUnit(ASTContext &Context) override {
Callback(Context);
}
};
class ActionWrapper {
public:
TUCallbackType Callback;
ActionWrapper(TUCallbackType Callback) : Callback(Callback) {}

std::unique_ptr<ASTConsumer> newASTConsumer() {
return llvm::make_unique<ToolASTConsumer>(std::move(Callback));
}
};

ClangTool Tool(*Compilations, Subcommand.getSources());
ActionWrapper ToolAction(std::move(Callback));
std::unique_ptr<tooling::FrontendActionFactory> Factory =
tooling::newFrontendActionFactory(&ToolAction);
return Tool.run(Factory.get());
}

/// Logs an individual refactoring action invocation to STDOUT.
void logInvocation(RefactoringActionSubcommand &Subcommand,
const RefactoringRuleContext &Context) {
if (!opts::Verbose)
return;
llvm::outs() << "invoking action '" << Subcommand.getName() << "':\n";
if (Context.getSelectionRange().isValid()) {
SourceRange R = Context.getSelectionRange();
llvm::outs() << " -selection=";
R.getBegin().print(llvm::outs(), Context.getSources());
llvm::outs() << " -> ";
R.getEnd().print(llvm::outs(), Context.getSources());
llvm::outs() << "\n";
}
}

bool invokeAction(RefactoringActionSubcommand &Subcommand) {
// Find a set of matching rules.
SmallVector<RefactoringActionRule *, 4> MatchingRules;
llvm::StringSet<> MissingOptions;

bool HasSelection = false;
for (const auto &Rule : Subcommand.getActionRules()) {
if (Rule->hasSelectionRequirement()) {
HasSelection = true;
if (Subcommand.getSelection())
MatchingRules.push_back(Rule.get());
else
MissingOptions.insert("selection");
}
// FIXME (Alex L): Support custom options.
}
if (MatchingRules.empty()) {
llvm::errs() << "error: '" << Subcommand.getName()
<< "' can't be invoked with the given arguments:\n";
for (const auto &Opt : MissingOptions)
llvm::errs() << " missing '-" << Opt.getKey() << "' option\n";
return true;
}

bool HasFailed = false;
ClangRefactorConsumer Consumer;
if (foreachTranslationUnit(Subcommand, [&](ASTContext &AST) {
RefactoringRuleContext Context(AST.getSourceManager());
Context.setASTContext(AST);

auto InvokeRule = [&](RefactoringResultConsumer &Consumer) {
logInvocation(Subcommand, Context);
for (RefactoringActionRule *Rule : MatchingRules) {
if (!Rule->hasSelectionRequirement())
continue;
Rule->invoke(Consumer, Context);
return;
}
// FIXME (Alex L): If more than one initiation succeeded, then the
// rules are ambiguous.
llvm_unreachable(
"The action must have at least one selection rule");
};

if (HasSelection) {
assert(Subcommand.getSelection() && "Missing selection argument?");
if (opts::Verbose)
Subcommand.getSelection()->print(llvm::outs());
auto CustomConsumer =
Subcommand.getSelection()->createCustomConsumer();
if (Subcommand.getSelection()->forAllRanges(
Context.getSources(), [&](SourceRange R) {
Context.setSelectionRange(R);
InvokeRule(CustomConsumer ? *CustomConsumer : Consumer);
}))
HasFailed = true;
return;
}
// FIXME (Alex L): Implement non-selection based invocation path.
}))
return true;
return HasFailed;
}
};

} // end anonymous namespace

int main(int argc, const char **argv) {
ClangRefactorTool Tool;

// FIXME: Use LibTooling's CommonOptions parser when subcommands are supported
// by it.
cl::HideUnrelatedOptions(opts::CommonRefactorOptions);
cl::ParseCommandLineOptions(
argc, argv, "Clang-based refactoring tool for C, C++ and Objective-C");
cl::PrintOptionValues();

// Figure out which action is specified by the user. The user must specify
// the action using a command-line subcommand, e.g. the invocation
// `clang-refactor local-rename` corresponds to the `LocalRename` refactoring
// action. All subcommands must have a unique names. This allows us to figure
// out which refactoring action should be invoked by looking at the first
// subcommand that's enabled by LLVM's command-line parser.
auto It = llvm::find_if(
Tool.SubCommands,
[](const std::unique_ptr<RefactoringActionSubcommand> &SubCommand) {
return !!(*SubCommand);
});
if (It == Tool.SubCommands.end()) {
llvm::errs() << "error: no refactoring action given\n";
llvm::errs() << "note: the following actions are supported:\n";
for (const auto &Subcommand : Tool.SubCommands)
llvm::errs().indent(2) << Subcommand->getName() << "\n";
return 1;
}
RefactoringActionSubcommand &ActionCommand = **It;

ArrayRef<std::string> Sources = ActionCommand.getSources();
// When -no-dbs is used, at least one file (TU) must be given to any
// subcommand.
if (opts::NoDatabases && Sources.empty()) {
llvm::errs() << "error: must provide paths to the source files when "
"'-no-dbs' is used\n";
return 1;
}
if (ActionCommand.parseArguments())
return 1;
if (Tool.invokeAction(ActionCommand))
return 1;

return 0;
}
343 changes: 343 additions & 0 deletions clang/tools/clang-refactor/TestSupport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
//===--- TestSupport.cpp - Clang-based refactoring tool -------------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// \brief This file implements routines that provide refactoring testing
/// utilities.
///
//===----------------------------------------------------------------------===//

#include "TestSupport.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/ErrorOr.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Regex.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace clang {
namespace refactor {

void TestSelectionRangesInFile::dump(raw_ostream &OS) const {
for (const auto &Group : GroupedRanges) {
OS << "Test selection group '" << Group.Name << "':\n";
for (const auto &Range : Group.Ranges) {
OS << " " << Range.Begin << "-" << Range.End << "\n";
}
}
}

bool TestSelectionRangesInFile::foreachRange(
const SourceManager &SM,
llvm::function_ref<void(SourceRange)> Callback) const {
const FileEntry *FE = SM.getFileManager().getFile(Filename);
FileID FID = FE ? SM.translateFile(FE) : FileID();
if (!FE || FID.isInvalid()) {
llvm::errs() << "error: -selection=test:" << Filename
<< " : given file is not in the target TU";
return true;
}
SourceLocation FileLoc = SM.getLocForStartOfFile(FID);
for (const auto &Group : GroupedRanges) {
for (const TestSelectionRange &Range : Group.Ranges) {
// Translate the offset pair to a true source range.
SourceLocation Start =
SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.Begin));
SourceLocation End =
SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.End));
assert(Start.isValid() && End.isValid() && "unexpected invalid range");
Callback(SourceRange(Start, End));
}
}
return false;
}

namespace {

void dumpChanges(const tooling::AtomicChanges &Changes, raw_ostream &OS) {
for (const auto &Change : Changes)
OS << const_cast<tooling::AtomicChange &>(Change).toYAMLString() << "\n";
}

bool areChangesSame(const tooling::AtomicChanges &LHS,
const tooling::AtomicChanges &RHS) {
if (LHS.size() != RHS.size())
return false;
for (const auto &I : llvm::zip(LHS, RHS)) {
if (!(std::get<0>(I) == std::get<1>(I)))
return false;
}
return true;
}

bool printRewrittenSources(const tooling::AtomicChanges &Changes,
raw_ostream &OS) {
std::set<std::string> Files;
for (const auto &Change : Changes)
Files.insert(Change.getFilePath());
tooling::ApplyChangesSpec Spec;
Spec.Cleanup = false;
for (const auto &File : Files) {
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> BufferErr =
llvm::MemoryBuffer::getFile(File);
if (!BufferErr) {
llvm::errs() << "failed to open" << File << "\n";
return true;
}
auto Result = tooling::applyAtomicChanges(File, (*BufferErr)->getBuffer(),
Changes, Spec);
if (!Result) {
llvm::errs() << toString(Result.takeError());
return true;
}
OS << *Result;
}
return false;
}

class TestRefactoringResultConsumer final
: public tooling::RefactoringResultConsumer {
public:
TestRefactoringResultConsumer(const TestSelectionRangesInFile &TestRanges)
: TestRanges(TestRanges) {
Results.push_back({});
}

~TestRefactoringResultConsumer() {
// Ensure all results are checked.
for (auto &Group : Results) {
for (auto &Result : Group) {
if (!Result) {
(void)llvm::toString(Result.takeError());
}
}
}
}

void handleError(llvm::Error Err) override { handleResult(std::move(Err)); }

void handle(tooling::AtomicChanges Changes) override {
handleResult(std::move(Changes));
}

void handle(tooling::SymbolOccurrences Occurrences) override {
tooling::RefactoringResultConsumer::handle(std::move(Occurrences));
}

private:
bool handleAllResults();

void handleResult(Expected<tooling::AtomicChanges> Result) {
Results.back().push_back(std::move(Result));
size_t GroupIndex = Results.size() - 1;
if (Results.back().size() >=
TestRanges.GroupedRanges[GroupIndex].Ranges.size()) {
++GroupIndex;
if (GroupIndex >= TestRanges.GroupedRanges.size()) {
if (handleAllResults())
exit(1); // error has occurred.
return;
}
Results.push_back({});
}
}

const TestSelectionRangesInFile &TestRanges;
std::vector<std::vector<Expected<tooling::AtomicChanges>>> Results;
};

std::pair<unsigned, unsigned> getLineColumn(StringRef Filename,
unsigned Offset) {
ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
MemoryBuffer::getFile(Filename);
if (!ErrOrFile)
return {0, 0};
StringRef Source = ErrOrFile.get()->getBuffer();
Source = Source.take_front(Offset);
size_t LastLine = Source.find_last_of("\r\n");
return {Source.count('\n') + 1,
(LastLine == StringRef::npos ? Offset : Offset - LastLine) + 1};
}

} // end anonymous namespace

bool TestRefactoringResultConsumer::handleAllResults() {
bool Failed = false;
for (auto &Group : llvm::enumerate(Results)) {
// All ranges in the group must produce the same result.
Optional<tooling::AtomicChanges> CanonicalResult;
Optional<std::string> CanonicalErrorMessage;
for (auto &I : llvm::enumerate(Group.value())) {
Expected<tooling::AtomicChanges> &Result = I.value();
std::string ErrorMessage;
bool HasResult = !!Result;
if (!HasResult) {
// FIXME: Handle diagnostic error as well.
handleAllErrors(Result.takeError(), [&](StringError &Err) {
ErrorMessage = Err.getMessage();
});
}
if (!CanonicalResult && !CanonicalErrorMessage) {
if (HasResult)
CanonicalResult = std::move(*Result);
else
CanonicalErrorMessage = std::move(ErrorMessage);
continue;
}

// Verify that this result corresponds to the canonical result.
if (CanonicalErrorMessage) {
// The error messages must match.
if (!HasResult && ErrorMessage == *CanonicalErrorMessage)
continue;
} else {
assert(CanonicalResult && "missing canonical result");
// The results must match.
if (HasResult && areChangesSame(*Result, *CanonicalResult))
continue;
}
Failed = true;
// Report the mismatch.
std::pair<unsigned, unsigned> LineColumn = getLineColumn(
TestRanges.Filename,
TestRanges.GroupedRanges[Group.index()].Ranges[I.index()].Begin);
llvm::errs()
<< "error: unexpected refactoring result for range starting at "
<< LineColumn.first << ':' << LineColumn.second << " in group '"
<< TestRanges.GroupedRanges[Group.index()].Name << "':\n ";
if (HasResult)
llvm::errs() << "valid result";
else
llvm::errs() << "error '" << ErrorMessage << "'";
llvm::errs() << " does not match initial ";
if (CanonicalErrorMessage)
llvm::errs() << "error '" << *CanonicalErrorMessage << "'\n";
else
llvm::errs() << "valid result\n";
if (HasResult && !CanonicalErrorMessage) {
llvm::errs() << " Expected to Produce:\n";
dumpChanges(*CanonicalResult, llvm::errs());
llvm::errs() << " Produced:\n";
dumpChanges(*Result, llvm::errs());
}
}

// Dump the results:
const auto &TestGroup = TestRanges.GroupedRanges[Group.index()];
if (!CanonicalResult) {
llvm::errs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
<< "' results:\n";
llvm::errs() << *CanonicalErrorMessage << "\n";
} else {
llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
<< "' results:\n";
if (printRewrittenSources(*CanonicalResult, llvm::outs()))
return true;
}
}
return Failed;
}

std::unique_ptr<tooling::RefactoringResultConsumer>
TestSelectionRangesInFile::createConsumer() const {
return llvm::make_unique<TestRefactoringResultConsumer>(*this);
}

/// Adds the \p ColumnOffset to file offset \p Offset, without going past a
/// newline.
static unsigned addColumnOffset(StringRef Source, unsigned Offset,
unsigned ColumnOffset) {
if (!ColumnOffset)
return Offset;
StringRef Substr = Source.drop_front(Offset).take_front(ColumnOffset);
size_t NewlinePos = Substr.find_first_of("\r\n");
return Offset +
(NewlinePos == StringRef::npos ? ColumnOffset : (unsigned)NewlinePos);
}

Optional<TestSelectionRangesInFile>
findTestSelectionRanges(StringRef Filename) {
ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
MemoryBuffer::getFile(Filename);
if (!ErrOrFile) {
llvm::errs() << "error: -selection=test:" << Filename
<< " : could not open the given file";
return None;
}
StringRef Source = ErrOrFile.get()->getBuffer();

// FIXME (Alex L): 3rd capture groups for +line:column.
// See the doc comment for this function for the explanation of this
// syntax.
static Regex RangeRegex("range[[:blank:]]*([[:alpha:]_]*)?[[:blank:]]*=[[:"
"blank:]]*(\\+[[:digit:]]+)?");

std::map<std::string, SmallVector<TestSelectionRange, 8>> GroupedRanges;

LangOptions LangOpts;
LangOpts.CPlusPlus = 1;
LangOpts.CPlusPlus11 = 1;
Lexer Lex(SourceLocation::getFromRawEncoding(0), LangOpts, Source.begin(),
Source.begin(), Source.end());
Lex.SetCommentRetentionState(true);
Token Tok;
for (Lex.LexFromRawLexer(Tok); Tok.isNot(tok::eof);
Lex.LexFromRawLexer(Tok)) {
if (Tok.isNot(tok::comment))
continue;
StringRef Comment =
Source.substr(Tok.getLocation().getRawEncoding(), Tok.getLength());
SmallVector<StringRef, 4> Matches;
// Allow CHECK: comments to contain range= commands.
if (!RangeRegex.match(Comment, &Matches) || Comment.contains("CHECK")) {
// Try to detect mistyped 'range:' comments to ensure tests don't miss
// anything.
if (Comment.contains_lower("range") && Comment.contains("=") &&
!Comment.contains_lower("run") && !Comment.contains("CHECK")) {
llvm::errs() << "error: suspicious comment '" << Comment
<< "' that "
"resembles the range command found\n";
llvm::errs() << "note: please reword if this isn't a range command\n";
return None;
}
continue;
}
unsigned Offset = Tok.getEndLoc().getRawEncoding();
unsigned ColumnOffset = 0;
if (!Matches[2].empty()) {
// Don't forget to drop the '+'!
if (Matches[2].drop_front().getAsInteger(10, ColumnOffset))
assert(false && "regex should have produced a number");
}
// FIXME (Alex L): Support true ranges.
Offset = addColumnOffset(Source, Offset, ColumnOffset);
TestSelectionRange Range = {Offset, Offset};
auto It = GroupedRanges.insert(std::make_pair(
Matches[1].str(), SmallVector<TestSelectionRange, 8>{Range}));
if (!It.second)
It.first->second.push_back(Range);
}
if (GroupedRanges.empty()) {
llvm::errs() << "error: -selection=test:" << Filename
<< ": no 'range' commands";
return None;
}

TestSelectionRangesInFile TestRanges = {Filename.str(), {}};
for (auto &Group : GroupedRanges)
TestRanges.GroupedRanges.push_back({Group.first, std::move(Group.second)});
return std::move(TestRanges);
}

} // end namespace refactor
} // end namespace clang
107 changes: 107 additions & 0 deletions clang/tools/clang-refactor/TestSupport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//===--- TestSupport.h - Clang-based refactoring tool -----------*- C++ -*-===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// \brief Declares datatypes and routines that are used by test-specific code
/// in clang-refactor.
///
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_CLANG_REFACTOR_TEST_SUPPORT_H
#define LLVM_CLANG_TOOLS_CLANG_REFACTOR_TEST_SUPPORT_H

#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Tooling/Refactoring/RefactoringResultConsumer.h"
#include "llvm/ADT/Optional.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/Error.h"
#include <map>
#include <string>

namespace clang {

class SourceManager;

namespace refactor {

/// A source selection range that's specified in a test file using an inline
/// command in the comment. These commands can take the following forms:
///
/// - /*range=*/ will create an empty selection range in the default group
/// right after the comment.
/// - /*range a=*/ will create an empty selection range in the 'a' group right
/// after the comment.
/// - /*range = +1*/ will create an empty selection range at a location that's
/// right after the comment with one offset to the column.
/// - /*range= -> +2:3*/ will create a selection range that starts at the
/// location right after the comment, and ends at column 3 of the 2nd line
/// after the line of the starting location.
///
/// Clang-refactor will expected all ranges in one test group to produce
/// identical results.
struct TestSelectionRange {
unsigned Begin, End;
};

/// A set of test selection ranges specified in one file.
struct TestSelectionRangesInFile {
std::string Filename;
struct RangeGroup {
std::string Name;
SmallVector<TestSelectionRange, 8> Ranges;
};
std::vector<RangeGroup> GroupedRanges;

TestSelectionRangesInFile(TestSelectionRangesInFile &&) = default;
TestSelectionRangesInFile &operator=(TestSelectionRangesInFile &&) = default;

bool foreachRange(const SourceManager &SM,
llvm::function_ref<void(SourceRange)> Callback) const;

std::unique_ptr<tooling::RefactoringResultConsumer> createConsumer() const;

void dump(llvm::raw_ostream &OS) const;
};

/// Extracts the grouped selection ranges from the file that's specified in
/// the -selection=test:<filename> option.
///
/// The grouped ranges are specified in comments using the following syntax:
/// "range" [ group-name ] "=" [ "+" starting-column-offset ] [ "->"
/// "+" ending-line-offset ":"
/// ending-column-position ]
///
/// The selection range is then computed from this command by taking the ending
/// location of the comment, and adding 'starting-column-offset' to the column
/// for that location. That location in turns becomes the whole selection range,
/// unless 'ending-line-offset' and 'ending-column-position' are specified. If
/// they are specified, then the ending location of the selection range is
/// the starting location's line + 'ending-line-offset' and the
/// 'ending-column-position' column.
///
/// All selection ranges in one group are expected to produce the same
/// refactoring result.
///
/// When testing, zero is returned from clang-refactor even when a group
/// produces an initiation error, which is different from normal invocation
/// that returns a non-zero value. This is done on purpose, to ensure that group
/// consistency checks can return non-zero, but still print the output of
/// the group. So even if a test matches the output of group, it will still fail
/// because clang-refactor should return zero on exit when the group results are
/// consistent.
///
/// \returns None on failure (errors are emitted to stderr), or a set of
/// grouped source ranges in the given file otherwise.
Optional<TestSelectionRangesInFile> findTestSelectionRanges(StringRef Filename);

} // end namespace refactor
} // end namespace clang

#endif // LLVM_CLANG_TOOLS_CLANG_REFACTOR_TEST_SUPPORT_H
20 changes: 13 additions & 7 deletions clang/unittests/Tooling/RefactoringActionRulesTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ createReplacements(const std::unique_ptr<RefactoringActionRule> &Rule,

TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) {
auto ReplaceAWithB =
[](std::pair<selection::SourceSelectionRange, int> Selection)
[](const RefactoringRuleContext &,
std::pair<selection::SourceSelectionRange, int> Selection)
-> Expected<AtomicChanges> {
const SourceManager &SM = Selection.first.getSources();
SourceLocation Loc = Selection.first.getRange().getBegin().getLocWithOffset(
Expand All @@ -71,7 +72,8 @@ TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) {
class SelectionRequirement : public selection::Requirement {
public:
std::pair<selection::SourceSelectionRange, int>
evaluateSelection(selection::SourceSelectionRange Selection) const {
evaluateSelection(const RefactoringRuleContext &,
selection::SourceSelectionRange Selection) const {
return std::make_pair(Selection, 20);
}
};
Expand Down Expand Up @@ -127,8 +129,10 @@ TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) {
}

TEST_F(RefactoringActionRulesTest, ReturnError) {
Expected<AtomicChanges> (*Func)(selection::SourceSelectionRange) =
[](selection::SourceSelectionRange) -> Expected<AtomicChanges> {
Expected<AtomicChanges> (*Func)(const RefactoringRuleContext &,
selection::SourceSelectionRange) =
[](const RefactoringRuleContext &,
selection::SourceSelectionRange) -> Expected<AtomicChanges> {
return llvm::make_error<llvm::StringError>(
"Error", llvm::make_error_code(llvm::errc::invalid_argument));
};
Expand All @@ -155,13 +159,14 @@ TEST_F(RefactoringActionRulesTest, ReturnInitiationDiagnostic) {
class SelectionRequirement : public selection::Requirement {
public:
Expected<Optional<int>>
evaluateSelection(selection::SourceSelectionRange Selection) const {
evaluateSelection(const RefactoringRuleContext &,
selection::SourceSelectionRange Selection) const {
return llvm::make_error<llvm::StringError>(
"bad selection", llvm::make_error_code(llvm::errc::invalid_argument));
}
};
auto Rule = createRefactoringRule(
[](int) -> Expected<AtomicChanges> {
[](const RefactoringRuleContext &, int) -> Expected<AtomicChanges> {
llvm::report_fatal_error("Should not run!");
},
requiredSelection(SelectionRequirement()));
Expand Down Expand Up @@ -201,7 +206,8 @@ Optional<SymbolOccurrences> findOccurrences(RefactoringActionRule &Rule,

TEST_F(RefactoringActionRulesTest, ReturnSymbolOccurrences) {
auto Rule = createRefactoringRule(
[](selection::SourceSelectionRange Selection)
[](const RefactoringRuleContext &,
selection::SourceSelectionRange Selection)
-> Expected<SymbolOccurrences> {
SymbolOccurrences Occurrences;
Occurrences.push_back(SymbolOccurrence(
Expand Down