遍历语法树AstNode
上节中,使用日志输出的方式将analysisResult的类型给打印了出来:
runZonedGuarded(() {
dartDriver.results.listen((analysisResult) {
pluginLogger.info('${analysisResult.runtimeType}');
});
}, (error, trace) {
//如果插件运行出错了
channel.sendNotification(PluginErrorParams(true, error.toString(), trace.toString()).toNotification());
}
);
可以看到它的类型是ResolvedUnitResultImpl,下面就是要通过这个ResolvedUnitResultImpl来进行语法的遍历,进而编写自定义的Lint规则。
首先我们是无法直接使用ResolvedUnitResultImpl的,但它是ResolvedUnitResult的实现类,ResolvedUnitResult是可以引入使用的,然后ResolvedUnitResult实现了FileResult。下面列举下ResolvedUnitResult中的常用字段以及含义:
- path
文件路径,表示被扫描分析的文件的地址。 - content
被扫描的文件的内容。 - exists
文件是否存在 - unit
这个文件被解析成的抽象语法树 - lineInfo
记录每行信息的,常用于框定lint提醒的范围(黄色提醒或红色报错的线从哪开始到哪结束)
要想实现自定义lint规则,就需要去遍历语法树,使用自定的规则去看,语法树中的结点是否满足。通过api,可以发现unit可以通过访问者模式,使用unit.accept(visitor)方法来进行遍历。好在analyzer的api中已经提供了几种常用的访问者模式的实现类,基本可以直接拿来使用:
- GeneralizingAstVisitor
- RecursiveAstVisitor
- UnifyingAstVisitor
- SimpleAstVisitor
直接在plugin的代码中进行修改然后在另一个项目中测试,实在是不太方便,所以这里推荐一种方式,可以方便测试和验证对AstNode的遍历。
在一个普通的dart项目中,编写下面代码:
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
void main() {
String s = 'int a = 1; int b = a + 2;';
ParseStringResult result = parseString(content: s);
CompilationUnit unit = result.unit;
unit.accept(MyVisitor());
}
这段代码中,使用parseString可以讲一段代码解析成AstNode,然后就能使用Visitor进行遍历,因此后面对于Visitor的讲解,都基于上面的代码。
这里直接先使用GeneralizingAstVisitor来进行遍历:
class MyVisitor extends GeneralizingAstVisitor {
@override
visitNode(AstNode node) {
//计算缩进,美化输出
int l = 0;
AstNode? p = node;
while (p != null && p.parent != null) {
l++;
p = p.parent;
}
String b = '';
for (int i = 0; i < l; i++) {
b += ' ';
}
print('${node.runtimeType}->${node.toSource()}');
return super.visitNode(node);
}
}
然后运行代码,输出如下:
CompilationUnitImpl ------ int a = 1; int b = a + 2;
TopLevelVariableDeclarationImpl ------ int a = 1;
VariableDeclarationListImpl ------ int a = 1
NamedTypeImpl ------ int
SimpleIdentifierImpl ------ int
VariableDeclarationImpl ------ a = 1
DeclaredSimpleIdentifier ------ a
IntegerLiteralImpl ------ 1
TopLevelVariableDeclarationImpl ------ int b = a + 2;
VariableDeclarationListImpl ------ int b = a + 2
NamedTypeImpl ------ int
SimpleIdentifierImpl ------ int
VariableDeclarationImpl ------ b = a + 2
DeclaredSimpleIdentifier ------ b
BinaryExpressionImpl ------ a + 2
SimpleIdentifierImpl ------ a
IntegerLiteralImpl ------ 2
可以明显看到语法树的结构,lint规则就是在遍历这些树的节点时看符不符合要求。
实现禁止以‘xx’开头定义变量名的规则
能够遍历语法树后,就能够实现自己的lint规则了,这里简单以禁止取xx为前缀的变量名为目标规则,说下实现的方法。
首先找到需要进行校验的语法树节点。从上面的例子中可以看到,有一个类型为VariableDeclarationImpl的节点,这个名字一看就是变量定义的,它还有一个子节点DeclaredSimpleIdentifier,里面存储的就是对应的定义的变量名,那么DeclaredSimpleIdentifier就是目标节点。但要注意DeclaredSimpleIdentifier可能无法被IDE提示如何引入,需要手动写import语句
import 'package:analyzer/src/dart/ast/ast.dart';
确认了节点后,就在MyVisitor中拦截该节点,实现对名称的验证
@override
visitSimpleIdentifier(SimpleIdentifier node) {
if (node is DeclaredSimpleIdentifier) {
String name = node.name;
if (name.startsWith('xx')) {
//TODO
}
}
return super.visitSimpleIdentifier(node);
}
发现名称违反规则了,则将该违反规则的节点的位置在这个Visitor中记录下来:
class MyVisitor extends GeneralizingAstVisitor {
late List<Pair<int, int>> issues = [];
@override
visitSimpleIdentifier(SimpleIdentifier node) {
if (node is DeclaredSimpleIdentifier) {
String name = node.name;
if (name.startsWith('xx')) {
issues.add(Pair(node.offset, node.length));
}
}
return super.visitSimpleIdentifier(node);
}
}
这样扫描完后,issues中就会记录所有的违规的位置。这里需要注意,一般来说Visitor不要共用,一个文件使用一个Visitor实例,因为记录的违规的位置等信息,都是针对于某个文件而言的。如果混在一起,那么如果没有同时记录文件,那么就无法获取具体的位置了。
然后回到最初的MyPlugin中,接入这个MyVisitor:
dartDriver.results.listen((analysisResult) {
//每次有文件改动或新建文件,就会触发这里
if (analysisResult is ResolvedUnitResult) {
var visitor = MyVisitor();
analysisResult.unit.accept(visitor);
//TODO 处理visitor.issues
}
});
最后就是将issues转化成语法分析结果就行了。
-
将每个issue转成
AnalysisErrorint offset = issue.first; int length = issue.last; var locationInfo = analysisResult.unit.lineInfo.getLocation(offset); var error = AnalysisError( AnalysisErrorSeverity.ERROR, //提示级别(INFO、WARNING、ERROR) AnalysisErrorType.LINT, Location(analysisResult.path, offset, length, locationInfo.lineNumber, locationInfo.columnNumber), '禁止以xx为前缀命名变量', //提示信息 '变量命名规则' //提示错误名 ); -
将List<AnalysisError> errors发送出去
channel.sendNotification( AnalysisErrorsParams(analysisResult.path, errors).toNotification());注意channel是在
MyPlugin中的。
最后将MyPlugin插件放到测试项目中,看下IDE中的分析结果:
大功告成!
PS. 为啥SimpleAstVisitor不能直接使用
首先介绍SimpleAstVisitor。这个Visitor是无法直接使用的,可以看到它内部的代码实现:
class SimpleAstVisitor<R> implements AstVisitor<R> {
/// Initialize a newly created visitor.
const SimpleAstVisitor();
@override
R? visitAdjacentStrings(AdjacentStrings node) => null;
@override
R? visitAnnotation(Annotation node) => null;
@override
R? visitArgumentList(ArgumentList node) => null;
...
...
}
全部都是直接return null,那为啥return null了就不能使用了?因为使用Visitor访问者模式进行遍历,步骤分为两部:1. 访问自身节点;2. 访问该节点的子节点。访问自身节点,那么使用某个节点的node.accpet(visitor),访问节点的子节点,需要使用node.visitChildren(visitor)。而SimpleAstVisitor的实现中,所有的方法都只是return null,所以并不会去遍历任何节点。其他的Visitor实现类GeneralizingAstVisitor、RecursiveAstVisitor、UnifyingAstVisitor都有主动调用node.visitChildren(visitor)。
完整MyPluign代码
import 'dart:async';
import 'package:analyzer/dart/analysis/context_builder.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer_plugin/utilities/pair.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
class MyPlugin extends ServerPlugin {
MyPlugin(ResourceProvider provider) : super(provider);
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
@override
String get name => 'My fantastic plugin';
@override
String get version => '1.0.1';
@override
AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot) {
final rootPath = contextRoot.root;
final locator =
ContextLocator(resourceProvider: resourceProvider).locateRoots(
includedPaths: [rootPath],
excludedPaths: contextRoot.exclude,
optionsFile: contextRoot.optionsFile,
);
final builder = ContextBuilder(resourceProvider: resourceProvider);
final context = builder.createContext(contextRoot: locator.first)
as DriverBasedAnalysisContext;
final dartDriver = context.driver;
//启动沙盒去监听处理文件改动
runZonedGuarded(() {
dartDriver.results.listen((analysisResult) {
//每次有文件改动或新建文件,就会触发这里
if (analysisResult is ResolvedUnitResult) {
var visitor = MyVisitor();
analysisResult.unit.accept(visitor);
if (visitor.issues.isNotEmpty) {
var errors = visitor.issues.map((issue) {
int offset = issue.first;
int length = issue.last;
var locationInfo =
analysisResult.unit.lineInfo.getLocation(offset);
return AnalysisError(
AnalysisErrorSeverity.ERROR, //提示级别(INFO、WARNING、ERROR)
AnalysisErrorType.LINT,
Location(analysisResult.path, offset, length,
locationInfo.lineNumber, locationInfo.columnNumber),
'禁止以xx为前缀命名变量', //提示信息
'变量命名规则' //提示错误名
);
}).toList();
channel.sendNotification(
AnalysisErrorsParams(analysisResult.path, errors)
.toNotification());
}
}
});
}, (error, trace) {
//如果插件运行出错了
channel.sendNotification(
PluginErrorParams(true, error.toString(), trace.toString())
.toNotification());
});
return dartDriver;
}
}
class MyVisitor extends GeneralizingAstVisitor {
late List<Pair<int, int>> issues = [];
@override
visitSimpleIdentifier(SimpleIdentifier node) {
if (node is DeclaredSimpleIdentifier) {
String name = node.name;
if (name.startsWith('xx')) {
issues.add(Pair(node.offset, node.length));
}
}
return super.visitSimpleIdentifier(node);
}
}