How to create a custom Dart analyzer plugin

Hey! My name is Dima and I am a frontend developer at Wrike. In this article I will tell you about how to write a plugin for analyzing Dart code. The text will be useful for those who lack the current functionality of the free-of-charge analyzer for static analysis or if you just want to try to write a simple analyzer yourself.

According to the documentation, a plugin is a code that communicates with an analysis server and additionally analyzes the code. It runs in the same VM as the server, but in a separate isolate. Integration with existing IDEs lies entirely on the server side, which allows you not to think about it when developing a plugin.

The server provides data that, using the analyzer driver (responsible for collecting information about the analyzed files), is converted into AST. The plugin is provided with an AST, with which you can work: additionally highlight errors in the code or collect statistics.

Plugin features and step-by-step creation scheme

Using the plugin, you can show errors and how to fix them, do syntax highlighting, navigation and autocompletion. For example, when highlighting a block of code, you can add an assist that will wrap that block in something or format it.

Plugin capabilities are described in detail in the specification

A short step-by-step diagram for creating a plugin looks like this:

  1. Create a package with dependencies for the analyzer and plugin.

  2. Create a class that inherits from ServerPlugin.

  3. Implement basic getters and a method for initialization from the server.

  4. Add the “starter” function.

  5. Create a separate subpackage located in tools / analyzerplugin /.

  6. To use the plugin in the client, specify the package dependency.

  7. Add the plugin name to analysis_options, in the plugin block.

Plugin implementation

A simple plugin looks something like this:

class CustomPlugin extends ServerPlugin {
  CustomPlugin(ResourceProvider provider): super(provider);

  @override
  List<String> get fileGlobsToAnalyze => const ['*.dart'];

  @override
  String get name => 'My custom plugin';

  @override
  String get version => '1.0.0';

  @override
  AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot) {
    // implementation
  }
}

Three getters – the name of the plugin, the version of the server API used (must match one of the existing versions, for example, 1.0.0-alpha.0) the plugin works with, and the pattern for which files to parse.

In the initialization method, you need to create a free driver:

  @override
  AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
    final root = ContextRoot(contextRoot.root, contextRoot.exclude,
        pathContext: resourceProvider.pathContext)
      ..optionsFilePath = contextRoot.optionsFile;

    final contextBuilder = ContextBuilder(resourceProvider, sdkManager, null)
      ..analysisDriverScheduler = analysisDriverScheduler
      ..byteStore = byteStore
      ..performanceLog = performanceLog
      ..fileContentOverlay = fileContentOverlay;

    final dartDriver = contextBuilder.buildDriver(root);

    dartDriver.results.listen((analysisResult) {
      _processResult(dartDriver, analysisResult);
    });

    return dartDriver;
  }

Only this driver comes with the analyzer by default. But the API allows you to create a driver for any language. The only drawback is that it will take enough effort: initialization of the driver and other classes, configuration, and so on.

When creating a driver, you need to subscribe to the result of file processing. For each file, the driver will push a result event.

The structure of the result looks like this:

abstract class ResolveResult implements AnalysisResultWithErrors {
  /// The content of the file that was scanned, parsed and resolved.
  String get content;

  /// The element representing the library containing the compilation [unit].
  LibraryElement get libraryElement;

  /// The type provider used when resolving the compilation [unit].
  TypeProvider get typeProvider;

  /// The type system used when resolving the compilation [unit].
  TypeSystem get typeSystem;

  /// The fully resolved compilation unit for the [content].
  CompilationUnit get unit;
}

In my experience, the Compilation unit is useful for code analysis. It is convenient to work with it, because it contains the AST tree of the dart file. This code snippet also contains other information about libraries, the type system, and so on.

To traverse the tree, a set of ready-made Visitors:

  • RecursiveAstVisitor

  • GeneralizingAstVisitor

  • SimpleAstVisitor

  • ThrowingAstVisitor

  • TimedAstVisitor

  • UnifyingAstVisitor

RecursiveAstVisitor recursively traverses all AST nodes. For example, when traversing a node [Block], the visitor will go through all child nodes.

GeneralizingAstVisitor recursively traverses all AST nodes (similar to RecursiveAstVisitor), but for all nodes, not only methods will be called to traverse this node type, but also to traverse the base class for this type.

SimpleAstVisitor does nothing when traversing AST nodes. Suitable for cases where recursive traversal is not required.

