From 5a2269c689fbe654ff15a8783bd61b1047353b67 Mon Sep 17 00:00:00 2001 From: Alex Lorenz Date: Thu, 14 Sep 2017 10:06:52 +0000 Subject: [PATCH] [refactor] add clang-refactor tool with initial testing support and local-rename action This commit introduces the clang-refactor tool alongside the local-rename action which uses the existing renaming engine used by clang-rename. The tool doesn't actually perform the source transformations yet, it just provides testing support. This commit also moves only one test from clang-rename over to test/Refactor. I will continue to move the other tests throughout development of clang-refactor. The following options are supported by clang-refactor: -v: use verbose output -selection: The source range that corresponds to the portion of the source that's selected (currently only special command test: is supported). Please note that a follow-up commit will migrate clang-refactor to libTooling's common option parser, so clang-refactor will be able to use the common interface with compilation database and options like -p, -extra-arg, etc. The testing support provided by clang-refactor is described below: When -selection=test: is given, clang-refactor will parse the selection commands from that file. The selection commands are grouped and the specified refactoring action invoked by the tool. Each command in a group is expected to produce an identical result. The precise syntax for the selection commands is described in a comment in TestSupport.h. Differential Revision: https://reviews.llvm.org/D36574 git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@313244 91177308-0d34-0410-b5e6-96231b3b80d8 --- .../clang/Tooling/Refactoring/AtomicChange.h | 2 + .../Tooling/Refactoring/RefactoringAction.h | 64 +++ .../Refactoring/RefactoringActionRegistry.def | 7 + .../Refactoring/RefactoringActionRule.h | 4 + ...efactoringActionRuleRequirementsInternal.h | 16 +- .../Refactoring/RefactoringActionRules.h | 9 +- .../RefactoringActionRulesInternal.h | 13 +- .../Refactoring/RefactoringRuleContext.h | 15 + .../Refactoring/Rename/USRFindingAction.h | 5 + .../Refactoring/SourceSelectionConstraints.h | 15 +- include/clang/module.modulemap | 2 + lib/Tooling/Refactoring/AtomicChange.cpp | 9 + lib/Tooling/Refactoring/CMakeLists.txt | 1 + .../Refactoring/RefactoringActions.cpp | 35 ++ .../Refactoring/Rename/RenamingAction.cpp | 61 +++ .../Refactoring/Rename/USRFindingAction.cpp | 6 + test/CMakeLists.txt | 1 + test/Refactor/LocalRename/Field.cpp | 9 + test/Refactor/tool-common-options.c | 6 + test/Refactor/tool-test-support.c | 41 ++ test/clang-rename/Field.cpp | 15 - tools/CMakeLists.txt | 1 + tools/clang-refactor/CMakeLists.txt | 20 + tools/clang-refactor/ClangRefactor.cpp | 391 ++++++++++++++++++ tools/clang-refactor/TestSupport.cpp | 343 +++++++++++++++ tools/clang-refactor/TestSupport.h | 107 +++++ .../Tooling/RefactoringActionRulesTest.cpp | 20 +- 27 files changed, 1183 insertions(+), 35 deletions(-) create mode 100644 include/clang/Tooling/Refactoring/RefactoringAction.h create mode 100644 include/clang/Tooling/Refactoring/RefactoringActionRegistry.def create mode 100644 lib/Tooling/Refactoring/RefactoringActions.cpp create mode 100644 test/Refactor/LocalRename/Field.cpp create mode 100644 test/Refactor/tool-common-options.c create mode 100644 test/Refactor/tool-test-support.c delete mode 100644 test/clang-rename/Field.cpp create mode 100644 tools/clang-refactor/CMakeLists.txt create mode 100644 tools/clang-refactor/ClangRefactor.cpp create mode 100644 tools/clang-refactor/TestSupport.cpp create mode 100644 tools/clang-refactor/TestSupport.h diff --git a/include/clang/Tooling/Refactoring/AtomicChange.h b/include/clang/Tooling/Refactoring/AtomicChange.h index 1f8ff8258f..8a468a6de8 100644 --- a/include/clang/Tooling/Refactoring/AtomicChange.h +++ b/include/clang/Tooling/Refactoring/AtomicChange.h @@ -52,6 +52,8 @@ public: AtomicChange &operator=(AtomicChange &&) = default; AtomicChange &operator=(const AtomicChange &) = default; + bool operator==(const AtomicChange &Other) const; + /// \brief Returns the atomic change as a YAML string. std::string toYAMLString(); diff --git a/include/clang/Tooling/Refactoring/RefactoringAction.h b/include/clang/Tooling/Refactoring/RefactoringAction.h new file mode 100644 index 0000000000..c4080237f1 --- /dev/null +++ b/include/clang/Tooling/Refactoring/RefactoringAction.h @@ -0,0 +1,64 @@ +//===--- RefactoringAction.h - Clang refactoring library ------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLING_REFACTOR_REFACTORING_ACTION_H +#define LLVM_CLANG_TOOLING_REFACTOR_REFACTORING_ACTION_H + +#include "clang/Basic/LLVM.h" +#include "clang/Tooling/Refactoring/RefactoringActionRules.h" +#include + +namespace clang { +namespace tooling { + +/// A refactoring action is a class that defines a set of related refactoring +/// action rules. These rules get grouped under a common umbrella - a single +/// clang-refactor subcommand. +/// +/// A subclass of \c RefactoringAction is responsible for creating the set of +/// grouped refactoring action rules that represent one refactoring operation. +/// Although the rules in one action may have a number of different +/// implementations, they should strive to produce a similar result. It should +/// be easy for users to identify which refactoring action produced the result +/// regardless of which refactoring action rule was used. +/// +/// The distinction between actions and rules enables the creation of action +/// that uses very different rules, for example: +/// - local vs global: a refactoring operation like +/// "add missing switch cases" can be applied to one switch when it's +/// selected in an editor, or to all switches in a project when an enum +/// constant is added to an enum. +/// - tool vs editor: some refactoring operation can be initiated in the +/// editor when a declaration is selected, or in a tool when the name of +/// the declaration is passed using a command-line argument. +class RefactoringAction { +public: + virtual ~RefactoringAction() {} + + /// Returns the name of the subcommand that's used by clang-refactor for this + /// action. + virtual StringRef getCommand() const = 0; + + virtual StringRef getDescription() const = 0; + + RefactoringActionRules createActiveActionRules(); + +protected: + /// Returns a set of refactoring actions rules that are defined by this + /// action. + virtual RefactoringActionRules createActionRules() const = 0; +}; + +/// Returns the list of all the available refactoring actions. +std::vector> createRefactoringActions(); + +} // end namespace tooling +} // end namespace clang + +#endif // LLVM_CLANG_TOOLING_REFACTOR_REFACTORING_ACTION_H diff --git a/include/clang/Tooling/Refactoring/RefactoringActionRegistry.def b/include/clang/Tooling/Refactoring/RefactoringActionRegistry.def new file mode 100644 index 0000000000..a1191f1e50 --- /dev/null +++ b/include/clang/Tooling/Refactoring/RefactoringActionRegistry.def @@ -0,0 +1,7 @@ +#ifndef REFACTORING_ACTION +#define REFACTORING_ACTION(Name) +#endif + +REFACTORING_ACTION(LocalRename) + +#undef REFACTORING_ACTION diff --git a/include/clang/Tooling/Refactoring/RefactoringActionRule.h b/include/clang/Tooling/Refactoring/RefactoringActionRule.h index dbf6574b25..1eb6342dfb 100644 --- a/include/clang/Tooling/Refactoring/RefactoringActionRule.h +++ b/include/clang/Tooling/Refactoring/RefactoringActionRule.h @@ -30,6 +30,10 @@ public: /// consumer to propagate the result of the refactoring action. virtual void invoke(RefactoringResultConsumer &Consumer, RefactoringRuleContext &Context) = 0; + + /// Returns true when the rule has a source selection requirement that has + /// to be fullfilled before refactoring can be performed. + virtual bool hasSelectionRequirement() = 0; }; /// A set of refactoring action rules that should have unique initiation diff --git a/include/clang/Tooling/Refactoring/RefactoringActionRuleRequirementsInternal.h b/include/clang/Tooling/Refactoring/RefactoringActionRuleRequirementsInternal.h index c0837da5a9..bbad23f8fb 100644 --- a/include/clang/Tooling/Refactoring/RefactoringActionRuleRequirementsInternal.h +++ b/include/clang/Tooling/Refactoring/RefactoringActionRuleRequirementsInternal.h @@ -58,7 +58,7 @@ struct SourceSelectionRequirement Optional Value = InputT::evaluate(Context); if (!Value) return None; - return std::move(Requirement.evaluateSelection(*Value)); + return std::move(Requirement.evaluateSelection(Context, *Value)); } private: @@ -82,6 +82,20 @@ struct IsRequirement : std::conditional::value, std::true_type, std::false_type>::type {}; +/// A type trait that returns true when the given type has at least one source +/// selection requirement. +template +struct HasSelectionRequirement + : std::conditional::value || + HasSelectionRequirement::value, + std::true_type, std::false_type>::type {}; + +template +struct HasSelectionRequirement> + : std::true_type {}; + +template struct HasSelectionRequirement : std::false_type {}; + } // end namespace traits } // end namespace refactoring_action_rules } // end namespace tooling diff --git a/include/clang/Tooling/Refactoring/RefactoringActionRules.h b/include/clang/Tooling/Refactoring/RefactoringActionRules.h index 0963c36690..1ae9953485 100644 --- a/include/clang/Tooling/Refactoring/RefactoringActionRules.h +++ b/include/clang/Tooling/Refactoring/RefactoringActionRules.h @@ -15,6 +15,9 @@ namespace clang { namespace tooling { + +class RefactoringRuleContext; + namespace refactoring_action_rules { /// Creates a new refactoring action rule that invokes the given function once @@ -40,6 +43,7 @@ namespace refactoring_action_rules { template std::unique_ptr createRefactoringRule(Expected (*RefactoringFunction)( + const RefactoringRuleContext &, typename RequirementTypes::OutputType...), const RequirementTypes &... Requirements) { static_assert(tooling::traits::IsValidRefactoringResult::value, @@ -56,13 +60,12 @@ template < typename Fn = decltype(&Callable::operator()), typename ResultType = typename internal::LambdaDeducer::ReturnType, bool IsNonCapturingLambda = std::is_convertible< - Callable, - ResultType (*)(typename RequirementTypes::OutputType...)>::value, + Callable, typename internal::LambdaDeducer::FunctionType>::value, typename = typename std::enable_if::type> std::unique_ptr createRefactoringRule(const Callable &C, const RequirementTypes &... Requirements) { - ResultType (*Func)(typename RequirementTypes::OutputType...) = C; + typename internal::LambdaDeducer::FunctionType Func = C; return createRefactoringRule(Func, Requirements...); } diff --git a/include/clang/Tooling/Refactoring/RefactoringActionRulesInternal.h b/include/clang/Tooling/Refactoring/RefactoringActionRulesInternal.h index 7103e47167..e26b92dd64 100644 --- a/include/clang/Tooling/Refactoring/RefactoringActionRulesInternal.h +++ b/include/clang/Tooling/Refactoring/RefactoringActionRulesInternal.h @@ -39,6 +39,10 @@ public: llvm::index_sequence_for()); } + bool hasSelectionRequirement() override { + return traits::HasSelectionRequirement::value; + } + private: /// Returns \c T when given \c Expected>, or \c T otherwise. template @@ -79,7 +83,7 @@ private: template void invokeImpl(RefactoringResultConsumer &Consumer, RefactoringRuleContext &Context, - llvm::index_sequence) { + llvm::index_sequence Seq) { // Initiate the operation. auto Values = std::make_tuple(std::get(Requirements).evaluate(Context)...); @@ -96,8 +100,8 @@ private: return Consumer.handleError(std::move(Error)); } // Perform the operation. - auto Result = - Function(unwrapRequirementResult(std::move(std::get(Values)))...); + auto Result = Function( + Context, unwrapRequirementResult(std::move(std::get(Values)))...); if (!Result) return Consumer.handleError(Result.takeError()); Consumer.handle(std::move(*Result)); @@ -111,8 +115,9 @@ private: /// createRefactoringRule. template struct LambdaDeducer; template -struct LambdaDeducer { +struct LambdaDeducer { using ReturnType = R; + using FunctionType = R (*)(const RefactoringRuleContext &, Args...); }; } // end namespace internal diff --git a/include/clang/Tooling/Refactoring/RefactoringRuleContext.h b/include/clang/Tooling/Refactoring/RefactoringRuleContext.h index d4b08cfd25..877ddef737 100644 --- a/include/clang/Tooling/Refactoring/RefactoringRuleContext.h +++ b/include/clang/Tooling/Refactoring/RefactoringRuleContext.h @@ -13,6 +13,9 @@ #include "clang/Basic/SourceManager.h" namespace clang { + +class ASTContext; + namespace tooling { /// The refactoring rule context stores all of the inputs that might be needed @@ -38,6 +41,15 @@ public: void setSelectionRange(SourceRange R) { SelectionRange = R; } + bool hasASTContext() const { return AST; } + + ASTContext &getASTContext() const { + assert(AST && "no AST!"); + return *AST; + } + + void setASTContext(ASTContext &Context) { AST = &Context; } + private: /// The source manager for the translation unit / file on which a refactoring /// action might operate on. @@ -45,6 +57,9 @@ private: /// An optional source selection range that's commonly used to represent /// a selection in an editor. SourceRange SelectionRange; + /// An optional AST for the translation unit on which a refactoring action + /// might operate on. + ASTContext *AST = nullptr; }; } // end namespace tooling diff --git a/include/clang/Tooling/Refactoring/Rename/USRFindingAction.h b/include/clang/Tooling/Refactoring/Rename/USRFindingAction.h index 644a3988e4..f1c5ae60f8 100644 --- a/include/clang/Tooling/Refactoring/Rename/USRFindingAction.h +++ b/include/clang/Tooling/Refactoring/Rename/USRFindingAction.h @@ -23,6 +23,7 @@ namespace clang { class ASTConsumer; +class ASTContext; class CompilerInstance; class NamedDecl; @@ -37,6 +38,10 @@ namespace tooling { /// - A destructor is canonicalized to its class. const NamedDecl *getCanonicalSymbolDeclaration(const NamedDecl *FoundDecl); +/// Returns the set of USRs that correspond to the given declaration. +std::vector getUSRsForDeclaration(const NamedDecl *ND, + ASTContext &Context); + struct USRFindingAction { USRFindingAction(ArrayRef SymbolOffsets, ArrayRef QualifiedNames, bool Force) diff --git a/include/clang/Tooling/Refactoring/SourceSelectionConstraints.h b/include/clang/Tooling/Refactoring/SourceSelectionConstraints.h index 8e2fcef514..7369ca0408 100644 --- a/include/clang/Tooling/Refactoring/SourceSelectionConstraints.h +++ b/include/clang/Tooling/Refactoring/SourceSelectionConstraints.h @@ -12,6 +12,7 @@ #include "clang/Basic/LLVM.h" #include "clang/Basic/SourceLocation.h" +#include "clang/Tooling/Refactoring/RefactoringRuleContext.h" #include namespace clang { @@ -40,9 +41,10 @@ private: /// A custom selection requirement. class Requirement { - /// Subclasses must implement 'T evaluateSelection(SelectionConstraint) const' - /// member function. \c T is used to determine the return type that is - /// passed to the refactoring rule's function. + /// Subclasses must implement + /// 'T evaluateSelection(const RefactoringRuleContext &, + /// SelectionConstraint) const' member function. \c T is used to determine + /// the return type that is passed to the refactoring rule's function. /// If T is \c DiagnosticOr , then \c S is passed to the rule's function /// using move semantics. /// Otherwise, T is passed to the function directly using move semantics. @@ -64,14 +66,17 @@ namespace internal { template struct EvaluateSelectionChecker : std::false_type {}; template -struct EvaluateSelectionChecker : std::true_type { +struct EvaluateSelectionChecker : std::true_type { using ReturnType = R; using ArgType = A; }; template class Identity : public Requirement { public: - T evaluateSelection(T Value) const { return std::move(Value); } + T evaluateSelection(const RefactoringRuleContext &, T Value) const { + return std::move(Value); + } }; } // end namespace internal diff --git a/include/clang/module.modulemap b/include/clang/module.modulemap index 8f4fdf66cf..8c9736f236 100644 --- a/include/clang/module.modulemap +++ b/include/clang/module.modulemap @@ -138,6 +138,8 @@ module Clang_Tooling { // importing the AST matchers library gives a link dependency on the AST // matchers (and thus the AST), which clang-format should not have. exclude header "Tooling/RefactoringCallbacks.h" + + textual header "Tooling/Refactoring/RefactoringActionRegistry.def" } module Clang_ToolingCore { diff --git a/lib/Tooling/Refactoring/AtomicChange.cpp b/lib/Tooling/Refactoring/AtomicChange.cpp index 082bf5996d..e4cc6a5617 100644 --- a/lib/Tooling/Refactoring/AtomicChange.cpp +++ b/lib/Tooling/Refactoring/AtomicChange.cpp @@ -215,6 +215,15 @@ AtomicChange::AtomicChange(std::string Key, std::string FilePath, RemovedHeaders(std::move(RemovedHeaders)), Replaces(std::move(Replaces)) { } +bool AtomicChange::operator==(const AtomicChange &Other) const { + if (Key != Other.Key || FilePath != Other.FilePath || Error != Other.Error) + return false; + if (!(Replaces == Other.Replaces)) + return false; + // FXIME: Compare header insertions/removals. + return true; +} + std::string AtomicChange::toYAMLString() { std::string YamlContent; llvm::raw_string_ostream YamlContentStream(YamlContent); diff --git a/lib/Tooling/Refactoring/CMakeLists.txt b/lib/Tooling/Refactoring/CMakeLists.txt index b0b66f16f6..ff9cd1ff9e 100644 --- a/lib/Tooling/Refactoring/CMakeLists.txt +++ b/lib/Tooling/Refactoring/CMakeLists.txt @@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS Support) add_clang_library(clangToolingRefactor ASTSelection.cpp AtomicChange.cpp + RefactoringActions.cpp Rename/RenamingAction.cpp Rename/SymbolOccurrences.cpp Rename/USRFinder.cpp diff --git a/lib/Tooling/Refactoring/RefactoringActions.cpp b/lib/Tooling/Refactoring/RefactoringActions.cpp new file mode 100644 index 0000000000..25f055b727 --- /dev/null +++ b/lib/Tooling/Refactoring/RefactoringActions.cpp @@ -0,0 +1,35 @@ +//===--- RefactoringActions.cpp - Constructs refactoring actions ----------===// +// +// 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/RefactoringAction.h" + +namespace clang { +namespace tooling { + +// Forward declare the individual create*Action functions. +#define REFACTORING_ACTION(Name) \ + std::unique_ptr create##Name##Action(); +#include "clang/Tooling/Refactoring/RefactoringActionRegistry.def" + +std::vector> createRefactoringActions() { + std::vector> Actions; + +#define REFACTORING_ACTION(Name) Actions.push_back(create##Name##Action()); +#include "clang/Tooling/Refactoring/RefactoringActionRegistry.def" + + return Actions; +} + +RefactoringActionRules RefactoringAction::createActiveActionRules() { + // FIXME: Filter out rules that are not supported by a particular client. + return createActionRules(); +} + +} // end namespace tooling +} // end namespace clang diff --git a/lib/Tooling/Refactoring/Rename/RenamingAction.cpp b/lib/Tooling/Refactoring/Rename/RenamingAction.cpp index 0aed67f532..384e466015 100644 --- a/lib/Tooling/Refactoring/Rename/RenamingAction.cpp +++ b/lib/Tooling/Refactoring/Rename/RenamingAction.cpp @@ -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" @@ -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 + renameOccurrences(const RefactoringRuleContext &Context, + const NamedDecl *ND) { + std::vector 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> + 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 createLocalRenameAction() { + return llvm::make_unique(); +} + Expected> createRenameReplacements(const SymbolOccurrences &Occurrences, const SourceManager &SM, diff --git a/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp b/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp index 43287acb95..0c746bbbcb 100644 --- a/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp +++ b/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp @@ -154,6 +154,12 @@ private: }; } // namespace +std::vector getUSRsForDeclaration(const NamedDecl *ND, + ASTContext &Context) { + AdditionalUSRFinder Finder(ND, Context); + return Finder.Find(); +} + class NamedDeclFindingConsumer : public ASTConsumer { public: NamedDeclFindingConsumer(ArrayRef SymbolOffsets, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 084f405fee..a3a70282a7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,6 +48,7 @@ list(APPEND CLANG_TEST_DEPS clang-offload-bundler clang-import-test clang-rename + clang-refactor clang-diff ) diff --git a/test/Refactor/LocalRename/Field.cpp b/test/Refactor/LocalRename/Field.cpp new file mode 100644 index 0000000000..db8ada5fcc --- /dev/null +++ b/test/Refactor/LocalRename/Field.cpp @@ -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) {}; diff --git a/test/Refactor/tool-common-options.c b/test/Refactor/tool-common-options.c new file mode 100644 index 0000000000..e20c290ae7 --- /dev/null +++ b/test/Refactor/tool-common-options.c @@ -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 diff --git a/test/Refactor/tool-test-support.c b/test/Refactor/tool-test-support.c new file mode 100644 index 0000000000..3eb8d22f51 --- /dev/null +++ b/test/Refactor/tool-test-support.c @@ -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 diff --git a/test/clang-rename/Field.cpp b/test/clang-rename/Field.cpp deleted file mode 100644 index c0e9a019e4..0000000000 --- a/test/clang-rename/Field.cpp +++ /dev/null @@ -1,15 +0,0 @@ -class Baz { - int Foo; /* Test 1 */ // CHECK: int Bar; -public: - Baz(); -}; - -Baz::Baz() : Foo(0) /* Test 2 */ {} // CHECK: Baz::Baz() : Bar(0) - -// Test 1. -// RUN: clang-rename -offset=18 -new-name=Bar %s -- | sed 's,//.*,,' | FileCheck %s -// Test 2. -// RUN: clang-rename -offset=89 -new-name=Bar %s -- | sed 's,//.*,,' | FileCheck %s - -// To find offsets after modifying the file, use: -// grep -Ubo 'Foo.*' diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 17ad9f4a1a..6e18724a32 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -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) diff --git a/tools/clang-refactor/CMakeLists.txt b/tools/clang-refactor/CMakeLists.txt new file mode 100644 index 0000000000..27217c11ac --- /dev/null +++ b/tools/clang-refactor/CMakeLists.txt @@ -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) diff --git a/tools/clang-refactor/ClangRefactor.cpp b/tools/clang-refactor/ClangRefactor.cpp new file mode 100644 index 0000000000..047e276d1b --- /dev/null +++ b/tools/clang-refactor/ClangRefactor.cpp @@ -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 + +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 + 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 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 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 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 Callback) = 0; +}; + +/// Stores the parsed -selection=test: 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 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 Callback) override { + return TestSelections.foreachRange(SM, Callback); + } + +private: + TestSelectionRangesInFile TestSelections; +}; + +std::unique_ptr +SourceSelectionArgument::fromString(StringRef Value) { + if (Value.startswith("test:")) { + StringRef Filename = Value.drop_front(strlen("test:")); + Optional ParsedTestSelection = + findTestSelectionRanges(Filename); + if (!ParsedTestSelection) + return nullptr; // A parsing error was already reported. + return llvm::make_unique( + std::move(*ParsedTestSelection)); + } + // FIXME: Support true selection ranges. + llvm::errs() << "error: '-selection' option must be specified using " + ":: or " + "::-: format"; + return nullptr; +} + +/// A subcommand that corresponds to individual refactoring action. +class RefactoringActionSubcommand : public cl::SubCommand { +public: + RefactoringActionSubcommand(std::unique_ptr Action, + RefactoringActionRules ActionRules, + cl::OptionCategory &Category) + : SubCommand(Action->getCommand(), Action->getDescription()), + Action(std::move(Action)), ActionRules(std::move(ActionRules)) { + Sources = llvm::make_unique>( + cl::Positional, cl::ZeroOrMore, cl::desc(" [... ]"), + 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>( + "selection", + cl::desc("The selected source range in which the refactoring should " + "be initiated (::-: or " + "::)"), + 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 getSources() const { return *Sources; } + +private: + std::unique_ptr Action; + RefactoringActionRules ActionRules; + std::unique_ptr> Sources; + std::unique_ptr> Selection; + std::unique_ptr 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> SubCommands; + + ClangRefactorTool() { + std::vector> 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( + std::move(Action), Action->createActiveActionRules(), + opts::CommonRefactorOptions)); + } + } + + using TUCallbackType = llvm::function_ref; + + /// 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 Compilations; + if (opts::NoDatabases) { + // FIXME (Alex L): Support compilation options. + Compilations = + llvm::make_unique( + ".", std::vector()); + } 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 newASTConsumer() { + return llvm::make_unique(std::move(Callback)); + } + }; + + ClangTool Tool(*Compilations, Subcommand.getSources()); + ActionWrapper ToolAction(std::move(Callback)); + std::unique_ptr 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 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 &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 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; +} diff --git a/tools/clang-refactor/TestSupport.cpp b/tools/clang-refactor/TestSupport.cpp new file mode 100644 index 0000000000..66e1bb7807 --- /dev/null +++ b/tools/clang-refactor/TestSupport.cpp @@ -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 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(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 Files; + for (const auto &Change : Changes) + Files.insert(Change.getFilePath()); + tooling::ApplyChangesSpec Spec; + Spec.Cleanup = false; + for (const auto &File : Files) { + llvm::ErrorOr> 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 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>> Results; +}; + +std::pair getLineColumn(StringRef Filename, + unsigned Offset) { + ErrorOr> 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 CanonicalResult; + Optional CanonicalErrorMessage; + for (auto &I : llvm::enumerate(Group.value())) { + Expected &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 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 +TestSelectionRangesInFile::createConsumer() const { + return llvm::make_unique(*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 +findTestSelectionRanges(StringRef Filename) { + ErrorOr> 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> 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 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{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 diff --git a/tools/clang-refactor/TestSupport.h b/tools/clang-refactor/TestSupport.h new file mode 100644 index 0000000000..9d082a74ec --- /dev/null +++ b/tools/clang-refactor/TestSupport.h @@ -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 +#include + +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 Ranges; + }; + std::vector GroupedRanges; + + TestSelectionRangesInFile(TestSelectionRangesInFile &&) = default; + TestSelectionRangesInFile &operator=(TestSelectionRangesInFile &&) = default; + + bool foreachRange(const SourceManager &SM, + llvm::function_ref Callback) const; + + std::unique_ptr createConsumer() const; + + void dump(llvm::raw_ostream &OS) const; +}; + +/// Extracts the grouped selection ranges from the file that's specified in +/// the -selection=test: 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 findTestSelectionRanges(StringRef Filename); + +} // end namespace refactor +} // end namespace clang + +#endif // LLVM_CLANG_TOOLS_CLANG_REFACTOR_TEST_SUPPORT_H diff --git a/unittests/Tooling/RefactoringActionRulesTest.cpp b/unittests/Tooling/RefactoringActionRulesTest.cpp index 62c751ac12..cf91b5fc8f 100644 --- a/unittests/Tooling/RefactoringActionRulesTest.cpp +++ b/unittests/Tooling/RefactoringActionRulesTest.cpp @@ -57,7 +57,8 @@ createReplacements(const std::unique_ptr &Rule, TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) { auto ReplaceAWithB = - [](std::pair Selection) + [](const RefactoringRuleContext &, + std::pair Selection) -> Expected { const SourceManager &SM = Selection.first.getSources(); SourceLocation Loc = Selection.first.getRange().getBegin().getLocWithOffset( @@ -71,7 +72,8 @@ TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) { class SelectionRequirement : public selection::Requirement { public: std::pair - evaluateSelection(selection::SourceSelectionRange Selection) const { + evaluateSelection(const RefactoringRuleContext &, + selection::SourceSelectionRange Selection) const { return std::make_pair(Selection, 20); } }; @@ -127,8 +129,10 @@ TEST_F(RefactoringActionRulesTest, MyFirstRefactoringRule) { } TEST_F(RefactoringActionRulesTest, ReturnError) { - Expected (*Func)(selection::SourceSelectionRange) = - [](selection::SourceSelectionRange) -> Expected { + Expected (*Func)(const RefactoringRuleContext &, + selection::SourceSelectionRange) = + [](const RefactoringRuleContext &, + selection::SourceSelectionRange) -> Expected { return llvm::make_error( "Error", llvm::make_error_code(llvm::errc::invalid_argument)); }; @@ -155,13 +159,14 @@ TEST_F(RefactoringActionRulesTest, ReturnInitiationDiagnostic) { class SelectionRequirement : public selection::Requirement { public: Expected> - evaluateSelection(selection::SourceSelectionRange Selection) const { + evaluateSelection(const RefactoringRuleContext &, + selection::SourceSelectionRange Selection) const { return llvm::make_error( "bad selection", llvm::make_error_code(llvm::errc::invalid_argument)); } }; auto Rule = createRefactoringRule( - [](int) -> Expected { + [](const RefactoringRuleContext &, int) -> Expected { llvm::report_fatal_error("Should not run!"); }, requiredSelection(SelectionRequirement())); @@ -201,7 +206,8 @@ Optional findOccurrences(RefactoringActionRule &Rule, TEST_F(RefactoringActionRulesTest, ReturnSymbolOccurrences) { auto Rule = createRefactoringRule( - [](selection::SourceSelectionRange Selection) + [](const RefactoringRuleContext &, + selection::SourceSelectionRange Selection) -> Expected { SymbolOccurrences Occurrences; Occurrences.push_back(SymbolOccurrence( -- 2.40.0