Dart-自定义Lint之路(三)-实现简单的Lint规则

861 阅读4分钟
  1. # Dart-自定义Lint之路(一)-创建Analyzer Plugin
  2. # Dart-自定义Lint之路(二)-运行和调试Analyzer Plugin

遍历语法树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转化成语法分析结果就行了。

  1. 将每个issue转成AnalysisError

    int 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为前缀命名变量', //提示信息
        '变量命名规则' //提示错误名
        );
    
  2. 将List<AnalysisError> errors发送出去

    channel.sendNotification(
           AnalysisErrorsParams(analysisResult.path, errors).toNotification());
    

    注意channel是在MyPlugin中的。

最后将MyPlugin插件放到测试项目中,看下IDE中的分析结果:

image.png

大功告成!

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实现类GeneralizingAstVisitorRecursiveAstVisitorUnifyingAstVisitor都有主动调用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);
  }
}