本文将讲解如何实现一个自定义的Dart语法检查器。
官方链接 Customizing static analysis指的是修改工程中的检查规则、修改工程检查文件的匹配范围、调整某些规则的检查级别等。具体方法是通过analysis_options.yaml文件,修改analyzer和linter来修改官方提供的检查器设置。
include: package:lints/recommended.yaml
analyzer:
exclude: [build/**]
language:
strict-casts: true
strict-raw-types: true
linter:
rules:
- cancel_subscriptions
如果需要根据自身业务或者设计规范,来自定义规则时,这种方法就不行了。这时候就需要自定义Analysis Plugin来支持自定义的需求(例如继承BaseRepository的class,必须要已Repository作为结尾)。
Analysis Plugin是什么?
通过vscode或者Android Studio打开dart相关项目,可以看见进程中一直存在dart:analysis_server.dart.snapshot的进程,这就是与plugin交互的analysis server。具体的通信协议暂不解释,看dart-sdk相关源码即可。
通过查看dart sdk的源码,可以发现在pkg/analysis_server_client包下,server.dart存在启动该进程的逻辑。实际上Android studio上Dart Analysis栏的信息,就是通过与该server进行通信来实现。(每个单独的dart项目,会打开analysis server,server对于不同的项目不会共享)
class Server extends ServerBase {
...
if (_process != null) {
throw Exception('Process already started');
}
dartBinary ??= Platform.executable;
// The integration tests run 3x faster when run from snapshots
// (you need to run test.py with --use-sdk).
if (serverPath == null) {
// Look for snapshots/analysis_server.dart.snapshot.
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
'snapshots', 'analysis_server.dart.snapshot'));
if (!FileSystemEntity.isFileSync(serverPath)) {
// Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot.
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot'));
}
}
var arguments = <String>[];
//
// Add VM arguments.
//
if (profileServer) {
if (servicePort == null) {
arguments.add('--observe');
} else {
arguments.add('--observe=$servicePort');
}
arguments.add('--pause-isolates-on-exit');
} else if (servicePort != null) {
arguments.add('--enable-vm-service=$servicePort');
}
if (Platform.packageConfig != null) {
arguments.add('--packages=${Platform.packageConfig}');
}
if (enableAsserts) {
arguments.add('--enable-asserts');
}
//
// Add the server executable.
//
arguments.add(serverPath);
arguments.addAll(getServerArguments(
clientId: clientId,
clientVersion: clientVersion,
suppressAnalytics: suppressAnalytics,
diagnosticPort: diagnosticPort,
instrumentationLogFile: instrumentationLogFile,
sdkPath: sdkPath,
useAnalysisHighlight2: useAnalysisHighlight2));
listener?.startingServer(dartBinary, arguments);
final process = await Process.start(dartBinary, arguments);
_process = process;
// ignore: unawaited_futures
process.exitCode.then((int code) {
if (code != 0 && _process != null) {
// Report an error if server abruptly terminated
listener?.unexpectedStop(code);
}
});
...
}
那么如何看dart项目的server加载了什么analysis plugin,以Android Studio为例子,找到dart analysis栏,点击View analyzer diagnostics,就可以打开web的管理页面。
可以看见有个plugins的选择菜单,选中后可以看见server目前已经加载的analysis plugin。
自定义的plugins流程
dart sdk版本:2.19.6
analyzer.plugin版本:5.10.0
由于sdk版本导致流程变化,老版本流程请参考:自定义Flutter Lint插件实现自己的Dart语法规则 - 掘金
在一个dart/flutter项目下,新建tools/analyzer_plugin路径。bin文件夹与plugin.dart为必须,且plugin.dart的方法结构必须固定,否则可以看见web页面上输出错误。然后pubspec.yaml依赖analyzer_plugin。
为什么要定义这个结构?(其实我也不知道,但是可以根据主项目依赖后,在/Users/xxx/.plugin_manager目录下,可以看见会将tools下的内容复制过去,所以先定义成这样,原因后续分析)
name: specification_plugin
version: 0.0.1
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
analyzer_plugin: any
plugin.dart的main将作为插件入口,当server启动时,将自动查找.plugin_manager下的缓存内容并运行。此处的main与dart的主方法main有不同,可以看见多了一个sendPort参数,可以得出server与该插件进行通信,肯定是隔离了Isolate。
import 'dart:isolate';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_plugin/src/driver.dart';
void main(List<String> args, SendPort sendPort) {
Driver(SpecificationPlugin(
resourceProvider: PhysicalResourceProvider.INSTANCE))
.start(sendPort);
}
为了论证上面的观点,来看下Driver实现方法。可以看见start的方法实际上是使用ServerPlugin的start方法。而Serverplugin中则由PluginIsolateChannel进行监听。PluginIsolateChannel最终根据传过来的SendPort建立了一个新的Isolate,并与server的Isolate进行通信。
class Driver implements ServerPluginStarter {
/// The plugin that will be started.
final ServerPlugin plugin;
/// Initialize a newly created driver that can be used to start the given
/// plugin.
Driver(this.plugin);
@override
void start(SendPort sendPort) {
var channel = PluginIsolateChannel(sendPort);
plugin.start(channel);
}
}
/// Start this plugin by listening to the given communication [channel].
void start(PluginCommunicationChannel channel) {
_channel = channel;
_channel.listen(_onRequest, onError: onError, onDone: onDone);
/// The port used to send notifications and responses to the server.
final SendPort _sendPort;
/// The port used to receive requests from the server.
late final ReceivePort _receivePort;
/// The subscription that needs to be cancelled when the channel is closed.
StreamSubscription? _subscription;
/// Initialize a newly created channel to communicate with the server.
PluginIsolateChannel(this._sendPort) {
_receivePort = ReceivePort();
_sendPort.send(_receivePort.sendPort);
}
@override
void listen(void Function(Request request) onRequest,
{Function? onError, void Function()? onDone}) {
void onData(data) {
var requestMap = data as Map<String, Object>;
var request = Request.fromJson(requestMap);
onRequest(request);
}
if (_subscription != null) {
throw StateError('Only one listener is allowed per channel');
}
_subscription = _receivePort.listen(onData,
onError: onError, onDone: onDone, cancelOnError: false);
}
然后开始可以传入的SpecificationPlugin,该类需要自定义且继承ServerPlugin。ServerPlugin有一个必传参数resourceProvider,这个参数有3个具体的实现子类:
1.PhysicalResourceProvider
支持物理文件进行添加、修改、删除的监控功能。
2.MemoryResourceProvider
支持内存文件的改动监控功能。
3.OverlayResourceProvider
支持替换文件的改动监控功能。
一般情况来说,只需要使用PhysicalResourceProvider 即可。
SPecificationPlugin继承ServerPlugin后,则需要强制实现analyzeFile、fileGlobsToAnalyze、name、version、isCompatibleWith这5个方法和属性,现在开始通过这几个可继承的来理解ServerPlugin是如何处理的。
class SpecificationPlugin extends ServerPlugin {
SpecificationPlugin({required super.resourceProvider});
///具体实现的方法
@override
Future<void> analyzeFile(
{required AnalysisContext analysisContext, required String path}) async {
///
}
///想要分析的文件
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
///插件名称
@override
String get name => 'specification';
///插件版本
@override
String get version => "0.0.1";
@override
bool isCompatibleWith(Version serverVersion) => true;
}
首先根据ServerPlugin.start()方法,可以得知具体server发给插件的处理都在_onRequest方法中。_onRequest将server的传给_getResponse,如果_getResponse有结果,则需要根据requestId生成对应的Response。
由于server发的事件比较多,所以暂时先看ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS与PLUGIN_REQUEST_VERSION_CHECK事件的处理。
Future<void> _onRequest(Request request) async {
var requestTime = DateTime.now().millisecondsSinceEpoch;
var id = request.id;
Response? response;
try {
response = await _getResponse(request, requestTime);
} on RequestFailure catch (exception) {
response = Response(id, requestTime, error: exception.error);
} catch (exception, stackTrace) {
response = Response(id, requestTime,
error: RequestError(
RequestErrorCode.PLUGIN_ERROR, exception.toString(),
stackTrace: stackTrace.toString()));
}
if (response != null) {
_channel.sendResponse(response);
}
}
Future<Response?> _getResponse(Request request, int requestTime) async {
ResponseResult? result;
switch (request.method) {
...
case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS:
var params = AnalysisHandleWatchEventsParams.fromRequest(request);
result = await handleAnalysisHandleWatchEvents(params);
break;
...
case PLUGIN_REQUEST_VERSION_CHECK:
var params = PluginVersionCheckParams.fromRequest(request);
result = await handlePluginVersionCheck(params);
break;
}
if (result == null) {
return Response(request.id, requestTime,
error: RequestErrorFactory.unknownRequest(request.method));
}
return result.toResponse(request.id, requestTime);
}
PLUGIN_REQUEST_VERSION_CHECK:
可以看见handlePluginVersionCheck返回很简单,就是封装了一个Response的参数,其中包含了name、version、fileGlobsToAnalyze、isCompatibleWith 4个继承的属性。name和version分别表示了插件的名称和版本,isCompatibleWith则判断server是否可以兼容老版本的plugin,如果不兼容,则老版本plugin连接上后,将会自动断开连接,fileGlobsToAnalyze则代表了plugin需要解析的文件类型,如果是dart文件,则需要设置为"**/*.dart"表示所有路径下的dart文件。
/// Handle a 'plugin.versionCheck' request.
///
/// Throw a [RequestFailure] if the request could not be handled.
Future<PluginVersionCheckResult> handlePluginVersionCheck(
PluginVersionCheckParams parameters) async {
_sdkPath = parameters.sdkPath;
var versionString = parameters.version;
var serverVersion = Version.parse(versionString);
return PluginVersionCheckResult(
isCompatibleWith(serverVersion), name, version, fileGlobsToAnalyze,
contactInfo: contactInfo);
}
/// Return `true` if this plugin is compatible with an analysis server that is
/// using the given version of the plugin API.
bool isCompatibleWith(Version serverVersion) =>
serverVersion <= Version.parse(version);
/// Return a list of glob patterns selecting the files that this plugin is
/// interested in analyzing.
List<String> get fileGlobsToAnalyze;
ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS:
通过handleAnalysisHandleWatchEvents可以判断出,如果是文件内容出现了修改,则调用链为contentChanged -> handleAffectedFiles -> analyzeFiles。最终在analyzeFiles中,发现在analyzeFile被传入的是修改的文件路径,以及上下文。analyzeFile正是必须实现的方法。
Future<AnalysisHandleWatchEventsResult> handleAnalysisHandleWatchEvents(
AnalysisHandleWatchEventsParams parameters) async {
for (var event in parameters.events) {
switch (event.type) {
case WatchEventType.ADD:
// TODO(brianwilkerson) Handle the event.
break;
case WatchEventType.MODIFY:
await contentChanged([event.path]);
break;
case WatchEventType.REMOVE:
// TODO(brianwilkerson) Handle the event.
break;
default:
// Ignore unhandled watch event types.
break;
}
}
return AnalysisHandleWatchEventsResult();
}
Future<void> contentChanged(List<String> paths) async {
final contextCollection = _contextCollection;
if (contextCollection != null) {
await _forAnalysisContexts(contextCollection, (analysisContext) async {
...
...
await handleAffectedFiles(
analysisContext: analysisContext,
paths: affected,
);
});
}
}
Future<void> handleAffectedFiles({
required AnalysisContext analysisContext,
required List<String> paths,
}) async {
final analyzedPaths = paths
.where(analysisContext.contextRoot.isAnalyzed)
.toList(growable: false);
await analyzeFiles(
analysisContext: analysisContext,
paths: analyzedPaths,
);
}
Future<void> analyzeFiles({
required AnalysisContext analysisContext,
required List<String> paths,
}) async {
final pathSet = paths.toSet();
// First analyze priority files.
for (final path in priorityPaths) {
pathSet.remove(path);
await analyzeFile(
analysisContext: analysisContext,
path: path,
);
}
// Then analyze the remaining files.
for (final path in pathSet) {
await analyzeFile(
analysisContext: analysisContext,
path: path,
);
}
}
analyzeFile如果要使用ast功能,则需要通过上下文,获取到指定文件的编译单元,然后通过编译单元进行解析。解析出的结果,可以plugin自带的channel发送notification来通知server。具体Ast解析与判断过程就不展示了。
如果要显示语法错误的提示,则需要生成AnalysisError实例。
AnalysisError的属性正好dart analysis栏的提示:
AnalysisErrorSeverity severity:提示错误等级,INFO、WARNING、ERROR
AnalysisErrorType type:什么时机触发的错误
Location location:发生错误的具体文件定位点
String message:提示什么错误信息
String correction:如何修复此类错误
String code:该错误的编号
以上如果配置没有错误,则需要在你需要的项目中引入该插件的依赖。(注意:是引入该插件的根路径,不需要引入到tools路径下)
dev_dependencies:
improve_kit:
path: ../../app_debugger
此时在重新点击按钮重启server,则将变动进行刷新。如果你的处理逻辑都在tools下,那么你每次改动,必须要要将.dart-server/.plugin_manager下的缓存删除,才能生效,因为上面说过,每次server启动时,会复制tools到该路径下缓存,不删除该缓存,即使重启了server也是读取的原来的副本。
如果成功的话,就能看见web页面上显示Performance。如果出现什么错误的话,只会出现Error或者not running的提示。