发布时间:2021年3月2日 - 5分钟阅读
嗨!我叫Dmitry,是Wrike的一名前端开发人员。在这篇文章中,我会告诉你如何开发一个自定义的Dart代码分析器插件。对于那些觉得基本的Dart分析器在静态分析方面缺乏功能的人,以及那些想尝试自己开发一个简单的分析器的人来说,这篇文章会很方便。
插件是一个可执行的代码,它与分析服务器进行通信,并对Dart代码进行额外的分析。插件与服务器在同一个虚拟机中执行,但在一个单独的隔离区。服务器负责与现有IDE的集成,让你自由地专注于插件的开发过程。
服务器提供数据。AnalysisDriver,它收集分析文件的信息,然后将数据转换为AST。然后,插件被提供AST来工作--从突出代码错误到统计汇总。
插件功能和创建步骤
一个插件可以突出显示错误(并显示纠正错误的方法)、语法和导航,并执行自动完成。选择一个代码块,你可以添加辅助,它可以把它包装成某样东西或适当的格式。
更多内容请参考API规范插件。
下面简单列举一下创建插件的步骤。
- 为分析器和插件创建一个依赖包。
- 创建一个继承自ServerPlugin的类。
- 从服务器中实现基本的getters和init方法。
- 添加一个启动函数。
- 在...tools/analyzer_plugin/中创建一个单独的子包。
- 在包上设置依赖关系,将插件与客户端连接起来。
- 在插件块中添加插件名称为analysis_options。
插件的实现
下面是一个简单的插件的样子。
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;
}
}
我们有三个获取器 -- name, version, 和 fileGlobsToAnalyze -- 其中:
- Version是插件所使用的服务器API的版本,它必须与现有的版本相匹配(例如,1.0.0-alpha.0)。
- fileGlobsToAnalyze是一个glob模式,显示了这个插件有兴趣分析的文件。
方法初始化应该有一个dartDriver在里面。
@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
final root = 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;
}
这是唯一的驱动,默认情况下,它是分析器的一个包。然而,API允许你为任何语言创建一个驱动程序。唯一的缺点是需要花费大量的精力,比如驱动和类的初始化、配置等。
在创建驱动时,你需要订阅分析结果。驱动程序将推送事件与结果。
结构会类似于这样。
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;
}
根据我的经验,编译单元对代码分析很有用。它使用起来也很方便,因为它包含了DART文件的AST结构。这个代码片段还包含了库、typeSystem等附加信息。
下面是可以解析AST结构的访问者模式。
- RecursiveAstVisitor
- GeneralizingAstVisitor
- SimpleAstVisitor
- ThrowingAstVisitor
- TimedAstVisitor
- UnifyingAstVisitor
RecursiveAstVisitor 递归地访问所有AST节点。例如,它将访问[Block]节点和它的所有子节点。
GeneralizingAstVisitor,类似于RecursiveAstVisitor,递归地访问所有AST节点。然而,当它访问一个特定的节点时,它将调用特定于该特定类型节点的访问方法,以及节点的超类的附加方法。
SimpleAstVisitor访问所有AST节点,不做任何事情。这适用于不需要递归访问者的情况。
ThrowingAstVisitor会在任何被调用的访问方法没有被重写的情况下抛出异常。
TimedAstVisitor测量访问调用时机。
UnifyingAstVisitor递归地访问所有的AST节点(类似于RecursiveAstVisitor),但每个节点也会通过使用统一的visitNode方法进行访问。
下面是递归访问者的简单实现。
String checkCompilationUnit(CompilationUnit unit) {
final visitor = _Visitor();
unit.visitChildren(visitor);
return visitor.result;
}
class _Visitor extends RecursiveAstVisitor<void> {
String result = ‘’;
@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);
// implementation
}
}
调用visitChildren方法来访问一个CompilationUnit。如果访问者覆盖任何AST访问方法,它将通过visitChildren调用。然后你可以进一步分析代码。
要定位和初始化插件,发起一个Starter函数,并在特定的目录下执行它--"...tools/analyzer_plugin/bin/plugin.dart"。
void start(Iterable<String> _, SendPort sendPort) {
ServerPluginStarter(CustomPlugin(PhysicalResourceProvider.INSTANCE))
.start(sendPort);
它可以是一个单独的包,也可以是一个子包,但根据文档,这正是插件初始化器必须位于的地方。 该插件很容易配置。驱动程序提供了对 "analysis_options.yaml "中所有内容的访问。你可以对其进行解析,并提取你需要的数据。理想情况下,你希望在创建驱动程序时解析配置文件。
下面是我们在Dart代码度量项目中配置插件的一个例子(这是一个自定义插件)。
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
测试
服务器和插件之间的交互很难覆盖,但CompilationUnits覆盖了其余的代码就可以了。在测试中,你可以使用熟悉的包(如test、mokito)和附加函数,它们有助于将代码行和内容转换为AST。
下面是一个简单的测试。
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,
);
});
}
调试
它可以是复杂的。有三种方法。
- 使用日志。这可能不是最有效的方法,但日志真的很有用。日志帮助我们了解为什么插件在我们的项目中编辑时没有处理打开的文件。
日志可能会杂乱无章,并产生大量的数据输出。但是,它们可能会帮助你发现一些错误。
- 参考诊断程序。在VS Code中运行Analyzero Diagnostics命令,通过Dart调用诊断程序。
在这里你可以得到服务器信息、使用中的插件、错误等有用信息。
- 使用Observatory和DartVM。这个我就不多说了,因为有一个Dart React绑定插件。它的文档广泛介绍了如何使用Observatory进行调试。
你可能遇到的问题
缺乏例子是创建插件时的主要问题。这让人很难准确地找出问题所在,当场找到解决方案。而且文档大多是代码注释,所以使用起来也相当复杂。
没有明显的说明,不能只分析Dart代码,也可以实现其他语言的驱动来分析。例如,有一个DartAngular-plugin来分析HTML代码。
有用的链接
- 文件资料
- Dart代码度量 - 我们的Dart代码静态分析开源项目。它允许我们收集代码指标,本质上是一套额外的分析器规则。对于那些想尝试编写静态分析工具,或者想更熟悉插件的人来说,可能会有兴趣。
- Built value - 一个带有dartDriver的插件的例子。
- DartAngular插件--一个以HTML分析为特色的插件。
- Over react - Dart-React绑定的插件,有有用的调试例子。