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 project
Something agreed upon
Can be loaded by the dartanalyzer service. - The incoming dartanalyzer service is available in the analyzer plugin project
Compilation unit
Then 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 to
Convention format
Back 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.yaml
and./tools/analyzer_plugin/bin/plugin.dart
These 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…