SonarQube 体系结构与自定义Java规则并集成至Sonar中教程

Sonar体系结构与扫描原理

体系结构:sonar是基于静态代码扫描的框架,本质上基于静态代码的语法树,所以不能够在获取代码在运行时的状态,所以反射就无法使用了,所有变量的class类型是不可动态获取的,如果需要获取变量的类型,只能在扫描到建立对象或者局部变量的时候,自己使用hashmap把对象名与对象类型建立关系。

语法树有多种类型,如java 中的If语句对应的就是IfStatementTree , 成员方法则对应MethodTree , 树的关系在代码里是组合关系,但实际上是体现父级思想的,这些不同的数据结构(语法树)里对应着各自不同的回调方法。

扫描原理:按照层级思想,进行代码扫描,若为同一个层级,则依次遍历。比如一个类中有3个成员方法,则每次扫描到一个成员方法,就会回调visitMethodTree这个回调方法,看回调中要不要进行对当前成员方法的进一步处理,进一步处理则扫描方法里的代码,如果不进一步处理则方法里的内容相当于跳过扫描。后文有具体代码上的讲解。
如果你不熟悉观察者模式,则需要先去了解观察者模式,它是整个代码检测的核心,sonar进行扫描时会根据语法树种类回调对应的方法。

需要去浏览学习的网站

https://github.com/SonarSource/sonar-java/blob/master/docs/CUSTOM_RULES_101.md
https://www.jianshu.com/p/141d80624f3c?utm_campaign=maleskine 这个demo需要好好理解,debug慢慢看数据结构,下文也会附这里例子的解释。

Demo

项目结构

在这里插入图片描述

DontUseCacheCheck.java
该文件不要求编译,但是构造应该是正确的,否则会解析错误。

class DontUseCacheCheck {
 public void test() {
     Map<String,Object>map = (Map<String,Object>)GlobalCache.getInstance().getCacheData();
     GlobalCache.getInstance().setCacheData(map);// Noncompliant
	}
}

DontUseCacheCheckTest类

public class DontUseCacheCheckTest {
  @Test
  public void detected2() {
   JavaCheckVerifier.verify("src/test/mytestfile/DontUseCacheCheck.java",new GrpcMessageCheck());
  }
}

其中DontUseCacheRule 、json、html都是集成到sonar中的必备文件
DontUseCacheRule



import org.sonar.check.Rule;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.*;
//注意@Rule的key要与html json的名字一样
@Rule(key = "S5999")
public class DontUseCacheRule extends BaseTreeVisitor implements JavaFileScanner {

  private JavaFileScannerContext context;

  String parameter1 = "";
  String parameter2 = "";
  String map1 = "";
  String map2 = "";


  @Override
  public void scanFile(JavaFileScannerContext context) {
    //这里的context会由sonar自己注入,报告的所有问题都会通过context进行上传
    this.context = context;
    scan(context.getTree());
  }

  @Override
  public void visitConditionalExpression(ConditionalExpressionTree tree) {
  }

