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

2023-05-17  本文已影响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包括以下几类:

语法分析(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

记录到符号列表

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

Annotation Processing

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

Analyse and Generate

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

经过以上几个步骤,最终生成字节码文件(.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代码扫描规则。

6.png

四、总结

五、相关连接

Java编译概述

AST树可视化工具

语法树AST全面解析

如何修改语法树

twilio blog

pmd docs

TreeMaker

tabnine

Baeldung

上一篇下一篇

猜你喜欢

热点阅读