ThrowingAstVisitor throws an exception when traversing an AST node whose traversal method has not been overridden in the inheritor.

TimedAstVisitor allows you to measure the AST traversal time.

UnifyingAstVisitor recursively bypasses all AST nodes (similar to RecursiveAstVisitor), but additionally calls the common visitNode method for all nodes.

A simple implementation of a recursive visor might look like this:

String checkCompilationUnit(CompilationUnit unit) {
  final visitor = _Visitor();

  unit.visitChildren(visitor);

  return visitor.result;
}

class _Visitor extends RecursiveAstVisitor<void> {
  String result = ‘’;

  @override
  void visitMethodInvocation(MethodInvocation node) {
    super.visitMethodInvocation(node);

    // implementation
  }
}

To bypass the unit, call the visitChildren method. If the visitor overrides any method for traversing the AST node, then we will get into this method when passing the visitor to visitChildren. And then you can perform any manipulations with the code.

To detect and initialize the plugin, you need to implement the Starter function and call it in a special directory – tools / analyzerplugin / bin / plugin.dart.

void start(Iterable<String> _, SendPort sendPort) {
  ServerPluginStarter(CustomPlugin(PhysicalResourceProvider.INSTANCE))
      .start(sendPort);

It can be a separate package or a subpackage, but this location is strictly written in the documentation: this is exactly where the plugin initializer should be.

The plugin is easy to configure: the driver provides access to all analysis_options.yaml content. The content can be parsed and retrieved the necessary data from it. The best way is to parse the configuration file when creating the driver.

An example of how we configure a plugin in our project Dart code metrics:

dart_code_metrics:
  anti-patterns:
    - long-method
    - long-parameter-list
  metrics:
    cyclomatic-complexity: 20
    number-of-arguments: 4
  metrics-exclude:
    - test/**
  rules:
    - binary-expression-operand-order
    - double-literal-format
    - newline-before-return
    - no-boolean-literal-compare
    - no-equal-then-else
    - prefer-conditional-expressions
    - prefer-trailing-comma-for-collection

We use sheets and yaml scalar values.

Testing

When testing, it is difficult to cover the interaction between the server and the plugin, but the rest of the code is perfectly covered by units. For testing, you can use familiar packages (test, mokito) and additional functions that allow you to convert strings or file content to AST.

The first line converts to an AST string, the second to the content file, and the third fixes all imports and provides information about the types used.

const _content=""'

Object function(Object param) {
  return null;
}

''';

void main() {
  test('should return correct result', () {
    final sourceUrl = Uri.parse('/example.dart');

    final parseResult = parseString(
        content: _content,
        featureSet: FeatureSet.fromEnableFlags([]),
        throwIfDiagnostics: false);

    final result = checkCompilationUnit(parseResult.unit);

    expect(
      result,
      isNotEmpty,
    );
  });
}

Debag

Debugging is not easy. There are three ways.

The first is to use logs. Yes, this may not be the most efficient way, but they do help. When working on our project, there was a case when it was the logs that helped to understand why the already open files were not processed by the plugin when editing.

The logs are very “noisy”, a lot of things are generated there. But they can help catch some bugs.

The second way is to look at the diagnostics. It can be opened using the Dart command: Open Analyzer Diagnostics.

You can see information on the server, connected plugins and errors
You can see information on the server, connected plugins and errors

The third way is to use Observatory and datum VM. But in this article I will not consider it in detail, because there is plugin for free binding to the reaction. Its documentation describes in detail and clearly how to debug the Observatory.

Problems to face

The main problem when creating a plugin is the lack of examples. Therefore, it is very difficult to figure out what is going wrong and find a quick solution. It is also quite difficult to work with the documentation, because it is mostly just comments in the code.

It is not explicitly stated anywhere that it is possible not only to analyze the code in Dart, but also to implement drivers for other languages ​​and try to analyze them. So, there is an example with HTML parsing for the DartAngular plugin.

useful links

Documentation

Dart code metrics Is our open source project for static analysis of the gift code. May be of interest to those who want to try their hand at writing static analysis or just get to know more about plugins. It is a set of additional rules for the analyzer, and also allows you to collect metrics by code.

Built value – an example of a plug-in that uses a free driver.

DartAngular plugin – plugin that has an example with HTML parsing.

Over react – plugin for free binding to react, which has useful examples of debugging.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *