美文网首页
AST从了解到自定义sonar代码规则

AST从了解到自定义sonar代码规则

作者: 向心力 | 来源:发表于2023-05-17 23:10 被阅读0次

    AST是什么?

    抽象语法树(Abstract Syntax Tree)简称AST,它是源代码语法结构的抽象表示,以树状形式展示编程语言的语法结构,树的不同节点对应源代码的对应部分。

    不同的编程语言生成的AST不尽相同,相同的语言若是不同的解析工具,生成的AST也是不尽相同的,有些工具生成的AST节点会多出一些属性。为了统一,文章中举的例子统一是基于java语言的AST,但基本上每种编程语言都有类似的。

    AST在javac编译过程中是怎么产生的?

    Java源码的编译过程可以概括性的总结为以下几个步骤:

    javac-flow.png

    Parse and Enter

    将.java文件解析成语法树,并将相关定义记录到编译器符号表

    -93b6eda779d4.png

    词法分析(Lexical Analysis)

    通过Scanner将源码的字符流解析成符合规范的Token流,规范化的Token包括以下几类:

    • java关键字:如public,String等等
    • 自定义内容:如方法名、变量名、类名、包名、甚至注释内容等
    • 运算符号:如 加减乘除、与或非等等符号, + - * / && || !=

    语法分析(Syntax Analysis)

    根据已经处理好的Token流,通过TreeMaker构建抽象语法树,语法树是由JCTree的子类型构建的,所有节点实现了com.sun.source.Tree及其子类。如以下java示例代码对应生成的AST:

    package com.example.adams.astdemo;
    public class TestClass {
        int x = 0;
        int y = 1;
        public int testMethod(){
            int z = x + y;
            return z;
        }
    }
    
    TestClass.png

    记录到符号列表

    符号表记录的内容,主要是为做语义检查或生成中间代码,在目标代码生成阶段, 符号表是对符号名进行地址分配时的参考来源。

    • 将所有类出现的符号记录到类自身的符号表中,包括类符号、参数、类型、父类、继承、接口等都记录到一个To Do List中
    • 将To Do List中所有类解析到各自的类符号列表中,这个过程使用到MemberEnter.complete()

    Annotation Processing

    JDK1.6之后Java支持插入式注解,Java注解处理的过程可以获取到所有抽象语法树节点对象,并可以进行增删改查等,语法树被修改后回到"Parse and Enter"步骤,直到不再生成新的内容。

    Analyse and Generate

    分析树和生成类文件的工作通过访问者的形式执行,这些访问者处理编译器的To Do List中的条目。To Do列表中的每个类目由相应访问者处理:

    • Attr
      解析语法树中的名称、表达式和其他元素,并将其与相应的类型、符号关联起来,这个步骤可以通过Attr检测出潜在的语义错误。

    • Flow

      流分析用于检查变量的确定赋值,以及可能导致额外错误的不可达语句。

    • TransTypes

      流分析用于检查变量的确定赋值,以及可能导致额外错误的不可达语句。

    • Lower

      使用Lower处理“语法糖”,它重写语法树,通过替换等效的、更简单的树来消除特定类型的子树。这将处理嵌套类和内部类、类字面量、断言、foreach循环等等。对于被处理的每个类,Lower返回一个树列表,其中包含已翻译的类及其所有已翻译的嵌套类和内部类。

    • Gen

      类方法的代码由Gen生成,它创建包含JVM执行方法所需的字节码的Code属性。如果这一步成功后ClassWriter将写出该类。

    经过以上几个步骤,最终生成字节码文件(.class),此步骤由com.sun.tools.javac.jvm.Gen类来完成。编码过程中生成的AST、符号列表等等都记录到字节码文件中。

    如何使用AST?

    上文我们提到了AST是由JCTree及内部类构成的树节点,所以对AST的操作也是通过 com.sun.tools.javac.tree.JCTree类。具体的方法如下:

    `/** Visit this tree with a given visitor. */ 
    public abstract void accept(Visitor v);`
    

    通过入参Visitor可以获取到AST的所有语法节点信息,并且可以对AST做增删查改操作。

    sun工具库中提供了一个操作JCTree的类 com.sun.tools.javac.tree.TreeMaker这个类提供了操作AST的方法 具体API文档 ,有了TreeMaker就可以对AST做增删查改了。

    以下实现一个简单的demo:

    功能表述:通过修改AST的方式,对目标类自动生成类属性的set方法。

    步骤:

    1. 自定义一个注解

      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Retention(RetentionPolicy.SOURCE)
      @Target(ElementType.TYPE)
      public @interface SetterAnnotation {
      }
      
    2. 创建一个目标类,且将自定义注解加到目标类中,目标类中有name、age两个属性,没有set方法

      @SetterAnnotation
      public class Target {
          private String name;
          private int age;
      }
      
    3. 定义一个注解解析器(其中包括对AST的读取与修改)

      import com.sun.source.tree.Tree;
      import com.sun.tools.javac.api.JavacTrees;
      import com.sun.tools.javac.code.Flags;
      import com.sun.tools.javac.code.Type;
      import com.sun.tools.javac.processing.JavacProcessingEnvironment;
      import com.sun.tools.javac.tree.JCTree;
      import com.sun.tools.javac.tree.JCTree.JCAssign;
      import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
      import com.sun.tools.javac.tree.JCTree.JCIdent;
      import com.sun.tools.javac.tree.JCTree.JCModifiers;
      import com.sun.tools.javac.tree.TreeMaker;
      import com.sun.tools.javac.tree.TreeTranslator;
      import com.sun.tools.javac.util.Context;
      import com.sun.tools.javac.util.List;
      import com.sun.tools.javac.util.ListBuffer;
      import com.sun.tools.javac.util.Name;
      import com.sun.tools.javac.util.Names;
      
      import javax.annotation.processing.AbstractProcessor;
      import javax.annotation.processing.Messager;
      import javax.annotation.processing.ProcessingEnvironment;
      import javax.annotation.processing.RoundEnvironment;
      import javax.annotation.processing.SupportedAnnotationTypes;
      import javax.annotation.processing.SupportedSourceVersion;
      import javax.lang.model.SourceVersion;
      import javax.lang.model.element.Element;
      import javax.lang.model.element.TypeElement;
      import java.util.Set;
      
      /**
       * 自定义注解处理器
       */
      @SupportedSourceVersion(SourceVersion.RELEASE_8)
      @SupportedAnnotationTypes("SetterAnnotation")
      public class SetterProcessor extends AbstractProcessor {
      
          private Messager messager;
          private JavacTrees javacTrees;
          private TreeMaker treeMaker;
          private Names names;
      
          /**
           * JavacTrees 提供了待处理的抽象语法树
           * TreeMaker 封装了创建AST节点的一些方法
           * Names 提供了创建标识符的方法
           */
          @Override
          public synchronized void init(ProcessingEnvironment environment) {
              super.init(environment);
              this.messager = environment.getMessager();
              this.javacTrees = JavacTrees.instance(environment);
              Context context = ((JavacProcessingEnvironment) environment).getContext();
              this.treeMaker = TreeMaker.instance(context);
              this.names = Names.instance(context);
          }
      
          @Override
          public boolean process(Set<? extends TypeElement> annotation, RoundEnvironment roundEnv) {
              Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(SetterAnnotation.class);
              elementsAnnotatedWith.forEach(e -> {
                  //获取JCTree对象
                  JCTree tree = javacTrees.getTree(e);
                  tree.accept(new TreeTranslator() {
                      @Override
                      public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                          //定义一个TO DO list
                          List<JCTree.JCVariableDecl> declList = List.nil();
                          System.out.println("类名:" + jcClassDecl.name);
                          //遍历抽象树中的所有属性
                          for (JCTree jcTree : jcClassDecl.defs) {
                              if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                                  System.out.println("变量信息:" + jcTree.toString());
                                  //过滤掉只处理类属性
                                  JCTree.JCVariableDecl jcDecl = (JCTree.JCVariableDecl) jcTree;
                                  declList = declList.append(jcDecl);
                              }
                          }
                          //对TO DO List遍历
                          declList.forEach(decl -> {
                              //messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + "has been processed");
                              JCTree.JCMethodDecl methodDecl = generateMethodDecl(decl);
                              jcClassDecl.defs = jcClassDecl.defs.prepend(methodDecl);
                          });
                          super.visitClassDef(jcClassDecl);
                      }
                  });
              });
      
              return true;
          }
      
          /**
           * 根据类属性描述生成 方法描述
           *
           * @param variableDecl 类属性标识
           */
          private JCTree.JCMethodDecl generateMethodDecl(JCTree.JCVariableDecl variableDecl) {
              ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
              //1.方法表达式
              //左表达式 生成 this.name
              JCIdent thisN = treeMaker.Ident(names.fromString("this"));
              JCFieldAccess jcFieldAccess = treeMaker.Select(thisN, variableDecl.getName());
              //右表达式 name
              JCIdent name = treeMaker.Ident(variableDecl.getName());
              //左右表达式拼接后,生成表达式 this.name = name;
              JCTree.JCExpressionStatement statement = createExecExp(jcFieldAccess, name);
              statements.append(statement);
              //创建组合语句
              JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
      
              //2.方法参数
              //创建访问标志语法节点
              JCModifiers jcModifiers = treeMaker.Modifiers(Flags.PARAMETER);
              JCTree.JCVariableDecl param = treeMaker.VarDef(jcModifiers, variableDecl.getName(), variableDecl.vartype, null);
              List<JCTree.JCVariableDecl> parameters = List.of(param);
      
              //3.方法返回表达式
              JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
              JCModifiers publicModifiers = treeMaker.Modifiers(Flags.PUBLIC);
              Name newName = transformName(variableDecl.getName());
              return treeMaker.MethodDef(publicModifiers, newName, methodType, List.nil(), parameters, List.nil(), block, null);
          }
      
          private Name transformName(Name name) {
              String s = name.toString();
              return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
          }
      
          /**
           * 创建可执行语句语法树
           *
           * @param lhs 做表达时候
           * @param rhs 右表达式
           */
          private JCTree.JCExpressionStatement createExecExp(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
              return treeMaker.Exec(this.createAssign(lhs, rhs));
          }
      
          /**
           * 创建赋值语句语法树
           *
           * @param lhs 左表达式
           * @param rhs 右表达式
           */
          private JCAssign createAssign(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
              return treeMaker.Assign(lhs, rhs);
          }
      }
      
    4. 进入终端编译自定义注解和注解解析器,然后通过自定定义注解解析器编译目标类

      javac -cp $JAVA_HOME/lib/tools.jar Setter* -d .
      javac -processor SetterProcessor Target.java
      
    编码命令截图.png
    1. 生成的类文件中自动加了set方法


      被修改后的类文件.png

    基于AST自定义sonar代码扫描规则

    AST应用范围很广,很多业界的开源的或商业化的应用都或多或少使用到了AST技术。如java项目常用的开源库Lombok,JavaScript用于发现和修复代码的ESLint等都是基于AST实现的。另外还有业界比较流行的语法规则框架PMD(Programming Mistake Detector)的各个语言实现插件也是基于AST。下面就基于PMD规范实现一个自定义的sonarQube代码扫描规则。

    • fork sonar-pmd-p3c

    • 自定义规则类

      package org.sonar.plugins.pmd.rule.design;
      
      import lombok.extern.slf4j.Slf4j;
      import net.sourceforge.pmd.lang.ast.Node;
      import net.sourceforge.pmd.lang.java.ast.ASTSingleMemberAnnotation;
      import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
      
      import java.util.Arrays;
      import java.util.List;
      
      @Slf4j
      public class DmlNotBeDySqlRule extends AbstractJavaRule {
          private static final List<String> TARGET_ANNOTATION = Arrays.asList("Update", "Delete", "Insert");
          private static final String DYSQL = "DYSQL";
      
          /**
           * 因为检测目标是注解所以只需要重写 AbstractJavaRule#visit(ASTSingleMemberAnnotation, Object)
           * 若检测目标是类属性则重写 AbstractJavaRule#visit(ASTFieldDeclaration, Object)
           *
           * @param annotation astAnnotation
           * @param data data
           * @return data
           */
          @Override
          public Object visit(ASTSingleMemberAnnotation annotation, Object data) {
              try {
                  if (annotation != null) {
                      //限制检测范围值检测注解名称是 "Update", "Delete", "Insert" 的
                      if (TARGET_ANNOTATION.contains(annotation.getAnnotationName())) {
                          //根据 XPath路径检索AST节点
                          List<Node> list = annotation.findChildNodesWithXPath("MemberValue//PrimaryExpression//PrimaryPrefix//Name");
                          for (Node n : list) {
                              if (DYSQL.equals(n.getImage())) {
                                  log.info("有{}语句使用了动态SQL需要修改", annotation.getAnnotationName());
                                  addViolation(data, annotation);
                              }
                          }
                      }
                  }
              } catch (Exception e) {
                  log.error("DmlNotBeDySqlRule 遇到不是预期的文件格式", e);
              }
              return super.visit(annotation, data);
          }
      }
      
    • 添加规则配置


      1.png

      继续添加规则配置,规则标识需要唯一


      2.png

      添加配置文件,支持sonarQube做简略提示


      3.png
      新增规则xml文件,且配置
      4.png

      添加规则定义


      5.png
    • 构建项目打成jar包

      mvn -Dlicense.skip=true -Dmaven.test.skip=true clean package
      
    6.png
    • 将jar包上传到sonarQube插件目录下


      7.png
    • 重启sonarQube,并到管理界面激活规则


      8.png

      完成以上步骤后,项目编译过程中就可以检测风险代码,且可以再sonarQube平台正常提示。


      9.png

    四、总结

    • AST是什么?

      抽象语法树(Abstract Syntax Tree),是源代码语法结构的抽象表示,以树状形式展示编程语言的语法结构。

    • AST怎么产生的?

      源代码编译的过程中产生的,可支持增删查改。编译过程中的语法分析、语义分析步骤等都使用到AST。

    • 如何操作AST?

      AST是由JCTree及内部类构成的树节点(限java),而TreeMaker是AST的操作工具类,提供了操作AST的API,基于此API可操作AST。

    • AST的应用案例:

      基于PMD实现自定义代码规则。其实基于AST完全可以去扩展做很多事情,如动态加日志;基于注解自动添加代码非空判断;甚至可以往自动化测试方面去做一些工具。

    五、相关连接

    Java编译概述

    AST树可视化工具

    语法树AST全面解析

    如何修改语法树

    twilio blog

    pmd docs

    TreeMaker

    tabnine

    Baeldung

    相关文章

      网友评论

          本文标题:AST从了解到自定义sonar代码规则

          本文链接:https://www.haomeiwen.com/subject/qnhwsdtx.html