美文网首页
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

相关文章

  • 持续集成2-SonarQube

    sonar是一个代码质量管理平台,根据规则对代码进行静态检查,对保证工程的代码质量很有帮助 sonar5.5是最后...

  • Babel的简单了解

    在了解Babel之前我们有必要了解下AST, 什么是AST呢? AST(抽象语法树) 如上代码可以被表示如下的一棵...

  • sonar自定义规则

    Sonar并不是简单地把不同的代码检查工具结果(例如 FindBugs,PMD 等)直接显示在 Web 页面上,而...

  • Swift AST的一点研究

    作为一个2年的iOS开发也是最近一段时间才了解到 AST 这么一个东西。。。 AST在iOS中的应用 1、代码语法...

  • 创建 AST 节点写法示例

    AST (Abstract Syntax Tree(抽象语法树)) 是源代码语法结构的一种抽象表示。不了解 AST...

  • SonarQube结合FindBugs Security Aud

    背景 近期公司做的一个项目,客户对代码安全这块要求特别严格,不满足于sonar默认的sonar way规则集,因为...

  • sonar

    Sonar简介 Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量,可以从七个维度检测代码质量 通过...

  • sonar自定义规则笔记

    对于sonar的安装,笔记并未做相关记录,原因很简单,百度一下你就知道;笔记着重自定义规则开发,个人也是慢慢摸索,...

  • AST转化网易易盾的部分代码

    用AST把 转成这样 打开网站https://blogz.gitee.io/ast/把代码复制到这个网站解析成AS...

  • 使用typescript来修改js文件

    原理 使用typescript把源代码读成ast,然后修改ast结构,然后将ast重新还原成js代码,最后将js代...

网友评论

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

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