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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容