RefactoringEngine.rst 10.7 KB

Clang's refactoring engine

This document describes the design of Clang's refactoring engine and provides a couple of examples that show how various primitives in the refactoring API can be used to implement different refactoring actions. The :doc:`LibTooling` library provides several other APIs that are used when developing a refactoring action.

Refactoring engine can be used to implement local refactorings that are initiated using a selection in an editor or an IDE. You can combine :doc:`AST matchers<LibASTMatchers>` and the refactoring engine to implement refactorings that don't lend themselves well to source selection and/or have to query ASTs for some particular nodes.

We assume basic knowledge about the Clang AST. See the :doc:`Introduction to the Clang AST <IntroductionToTheClangAST>` if you want to learn more about how the AST is structured.

Introduction

Clang's refactoring engine defines a set refactoring actions that implement a number of different source transformations. The clang-refactor command-line tool can be used to perform these refactorings. Certain refactorings are also available in other clients like text editors and IDEs.

A refactoring action is a class that defines a list of related refactoring operations (rules). These rules are grouped under a common umbrella - a single clang-refactor command. In addition to rules, the refactoring action provides the action's command name and description to clang-refactor. Each action must implement the RefactoringAction interface. Here's an outline of a local-rename action:

class LocalRename final : 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";
  }

  RefactoringActionRules createActionRules() const override {
    ...
  }
};

Refactoring Action Rules

An individual refactoring action 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 actions that define a set of different rules that produce similar results. For example, the "add missing switch cases" refactoring operation typically adds missing cases to one switch at a time. However, it could be useful to have a refactoring that works on all switches that operate on a particular enum, as one could then automatically update all of them after adding a new enum constant. To achieve that, we can create two different rules that will use one clang-refactor subcommand. The first rule will describe a local operation that's initiated when the user selects a single switch. The second rule will describe a global operation that works across translation units and is initiated when the user provides the name of the enum to clang-refactor (or the user could select the enum declaration instead). The clang-refactor tool will then analyze the selection and other options passed to the refactoring action, and will pick the most appropriate rule for the given selection and other options.

Rule Types

Clang's refactoring engine supports several different refactoring rules:

  • SourceChangeRefactoringRule produces source replacements that are applied to the source files. Subclasses that choose to implement this rule have to implement the createSourceReplacements member function. This type of rule is typically used to implement local refactorings that transform the source in one translation unit only.
  • FindSymbolOccurrencesRefactoringRule produces a "partial" refactoring result: a set of occurrences that refer to a particular symbol. This type of rule is typically used to implement an interactive renaming action that allows users to specify which occurrences should be renamed during the refactoring. Subclasses that choose to implement this rule have to implement the findSymbolOccurrences member function.

The following set of quick checks might help if you are unsure about the type of rule you should use:

  1. If you would like to transform the source in one translation unit and if you don't need any cross-TU information, then the SourceChangeRefactoringRule should work for you.
  2. If you would like to implement a rename-like operation with potential interactive components, then FindSymbolOccurrencesRefactoringRule might work for you.

How to Create a Rule

Once you determine which type of rule is suitable for your needs you can implement the refactoring by subclassing the rule and implementing its interface. The subclass should have a constructor that takes the inputs that are needed to perform the refactoring. For example, if you want to implement a rule that simply deletes a selection, you should create a subclass of SourceChangeRefactoringRule with a constructor that accepts the selection range:

class DeleteSelectedRange final : public SourceChangeRefactoringRule {
public:
  DeleteSelection(SourceRange Selection) : Selection(Selection) {}

  Expected<AtomicChanges>
  createSourceReplacements(RefactoringRuleContext &Context) override {
    AtomicChange Replacement(Context.getSources(), Selection.getBegin());
    Replacement.replace(Context.getSource,
                        CharSourceRange::getCharRange(Selection), "");
    return { Replacement };
  }
private:
  SourceRange Selection;
};

The rule's subclass can then be added to the list of refactoring action's rules for a particular action using the createRefactoringActionRule function. For example, the class that's shown above can be added to the list of action rules using the following code:

RefactoringActionRules Rules;
Rules.push_back(
  createRefactoringActionRule<DeleteSelectedRange>(
        SourceRangeSelectionRequirement())
);

The createRefactoringActionRule function takes in a list of refactoring action rule requirement values. These values describe the initiation requirements that have to be satisfied by the refactoring engine before the provided action rule can be constructed and invoked. The next section describes how these requirements are evaluated and lists all the possible requirements that can be used to construct a refactoring action rule.

Refactoring Action Rule Requirements

A refactoring action rule requirement is a value whose type derives from the RefactoringActionRuleRequirement class. The type must define an evaluate member function that returns a value of type Expected<...>. When a requirement value is used as an argument to createRefactoringActionRule, that value is evaluated during the initiation of the action rule. The evaluated result is then passed to the rule's constructor unless the evaluation produced an error. For example, the DeleteSelectedRange sample rule that's defined in the previous section will be evaluated using the following steps:

  1. SourceRangeSelectionRequirement's evaluate member function will be called first. It will return an Expected<SourceRange>.
  2. If the return value is an error the initiation will fail and the error will be reported to the client. Note that the client may not report the error to the user.
  3. Otherwise the source range return value will be used to construct the DeleteSelectedRange rule. The rule will then be invoked as the initiation succeeded (all requirements were evaluated successfully).

The same series of steps applies to any refactoring rule. Firstly, the engine will evaluate all of the requirements. Then it will check if these requirements are satisfied (they should not produce an error). Then it will construct the rule and invoke it.

The separation of requirements, their evaluation and the invocation of the refactoring action rule allows the refactoring clients to:

  • Disable refactoring action rules whose requirements are not supported.
  • Gather the set of options and define a command-line / visual interface that allows users to input these options without ever invoking the action.

Selection Requirements

The refactoring rule requirements that require some form of source selection are listed below:

  • SourceRangeSelectionRequirement evaluates to a source range when the action is invoked with some sort of selection. This requirement should be satisfied when a refactoring is initiated in an editor, even when the user has not selected anything (the range will contain the cursor's location in that case).

Other Requirements

There are several other requirements types that can be used when creating a refactoring rule:

  • The RefactoringOptionsRequirement requirement is an abstract class that should be subclassed by requirements working with options. The more concrete OptionRequirement requirement is a simple implementation of the aforementioned class that returns the value of the specified option when it's evaluated. The next section talks more about refactoring options and how they can be used when creating a rule.

Refactoring Options

Refactoring options are values that affect a refactoring operation and are specified either using command-line options or another client-specific mechanism. Options should be created using a class that derives either from the OptionalRequiredOption or RequiredRefactoringOption. The following example shows how one can created a required string option that corresponds to the -new-name command-line option in clang-refactor:

class NewNameOption : public RequiredRefactoringOption<std::string> {
public:
  StringRef getName() const override { return "new-name"; }
  StringRef getDescription() const override {
    return "The new name to change the symbol to";
  }
};

The option that's shown in the example above can then be used to create a requirement for a refactoring rule using a requirement like OptionRequirement:

createRefactoringActionRule<RenameOccurrences>(
  ...,
  OptionRequirement<NewNameOption>())
);