AST从了解到自定义sonar代码规则
AST是什么?
抽象语法树(Abstract Syntax Tree)简称AST,它是源代码语法结构的抽象表示,以树状形式展示编程语言的语法结构,树的不同节点对应源代码的对应部分。
不同的编程语言生成的AST不尽相同,相同的语言若是不同的解析工具,生成的AST也是不尽相同的,有些工具生成的AST节点会多出一些属性。为了统一,文章中举的例子统一是基于java语言的AST,但基本上每种编程语言都有类似的。
AST在javac编译过程中是怎么产生的?
Java源码的编译过程可以概括性的总结为以下几个步骤:
javac-flow.pngParse 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方法。
步骤:
-
自定义一个注解
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 { }
-
创建一个目标类,且将自定义注解加到目标类中,目标类中有name、age两个属性,没有set方法
@SetterAnnotation public class Target { private String name; private int age; }
-
定义一个注解解析器(其中包括对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); } }
-
进入终端编译自定义注解和注解解析器,然后通过自定定义注解解析器编译目标类
javac -cp $JAVA_HOME/lib/tools.jar Setter* -d . javac -processor SetterProcessor Target.java
-
生成的类文件中自动加了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
-
将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完全可以去扩展做很多事情,如动态加日志;基于注解自动添加代码非空判断;甚至可以往自动化测试方面去做一些工具。