  @Override
  public void visitMemberSelectExpression(MemberSelectExpressionTree tree) {
    parameter1 = tree.identifier().toString();
    if (parameter1.equals("getCacheData")) {
      if (tree.parent().parent().parent().is(Tree.Kind.VARIABLE)) {
        VariableTree variableTree = (VariableTree) tree.parent().parent().parent();
        if (variableTree.simpleName() != null) {
          map1 = variableTree.simpleName().toString();
        }
      }
      if (tree.expression().is(Tree.Kind.METHOD_INVOCATION)) {
        MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree.expression();
        if (methodInvocationTree.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
          MemberSelectExpressionTree memberSelectExpressionTree = (MemberSelectExpressionTree) methodInvocationTree
            .methodSelect();
          parameter2 = memberSelectExpressionTree.lastToken().text();
          if (parameter1 != "" && parameter2 != "") {
            if ((parameter1.equals("getCacheData") || (parameter1.equals("setCacheData")))
              && (parameter2.equals("getInstance"))) {
              if (map1.equals(map2)) {
                  //该方法为向sonar报告错误的方法
                //context.reportIssue(this, tree, "不允许使用缓存");//自行注释或者解开注释
              }
            }
          }
          super.visitMemberSelectExpression(tree);
        }
      }
    }

    if (parameter1.equals("setCacheData")) {
      if (tree.parent().parent().is(Tree.Kind.EXPRESSION_STATEMENT)) {
        ExpressionStatementTree expressionStatementTree = (ExpressionStatementTree) tree.parent().parent();
        if (expressionStatementTree.expression().is(Tree.Kind.METHOD_INVOCATION)) {
          MethodInvocationTree methodInvocationTree = (MethodInvocationTree) expressionStatementTree.expression();
          if (methodInvocationTree.arguments().is(Tree.Kind.ARGUMENTS)) {
            Arguments argumentListTree = (Arguments) methodInvocationTree.arguments();
            if (argumentListTree.size() > 0) {
              map2 = argumentListTree.get(0).toString();
            }
          }
        }
      }
      if (tree.expression().is(Tree.Kind.METHOD_INVOCATION)) {
        MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree.expression();
        if (methodInvocationTree.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
          MemberSelectExpressionTree memberSelectExpressionTree = (MemberSelectExpressionTree) methodInvocationTree
            .methodSelect();
          parameter2 = memberSelectExpressionTree.lastToken().text();
          if (parameter1 != "" && parameter2 != "") {
            if ((parameter1.equals("getCacheData") || (parameter1.equals("setCacheData")))
              && (parameter2.equals("getInstance"))) {
              //if(map1.equals(map2)){ //自行注释或者解开注释
              context.reportIssue(this, tree, "不允许使用缓存");
              //}
            }
          }
          super.visitMemberSelectExpression(tree);
        }
      }
    }
  }
}

S5999_java.json
该规则在代码检测时的严重程度

{
  "title": "Check regular rule in grpc file of java",
  "type": "BUG",//CODE_SMELL
  "status": "ready",
  "tags": [
    "bugs"
  ],
  "defaultSeverity": "Critical" //Info
}

S5999_java.html
查看在sonar中查看规则详情时展示出来的页面

<p>grpc function without OnCompleted() lead to system resources leaking on condition of a large rate of flow</p>
<p>if you dont use primitive "ObserverStream" object as a transfer object,this addon may not work</p>
<p>if you use switch keyword on Controller,this addon may not work </p>

语法树的定义

在sonar中,共定义62种基本语法树,在此基础上的语法树里面又包含多种的语义树,基本上是一种java关键字就对应一个语法树,但基本上结构都是大同小异,当你明白如何去获取其中一种的结构的信息时,其他的结构也能通过debug调试出来,故在文章中仅仅解释几种基本的树,其他的如果你用到了可以进行补充。要结合demo debug看理解得更快

Tree(顶级父类)

后面所有的树全部实现这个类,是所有树的顶级父类

public interface Tree {
  //这个方法用于在对象运行时能够获取到它的实现类
  boolean is(Kind... kinds);
  //每种树都要实现这个回调方法,表示要对这个树下的哪些信息要进一步进行扫描
  void accept(TreeVisitor visitor);
  //这个parent方法很重要,表示
  Tree parent();

  SyntaxToken firstToken();

  SyntaxToken lastToken();
  //枚举类型,用于上面的is方法
  enum Kind implements GrammarRuleKey {
  ...
  /**
     * {@link ClassTree}
     */
    CLASS(ClassTree.class)
    ...
    }
if(true){
  ...
}
在用法上通常用于转换类型,如下面的代码可以判断的结构为上面的if块
if(block.parent().is(Tree.Kind.IF_STATEMENT){
	 IfStatementTree ifTree = (IfStatementTree) block.parent();
}

MethodTree

成员方法树,对应一个类中的成员方法。

public interface MethodTree extends Tree {
    
  //成员方法上的修饰符,如public private等
  ModifiersTree modifiers();

  TypeParameters typeParameters();

  //方法的返回类型
  TypeTree returnType();
  //方法的名称
  IdentifierTree simpleName();
	//终结符,与编译原理有关
  SyntaxToken openParenToken();
	//方法上的参数
  List<VariableTree> parameters();
//)终结符
  SyntaxToken closeParenToken();
  //throws关键字
  SyntaxToken throwsToken();
 //方法抛出的异常
  ListTree<TypeTree> throwsClauses();
	//方法体,重要,即用{}包起来的代码
  
  BlockTree block();

}

StatementTree(语句的父类)

语句树,所有的基本语句(调用方法,变量赋值等)都要实现该类
在这里插入图片描述

ExpressionStatementTree

表达式树,一条语句里面有多个组成部分(如user.setId().setName())这种嵌套调用,则会用一个ExpressionStatementTree表示一条语句

public void test(){
  CommonUtils.check();
}
//像这种的嵌套结构
//是BlockTree->ExpressionStatementTree-> MethodInvocationTree->MethodSelectExpressionTree->IdentifierTree
//实际上就是信息从大到小细化的过程,最后的IdentifierTree可以为对象CommonUtils与对象的方法check

VariableTree

局部变量树,只要在方法局部变量表中的变量都算是VariableTree,最简单的表现显示是int a = 1;

public interface VariableTree extends StatementTree {

  ModifiersTree modifiers();
  //当前变量的类型
  TypeTree type();
    //当前等号左值的名称
  IdentifierTree simpleName();
  //等号终结符
  SyntaxToken equalToken();
   //当前等号右值的名称
  ExpressionTree initializer();
  
  Symbol symbol();
  //结束分号符,即 ;,与编译原理有关
  SyntaxToken endToken();
}
获取变量上的注解的名称

这些操作不推荐照搬这里的,建议使用debug

for (AnnotationTree annotation : variableTree.modifiers().annotations()) {
  System.out.println(annotation.annotationType().firstToken().text());
}

获取注解上的标签名

这些操作不推荐照搬这里的,建议使用debug

List<AnnotationTree> annotations = variableTree.modifiers().annotations();
 Arguments arguments = annotation.arguments();

BlockTree

用{}包起来的代码块,包含于IfStatementTree, SwitchStatementTree WhileStatmenTree staticIn等等有{}代码块的地方

if(true){
 blockTree
}else{
    blockTree
}
while(){blockTree}
public void test(){
 blockTree
}
public interface BlockTree extends StatementTree {
  //终结符,与编译原理有关
  SyntaxToken openBraceToken();
  //只有这个是有用的,里面记录了全部的语句
  List<StatementTree> body();
  //终结符,与编译原理有关
  SyntaxToken closeBraceToken();

}

MethodInvocationTree

定义为方法调用树,能够获取方法的参数,成员信息树

@Beta
public interface MethodInvocationTree extends ExpressionTree {

    //这里的typeArguments实际上是sonar根据前文的变量的type类型推断出来的参数类型,因为不是运行时的环境,所有实际这个没啥用
    //如果有一些方法的返回值是未知的,最好自己建立一个hashmap来存变量与对应的类型
  TypeArguments typeArguments();
  //方法的成员选择树
  ExpressionTree methodSelect();
  //方法的参数,Arguments具体实现类请自己debug看
  Arguments arguments();
  Symbol symbol();
}

MemberSelectExpressionTree

定义为成员信息树,实际上如user.setId(333).setName(“张三”)这种嵌套调用,会是下面这种结构

MethodInvocationTree记为MIT,MemberSelectExpressionTree记为MSET
首先它是一个调用方法,则首先是MIT_1,
MIT_1里面包含一个MSET_1
(identifier为setName,expression为MIT类型(即user.setId(),记为MIT_2)
再对MIT_2进行递归解引用,
MIT_2里面包含一个MSET_2
(identifier为setId,expression为IdentifierTree类型,对象信息为user

参数信息 333,张三 在MethodInvocationTree中,调用的成员信息全部在MemberSelectExpressionTree

IfStatementTree

主要了解if 为true或false的情况下一般都是blocktree就可以了

@Beta
public interface IfStatementTree extends StatementTree {
 //if关键字终结符
  SyntaxToken ifKeyword();
  //终结符
  SyntaxToken openParenToken();
    //if包着的条件
  ExpressionTree condition();
      //)终结符
  SyntaxToken closeParenToken();
    //if为true的情况的树,一般情况下是blockTree,因为if后一般跟{}
  StatementTree thenStatement();

  SyntaxToken elseKeyword();
    //if为false的情况的树,一般情况下是blockTree,因为else后一般跟{}
  StatementTree elseStatement();

}
public void test(){
	if(true){
  	  ...
	}else{
	 ...
	}
}
如果要解析上面的方法,树的关系则会是
MethodTree
	BlockTree
		IfStatementTree 
			BlockTree
			BlockTree

定义规则的方法

  1. 首先使用好注解@Rule ,要继承BaseTreeVisitor与实现JavaFileScanner,这里的代码基本上是固定格式,是上下文信息与扫描入口
@Rule(key = "S5999")
public class YourRule extends BaseTreeVisitor implements JavaFileScanner {
    private JavaFileScannerContext context;
    @Override
  public void scanFile(JavaFileScannerContext context) {
    this.context = context;
    //扫描入口方法,一开始context.getTree()的type为一个CLASS类
    scan(context.getTree());
  }
}
  1. 重写BaseTreeVisitor中的回调方法
    如果你关心成员方法则重写visitMethod方法,对应上面的MethodTree, 如果关心变量赋值则重写visitVariable方法,对应上面的VariableTree
    如果关心方法调用的信息则可以visitMemberSelectExpression与visitMethodInvocation,对应MethodInvocationTree,MemberSelectExpressionTree这两种树又可以在某种程度上通过MemberSelectExpressionTree.parent() 与MethodInvocationTree. methodSelect()进行转换
    但是如果关心全限定类名则需要使用MemberSelectExpressionTree。
    你还需要关心调用super的时机,给出下面的例子
@Override
  public void visitMethod(MethodTree tree) {
      //获取成员方法的入参
    List<VariableTree> parameters = tree.parameters();
    if (parameters.size()>=1){
      //获取最后一个参数的类型
      VariableTree variableTree = parameters.get(parameters.size() - 1);
      String variableTypeName = SonarInfoUtils.getVariableTypeName(variableTree);
      //检查最后一个参数的类型是不是StreamObserver,如果是则可以认为是一个grpc方法
      if(isGrpcFunc(variableTypeName)){
        //如果是grpc方法才对这个方法继续进行扫描
        super.visitMethod(tree);
      }else{
          //如果该方法不是Grpc方法,则放弃对该方法进行遍历
      }
  }
@Override
//super中的方法,里面定义了该方法里的内容会被继续扫描
  public void visitMethod(MethodTree tree) {
    scan(tree.modifiers());
    scan(tree.typeParameters());
    scan(tree.returnType());
    scan(tree.simpleName());
    scan(tree.parameters());
    scan(tree.defaultValue());
    scan(tree.throwsClauses());
    //代码块树,即方法里的内容,只有调用了super.visitMethod,
    //该行才会被调用,才能看到方法里的内容
    scan(tree.block());
  }

其实就是选择对当前关心的部分是继续扫描还是放弃扫描,这个调用super的时机会影响到后面的代码逻辑是否会被遍历到
2. 设置合适的成员变量,代码都是重写BaseTreeVisitor中的回调方法,所以基本上只能通过成员变量进行传参
3. 使用合适的设计模式对代码进行设计,因为代码可以是多变的,而sonar是基于静态文本的方法进行扫描,是不能获取方法执行时的压栈情况的,所以需要对代码进行维护,策略模式、适配器模式都是可选的。

往sonar中注册插件

0.先去github拉取已经下载的sonarqube版本对应的java规则包

1.往java-checks包下的org.sonar.java.checks下添加自己的规则,把自己的规则和官方规则混在一起就可以了
在这里插入图片描述

2.配置信息文件
在这里插入图片描述

往sonar-java源码中添加自己的规则,同时修改Sonar_way_profile.json文件,加上自己的S5999规则
在这里插入图片描述

  1. 测试能否成功启动
    在test包下,运行debug_rules方法,如果能够运行成功,则说明规则添加成功了,不成功的话debug找原因,有可能是配置文件错了等等(因为就算json文件的属性写错了Bug写成BUG它也会报错)
    在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4X2Z9z3d-1631866271385)(/tfl/pictures/202107/tapd_32126111_1626859508_29.png)]

  1. mvn package 打包放进D:\sonarqube-7.4\extensions\plugins
    要把原来的sonar-java-plugin-5.9.2.jar 删掉,启动web没报错则说明规则添加成功
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值