Creating a Custom Plugin for Dart Analyzer
Hi! My name’s Dmitry, and I’m a front end developer at Wrike. In this article, I’ll show you how to develop a custom Dart code analyzer plugin. It will come in handy to those who feel that the basic Dart analyzer lacks functionality when it comes to static analysis, and to those who want to try to develop a simple analyzer themselves.
A plugin is an executable code that communicates with an analysis server and performs additional analysis of a Dart code. A plugin is executed in the same VM as the server, but in a separate isolate. The server takes care of the integration with the existing IDEs, leaving you free to focus on the plugin development process.
The server provides data. The AnalysisDriver, which gathers information on analyzed files, then converts the data to AST. The plugin is then provided the AST to work with — from highlighting code errors to statistics aggregation.
Plugin capabilities and step-by-step creation
A plugin highlights errors (and shows ways to correct them), syntax, and navigation, and executes autocomplete. Selecting a block of code, you can add assist, which can wrap it into something or format it appropriately.
Refer to the API Specification plugin for more.
Here’s a brief list of steps for creating the plugin:
- Create a dependencies package for the analyzer and the plugin.
- Create a class inherited from ServerPlugin.
- Implement basic getters and init method from the server.
- Add a starter function.
- Create a separate subpackage in … tools/analyzer_plugin/.
- Set dependency on the package to link the plugin with the client.
- Add the plugin name to analysis_options in the plugin block.
Here’s what a simple plugin might look like:
We have the three getters — name, version, and fileGlobsToAnalyze — where:
- Version is the version of the Server API used by the plugin, and it has to match the existing version (e.g., 1.0.0-alpha.0)
- fileGlobsToAnalyze is a glob pattern that shows the files that this plugin is interested in analyzing.
Method initialization should have a dartDriver in it:
This is the only driver that comes in a package with the analyzer by default. However, API allows you to create a driver for any language. The only downside is the amount of effort it takes, such as the driver and classes initialization, configuration, etc.
You need to subscribe to analysis results when creating a driver. The driver will push the event with the results.
The structure will look similar to this:
In my experience, a compilation unit is useful for code analysis. It’s also convenient to use, since it contains the AST structure of the DART file. This code fragment also contains additional information about libraries, typeSystem, etc.
Here are visitor patterns that can parse the AST-structure:
RecursiveAstVisitor recursively visits all AST-nodes. For example, it will visit the [Block] node and all of its sub-nodes as well.
GeneralizingAstVisitor, similarly to RecursiveAstVisitor, recursively visits all AST-nodes. However, when it visits a specific node, it will invoke the visit method specific to that particular type of node, and additional methods for the node’s superclasses.
SimpleAstVisitor visits all AST-nodes and does nothing. This is for cases when a recursive visitor isn’t needed.
ThrowingAstVisitor throws an exception in case any of the visit methods invoked haven’t been overridden.
TimedAstVisitor measures visit call timing.
UnifyingAstVisitor recursively visits all of the AST-nodes (similar to RecursiveAstVisitor), but every node will also be visited by using a unified visitNode method.
Here’s what a simple implementation of a recursive visitor might look like:
Invoke a visitChildren method to visit a CompilationUnit. If a visitor overrides any AST visit method, it will be invoked via visitChildren. You can then further analyze the code.
To locate and initialize the plugin, initiate a Starter function and execute it in a specific directory — “…tools/analyzer_plugin/bin/plugin.dart.”
It can be a separate package or a sub-package, but according to the documentation that’s exactly where the plugin initializer must be located.
The plugin is easy to configure: The driver provides access to all content in “analysis_options.yaml.” You can parse it and extract the data you need. Ideally, you want to parse the config file during the driver creation.
Here’s an example of how we configured the plugin in our Dart code metrics project (which is a custom plugin):
The interaction between the server and the plugin is hard to cover, but CompilationUnits covers the rest of the code just fine. In testing, you can use familiar packages (e.g., test, mokito) and additional functions, which help convert lines of code and content into an AST.
Here’s what a simple test might look like:
It can be complicated. There are three methods.
1. Use logs. This might not be the most effective method, but logs are really helpful. Logs helped us understand why the plugin didn’t process opened files during editing in our project.
Logs can be cluttered and generate a lot of data output. However, they may help you detect some errors.
2. Refer to diagnostics. Call diagnostics via Dart by running Analyzer Diagnostics command in VS Code.
Here you can get server information, plugins in use, errors and other useful information.
3. Use Observatory and DartVM. I won’t cover this one, since there’s a Dart React binding plugin. Its documentation extensively describes how to debug using Observatory.
Issues you might encounter
The lack of examples is the main issue when creating a plugin. It makes it difficult to pinpoint what’s wrong and find a solution on the spot. And documentation is mostly code commentary, so it’s also quite complicated to use.
There’s no obvious indication that you can’t only analyze Dart code, but also implement drivers for other languages to analyze them, too. For example, there’s a DartAngular-plugin to analyze HTML code.
- Dart code metrics — our open-source project for Dart code static analysis. It allows us to gather code metrics and is essentially a set of extra rules for the analyzer. It may be of interest to those who want to try writing a static analysis tool, or get more familiar with plugins.
- Built value — an example of a plugin with a dartDriver.
- DartAngular plugin — a plugin featuring HTML analysis.
- Over react — a plugin for Dart-React binding with useful debug examples.