| 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) {}; |
| 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 |
| 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 |
| 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) |
| 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; | ||
| } |
| 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 |
| 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 |