文章目录
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
定义规则的方法
- 首先使用好注解@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());
}
}
- 重写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规则
- 测试能否成功启动
在test包下,运行debug_rules方法,如果能够运行成功,则说明规则添加成功了,不成功的话debug找原因,有可能是配置文件错了等等(因为就算json文件的属性写错了Bug写成BUG它也会报错)
- mvn package 打包放进D:\sonarqube-7.4\extensions\plugins
要把原来的sonar-java-plugin-5.9.2.jar 删掉,启动web没报错则说明规则添加成功