Abstract:

This article explains how to implement a custom Dart syntax checking plug-in. When writing a dart class, if the class name contains ViewModel, you must add the HDW prefix. In vscode it looks like this:

Searching the Web for custom Dart syntax checks or custom Dart Lint will eventually lead to the document Customizing Static Analysis. This document describes the functions and usage of Dart Static Analysis.

If an incorrect variable name is used in the if statement, the following error message is displayed.

void main() { var count = 0; if (counts < 10) { count++; } print(count); } error • Undefined name 'counts'. • lib/main.dart:3:7 • undefined_identifierCopy the code

However, the so-called Customizing in the title of the article refers to Customizing the modification of configuration check rules, setting the check matching range of files in the project, and adjusting the check level of some rules (from Warning to error). To do this, first add the analysis_options.yaml file in the project directory:

include: Package: pedantic/analysis_options 1.8.0 comes with yaml

analyzer:
  exclude: # ignore the file configuration for detection
    - lib/client.dart
    - lib/server/*.g.dart
    - test/_data/**
  strong-mode: Set some rules to strict mode
    implicit-casts: false

linter:
  rules: # Enable or disable certain rules
    avoid_shadowing_type_parameters: false
    await_only_futures: true
Copy the code

But this kind of customization is not what we want. We wanted to be able to analyze the AST(Abstract syntax tree) of the current code and write custom rules that fit our own team’s internal syntax conventions or business conventions, rather than just syntax checks on the Dart website. Is it feasible? Yes, and this functionality is provided by Dart Static Analysis and can still be configured in analysis_options.yaml. However, for some reason, the official website does not provide relevant documentation and tutorials, and there are few articles that can be found on the Internet, and writing such custom rules will encounter many pitfalls in project creation and debugging, so I plan to use a complete example of custom rules to present the whole process.

The requirement for this example is that when you write a dart class, you must add the prefix HDW if the class name contains ViewModel. (Forget about the practical implications of this rule, consider it a strong constraint on business naming 😂)

// The user must add the prefix class ViewModel {}Copy the code

Introduction of Analyzer plugin

Customizing rules that conform to your own team’s internal syntax or business conventions can be done through the Analyzer Plugin.

These rules, written through the analyzer plugin, are used, checked, and represented in VSCode or AndroidStudio in exactly the same way as those provided by Dart Static analysis.

Before we look at what the Analyzer Plugin is, how does the Dart Static Analysis described in the first section of this article work? Why and how does the analysis_options.yaml file configuration in the project work?

The obvious answer is “What Dart SDK provides”. Open the DarT-Lang project on Github, and the relevant code is in the Analysis_Server, Analysis_CLI, Analysis_Plugin, etc folder under PKG. After we install Flutter and execute Flutter doctor, we will download the Dart SDK of the corresponding version. In the folder of Flutter /bin/cache/dart-sdk/bin, we can find the toolkit provided by Dart SDK. Dartanalyzer is a “service” (referred to here as a service rather than just a tool) that provides parsing and checking.

It can be executed directly from the command line to parse a specific file:

Dartanalyzer is more than just a command line tool, it can be thought of as a native server application. We can start a local dartanalyzer service by specifying a directory parameter, and then send the file we need to detect as a command to the service over a cross-process communication channel. The service returns the detected results in the agreed format (see Analysis_Server under Dart-lang for the communication format). When we open the Dart project using VSCode (or AndroidStudio), the Dart plug-in installed in the IDE automatically starts the Dart analyzer service corresponding to this project. Using VSCode as an example, enter the command >open analyzer diagnostics to open the web information panel corresponding to the current dartanalyzer service:

It is important to emphasize here that multiple Dart projects are opened with VSCDOE, and each Dart project corresponds to a different service (a different newly created Dart Analyzer process instance). Also, the Web Information panel is not turned on by default and will only be turned on by explicitly executing Open Analyzer Diagnostics. The information in this panel will be used later in our custom check rules.

When dartanalyzer is started, the analysis_options.yaml file in the project directory is looked for, and the contents of that file serve as the analyzer configuration information. For example, read the exclude field to filter files that do not need to be checked.

Those of you who are paying attention may have noticed the Plugins option in the information menu. That’s right, this is today’s main Analyzer Plugin. If you click on the Plugins option in the menu, you will notice that no Plugins have been loaded

What is the Analyzer Plugin? Here’s an overview:

  • First, an Analyzer Plugin is a separate Dart project, and anyone can create their own plugin project by creating pubspec.yaml to name the project.
  • Add some to the Analyzer Plugin projectSomething agreed uponCan be loaded by the dartanalyzer service.
  • The incoming dartanalyzer service is available in the analyzer plugin projectCompilation unitThen obtain the AST of the file code, so that you can write code for analysis.
  • Within the Analyzer Plugin project you can use the API provided by the Dart Analyzer SDK to analyze the results of your own analysis toConvention formatBack to the dartanalyzer service. Dartanalyzer service can prompt users with error, warning, how to modify in IDE (such as vscode), optimization suggestions, and more based on the returned content.

Custom plugin example

Dart-lang on Github has a detailed readme, including target package, host package, bootstrap package and plugin The package and description of the default unwritten loading rules make it difficult to successfully write a Demo all at once. It is recommended to look at my example steps first, and then go back to readme once they are up and running.

The first step is to create a project called test_Plugin

Name: test_plugin version: 0.0.1 environment: SDK: ">=2.7.0 <3.0.0" dependencies: analyzer_plugin: ^0.3.0 quick_log: any path: any dev_dependencies: test: anyCopy the code

The second step is to create a startup portal

Create two files in the current test_plugin project directory.

  • ./tools/analyzer_plugin/pubspec.yaml

    Name: test_plugin_bootstrap version: 0.0.1 environment: SDK: '>=2.7.0 <3.0.0' dependencies: test_plugin: path: / Users/David/Desktop/test_plugin # here must be an absolute path, this would explain whyCopy the code
  • ./tools/analyzer_plugin/bin/plugin.dart

    import 'dart:isolate';
    
    void main(List<String> args, SendPort sendPort) {
      print("start");
    }
    Copy the code

Now that we have a simple plugin, let’s create test_Project and let test_Project load the plugin.

name: test

version: 0.01.

environment:
  sdk: "> = 2.7.0 < 3.0.0"

dependencies:
  pedantic: ^ 1.9.2

dev_dependencies:
  test_plugin:
    path: ../test_plugin/


Copy the code

Performing pub upgrade does not load the plug-in into the dartanalyzer service; you also need to configure the analysis_options.yaml

include: package:pedantic/analysis_options.yaml

analyzer:
  plugins:
    - test_plugin
Copy the code

After adding test_plugin to the analysis_options.yaml plugins, a series of magical reactions happen:

  • First, every time analysis_options.yaml changes, VScode notifies the dartanalyzer service
  • The DartAnalyzer service found that a plug-in called test_Plugin is currently needed
  • Where can I find this test_plugin? That’s right, go to pubspec.lock of the current project and look for the address
  • Once the test_plugin directory is found, the current directory is checked for availability./tools/analyzer_plugin/pubspec.yamland./tools/analyzer_plugin/bin/plugin.dartThese two files (which is why the directory address and file name of the two files in step 1 cannot be changed arbitrarily). Once the existence of the file is confirmed, the analyzer_plugin directory will be placedcopyDart, which is why the dartAnalyzer service must be restarted for each subsequent change in plugin.dart, and why the path above must be an absolute path.
  • Dart the dartAnalyzer service will now start the main method in the current analyzer_plugin/bin/plugin.dart to load the plug-in

At this point we reopen the Web information panel:

As you can see, test_plugin has been successfully loaded into the dartanalyzer service launched by test_project. But the last line shows not running for unknown reason, because we didn’t write anything in main:

void main(List<String> args, SendPort sendPort) {
  print("start");
}
Copy the code

The sendPort corresponds to the current dartanalyzer service, and from there, we can write the concrete logic. Of course, we could write all the code in the same file as the main function, but since the analyzer_plugin content is copied directly into the cache. It is better to write all the logical code under the lib of test_Plugin. Add three files under lib:

! [12](/Users/david/Library/Application Support/typora-user-images/image-20210222154011599.png)

Start. dart, at its simplest, provides a global method to be called by main

Look at the details:

starter.dart

import 'dart:isolate';

import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_plugin/starter.dart';
import './mirror_plugin.dart';
import 'logger/log.dart';

void start(List<String> args, SendPort sendPort) {
  mirrorLog.info('-----------restarted-------------');
  ServerPluginStarter(MirrorPlugin(PhysicalResourceProvider.INSTANCE))
      .start(sendPort);
}

Copy the code

MirrorPlugin is a plug-in class that inherits the ServerPlugin customization and is loaded through the ServerPluginStarter. The code is in mirror_plugin.dart:


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

  static const excludedFolders = ['.dart_tool/**'];

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

  @override
  String get name => 'Mirror Plugin';

  @override
  String get version => '0.0.6';

  @override
  bool isCompatibleWith(Version serverVersion) => true;

  @override
  void contentChanged(String path) {
    // this is triggered every time a file is modified in vscode
    mirrorLog.info("contentChanged$path");
    AnalysisDriverGeneric driver = super.driverForPath(path);
    driver.addFile(path);
  }

  @override
  AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
    // called when the plug-in is loaded
    finalanalysisRoot = analyzer.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(analysisRoot);
    runZonedGuarded(() {
      // If no file changes or new files are created, listen is triggered
      dartDriver.results.listen((analysisResult) {
        _processResult(dartDriver, analysisResult);
      });
    }, (e, stackTrace) {
      channel.sendNotification(
          plugin.PluginErrorParams(false, e.toString(), stackTrace.toString())
              .toNotification());
    });
    return dartDriver;
  }

  void _processResult(
      AnalysisDriver driver, ResolvedUnitResult analysisResult) {
    Resolved Unit. This means we can get the element directly from the AST. (PS: If this is difficult to understand, you can first look at the documentation for the Dart syntax tree.)
    try {
      if(analysisResult.unit ! =null&& analysisResult.libraryElement ! =null) {
        // Throw the compilation unit to the custom MirrorChecker processing, which will be analyzed below
        final mirrorChecker = MirrorChecker(analysisResult.unit);
        // Get the analysis result
        final issues = mirrorChecker.enumToStringErrors();
        mirrorLog.info("MirrorCheckerissues: $issues");
        if (issues.isNotEmpty) {
          channel.sendNotification(
            // send the result back to the dartanalyzer service and vscode is automatically displayed in the editor
            plugin.AnalysisErrorsParams(
              analysisResult.path,
              issues
                  .map((issue) => analysisErrorFor(
                      analysisResult.path, issue, analysisResult.unit))
                  .toList(),
            ).toNotification(),
          );
        } else {
          // Returns an empty resultchannel.sendNotification( plugin.AnalysisErrorsParams(analysisResult.path, []) .toNotification()); }}else {
        // Returns an empty resultchannel.sendNotification( plugin.AnalysisErrorsParams(analysisResult.path, []) .toNotification()); }}on Exception catch (e, stackTrace) {
       // Returns an empty result
      channel.sendNotification(
          plugin.PluginErrorParams(false, e.toString(), stackTrace.toString()) .toNotification()); }}}Copy the code

The last and most critical file, mirror_visitor.dart


class MirrorChecker {
  final CompilationUnit _compilationUnit;
  String unitPath;

  MirrorChecker(this._compilationUnit) {
    unitPath = this._compilationUnit.declaredElement.source.fullName;
    mirrorLog.info("checker $unitPath");
  }

  可迭代<MirrorCheckerIssue> enumToStringErrors() {
    final visitor = _MirrorVisitor();
    visitor.unitPath = unitPath;
    _compilationUnit.accept(visitor);
    returnvisitor.issues; }}Create a syntax tree Visitor that integrates with RecursiveAstVisitor
class _MirrorVisitor extends RecursiveAstVisitor<void> {
  String unitPath;
  final _issues = <MirrorCheckerIssue>[];

  可迭代<MirrorCheckerIssue> get issues => _issues;

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    // In this example, just check the ClassDeclaration syntax node
    node.visitChildren(this);
    if (node.declaredElement.displayName.contains('ViewModle') &&
        !node.declaredElement.displayName.startsWith('HDW'{)),// Add an error report if the current class is ViewModle but no HDW business prefix is added
      _issues.add(
        MirrorCheckerIssue(
          plugin.AnalysisErrorSeverity.ERROR,
          plugin.AnalysisErrorType.LINT,
          node.offset,
          node.length,
          'Your model class is not prefixed with HDW'.'can be HDW${node.declaredElement.displayName}',),); }}}Copy the code

Ok, so far, all the key code has been written. Go back to the test_project project and run >restart analysis server to restart the dartanalyzer service for the current project.

Plugin_manager/XXXXX/the following cache is removed:

Effects in VS:

Isn’t it cool 😎

Does it feel like something’s wrong? Yeah, you can’t write that much code all at once. We usually write a little bit and debug a little bit. Can the plugin be debugged and run? I regret! Unable to complete debug run!

But never mind!

If you want to develop the plugin yourself, first use the code in this sample demo, except for MirrorVisitor. The main customization logic is in the MirrorVisitor. I’ve written a test case where you can click to break and debug your own MirrorVisitor

After the key code is written, we always have to check in the project, then how to do? Using mirrorLog. Info (” XXXXXX “);

This is actually a way of not doing it, which is actually writing to a file

  • Dart to the last line of logger/log.dart and adjust the comment.

  • Restart the dartanalyzer service and you will have an output.log file on your desktop

  • Use mirrorlog.info (” XXXXXX “) near code that you feel might have a problem; Write some logs

  • The output.log information is continuously added during the plug-in running. You can run the tail -f ~/Desktop/output.log command on the terminal to view the logs in real time.

Test_plugin can be published to Pubsepc so that the same custom rules can be shared across groups.

All the code in the article is uploaded here, like it give a thumbs-up!

Reference documentation

  • The dart. Cn/guides/lang…
  • The dart – lang. Making. IO/linter/lint…
  • Github.com/dart-lang/s…