Providing Quick Fixes

A quick fix is used by clients to provide a set of possible changes to code that are based on diagnostics reported against the code. Quick fixes are intended to help users resolve the issue being reported.

If your plugin generates any diagnostics then you should consider providing support for automatically fixing those diagnostics. There is often more than one potential way of fixing a given problem, so it is possible for your plugin to provide multiple fixes for a single problem.

For example, if an undefined identifier is used in the code, you might return a fix to create an appropriate definition for the identifier. If there is a similar identifier that is already defined, you might also return a second fix to replace the undefined identifier with the defined identifier.

The latter example illustrates that fixes can be conditionally returned. You will produce a better UX if only those fixes that actually make sense in the given context are returned. If a lot of work is required to determine which fixes make sense, it is possible to improve performance by generating different diagnostics for the same issue, depending on the context in which the issue occurs.

In addition, fixes have a priority associated with them. The priority allows the client to display the fixes that are most likely to be of use closer to the top of the list when there are multiple fixes available.

Implementation details

When appropriate, the analysis server will send your plugin an edit.getFixes request. The request includes the file and offset associated with the diagnostics for which fixes should be generated. Fixes are typically produced for all of the diagnostics on a given line of code. Your plugin should only return fixes associated with the errors that it produced earlier.

When an edit.getFixes request is received, the method handleEditGetFixes will be invoked. This method is responsible for returning a response that contains the available fixes.

The easiest way to implement this method is by adding the classes FixesMixin and DartFixesMixin (from package:analyzer_plugin/plugin/fix_mixin.dart) to the list of mixins for your subclass of ServerPlugin. This will leave you with one abstract method that you need to implement: getFixContributors. That method is responsible for returning a list of FixContributors. It is the fix contributors that produce the actual fixes. (Most plugins will only need a single fix contributor.)

To write a fix contributor, create a class that implements FixContributor. The interface defines a single method named computeFixes. The method has two arguments: a FixesRequest that describes the errors that should be fixed and a FixCollector through which fixes are to be added.

If you mix in the class DartFixesMixin, then the list of errors available through the request object will only include the errors for which fixes should be returned and the request will be an instance of DartFixesRequest, which also has analysis results.

The class FixContributorMixin defines a simple implementation of this method that captures the two arguments in fields, iterates through the errors, and invokes a method named computeFixesForError for each of the errors for which fixes are to be computed.

Example

Start by creating a class that implements FixContributor and that mixes in the class FixContributorMixin, then implement the method computeFixesForError. This method is typically implemented by a series of if statements that test the error code and invoke individual methods that compute the actual fixes to be proposed. (In addition to keeping the method computeFixesForError shorter, this also allows some fixes to be used for multiple error codes.)

To learn about the support available for creating the edits, see Creating Edits.

For example, your contributor might look something like the following:

class MyFixContributor extends Object
    with FixContributorMixin
    implements FixContributor {
  static FixKind defineComponent =
      FixKind('defineComponent', 100, "Define a component named {0}");

  AnalysisSession get session => request.result.session;

  @override
  Future<void> computeFixesForError(AnalysisError error) async {
    ErrorCode code = error.errorCode;
    if (code == MyErrorCode.undefinedComponent) {
      await _defineComponent(error);
      await _useExistingComponent(error);
    }
  }

  Future<void> _defineComponent(AnalysisError error) async {
    // TODO Get the name from the source code.
    String componentName = null;
    ChangeBuilder builder = ChangeBuilder(session: session);
    await changeBuilder.addDartFileEdit(path,
        (DartFileEditBuilder fileEditBuilder) {
      // TODO Build the edit to insert the definition of the component.
    });
    addFix(error, defineComponent, builder, args: [componentName]);
  }

  Future<void> _useExistingComponent(AnalysisError error) async {
    // ...
  }
}

Given a contributor like the one above, you can implement your plugin similar to the following:

class MyPlugin extends ServerPlugin with FixesMixin, DartFixesMixin {
  // ...

  @override
  List<FixContributor> getFixContributors(String path) {
    return <FixContributor>[MyFixContributor()];
  }
}