Flutter自定义语法检查器

本文将讲解如何实现一个自定义的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的提示。

源码地址:阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WxSkyqi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值