Original address: medium.com/wriketechcl…

Incendial.medium.com/

Published: March 2, 2021-5 minutes to read

A: hi! My name is 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 plug-in. This article will come in handy for those who feel that the basic Dart profiler lacks functionality for static analysis, and for those who want to try developing a simple profiler on their own.

A plug-in is executable code that communicates with the analysis server and performs additional analysis on the Dart code. The plug-in executes in the same virtual machine as the server, but in a separate quarantine. The server takes care of the integration with the existing IDE, leaving you free to focus on the plug-in development process.

The server provides the data. AnalysisDriver, which gathers information about an analysis file and then converts the data into an AST. The plug-in is then provided with an AST to do its work — from highlighting code errors to statistical summaries.

Plug-in functionality and creation steps

A plug-in can highlight errors (and show how to correct them), syntax, and navigation, and perform auto-complete. Select a block of code that you can add helper, which can wrap it into something or an appropriate format.

See the API specification plug-in for more information.

Here are a few steps to create a plug-in.

  1. Create a dependency package for the profiler and plug-in.
  2. Create a class that extends from ServerPlugin.
  3. Implement basic getters and init methods from the server.
  4. Add a start function.
  5. In the… Create a separate subpackage in tools/analyzer_plugin/.
  6. Set up dependencies on the package to connect the plug-in to the client.
  7. Add the plug-in name analysis_Options to the plug-in block.

Plug-in implementation

Here’s what a simple plug-in looks like.

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(plugin.ContextRoot contextRoot) {
    return null; }}Copy the code

We have three accessors — name, version, and fileGlobsToAnalyze — where:

  • Version is the Version of the server API used by the plug-in, which must match the existing Version (for example, 1.0.0-alpha.0).
  • FileGlobsToAnalyze is a glob pattern that shows the files the plug-in is interested in analyzing.

Method initialization should have a dartDriver in it.

@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
  finalroot = 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;
}
Copy the code

This is the only driver, and by default, it is a package of profilers. However, the API allows you to create a driver for any language. The only downside is that it takes a lot of effort, such as driver and class initialization, configuration, etc.

When creating a driver, you need to subscribe to the analysis results. The driver pushes events and results.

It’s going to look something 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;
}
Copy the code

In my experience, compilation units are useful for code analysis. It is also convenient to use because it includes the AST structure of the DART file. The snippet also contains additional information about libraries, typeSystem, and so on.

Here is the visitor pattern that can parse the AST structure.

  • RecursiveAstVisitor
  • GeneralizingAstVisitor
  • SimpleAstVisitor
  • ThrowingAstVisitor
  • TimedAstVisitor
  • UnifyingAstVisitor

RecursiveAstVisitor recursively visits all AST nodes. For example, it will access the [Block] node and all of its children.

The GeneralizingAstVisitor, similar to the RecursiveAstVisitor, recursively accesses all AST nodes. However, when it accesses a particular node, it calls access methods specific to that particular type of node, as well as additional methods of the node’s superclass.

The SimpleAstVisitor visits all AST nodes and does nothing. This applies when recursive visitors are not required.

The ThrowingAstVisitor throws an exception if any of the invoked access methods have not been overridden.

TimedAstVisitor measures access call timing.

The UnifyingAstVisitor visits all AST nodes recursively (similar to the RecursiveAstVisitor), but each node is also accessed by using the unified visitNode method.

Here is a simple implementation of the recursive visitor.

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

  unit.visitChildren(visitor);

  return visitor.result;
}

class _Visitor extends RecursiveAstVisitor<void> {
  StringResult = ";@override
  void visitMethodInvocation(MethodInvocation node) {
    super.visitMethodInvocation(node);

    // implementation}}Copy the code

Call the visitChildren method to access a CompilationUnit. If the visitor overrides any AST access methods, it will be called through visitChildren. Then you can analyze the code further.

To locate and initialize plug-ins, launch a Starter function and execute it in a specific directory –“… The tools/analyzer_plugin/bin/plugin. Dart “.

void start(可迭代<String> _, SendPort sendPort) {
  ServerPluginStarter(CustomPlugin(PhysicalResourceProvider.INSTANCE))
      .start(sendPort);
Copy the code

It can be a separate package or a subpackage, but according to the documentation, this is where the plug-in initializer must be located. The plug-in is easy to configure. The driver provides access to everything in “Analysis_options. yaml”. You can parse it and extract the data you need. Ideally, you want to parse the configuration file when you create the driver.

Here is an example of how we configured a plug-in (this is a custom plug-in) in the Dart code metrics project.

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
Copy the code

test

The interaction between the server and the plug-in is difficult to cover, but CompilationUnits cover the rest of the code. In testing, you can use familiar packages (such as Test, Mokito) and additional functions that help convert lines of code and content into an AST.

Here’s a simple test.

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,
    );
  });
}
Copy the code

debugging

It can be complex. There are three ways.

  1. Use a log. This may not be the most efficient approach, but journaling can be really useful. The logs help us understand why the plug-in did not process open files when editing in our project.

Logs can be messy and produce a lot of data output. However, they may help you find some errors.

  1. Refer to diagnostic procedures. Run the Analyzero Diagnostics command in VS Code to invoke the Diagnostics through Dart.

Here you can get server information, in-use plug-ins, errors, and more.

  1. Use Observatory and DartVM. I won’t go into this because there is a Dart React binding plugin. Its documentation extensively describes how to use Observatory for debugging.

Problems you may encounter

The lack of examples is a major problem when creating plug-ins. This makes it difficult to pinpoint the problem and find a solution on the spot. And the documentation is mostly code comments, so it can be quite complicated to use.

Without obvious explanation, you can’t just analyze the Dart code, but you can also implement drivers in other languages to do so. For example, there is a DartAngular-plugin to analyze HTML code.

Useful links

  • The file data
  • Dart Code Metrics – Our open source project for static analysis of Dart code. It allows us to collect code metrics, essentially an additional set of parser rules. This may be of interest to those who want to try their hand at writing static analysis tools, or who want to become more familiar with plug-ins.
  • Built value – an example of a plug-in with dartDriver.
  • DartAngular plugin – a plugin that features HTML analysis.
  • Over react-dart-react – react-dart-react – react-dart-react

www.deepl.com