Skip to content

A quick tutorial for writing custom Java rules using SonarJava plugin for SonarQube

Notifications You must be signed in to change notification settings

zbingsong/sonarjava-quick-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 

Repository files navigation

更新于2023-8-4

SonarQube自定义规则简便文档

Sonar通过遍历语法树的形式来进行代码检查。

官方的sonar-java插件里已经内置了很多规则,可以在这里看到源码。

语法树组件

语法树组件包含类型符号语法token

Tree

整个文件的语法树中的一个节点,表示一个语句、一个表达式、一个变量、一个方法、一个类等等。所有的树类型的组件都可以使用Tree类的方法。

Tree下有Tree.Kind枚举类,包含了所有树节点的种类。注:后面会遇到类型Type,和Tree.Kind不是同一个东西。为做区分,将Tree.Kind称为种类,将Type称为类型。

Tree可以大致分为表达式树ExpressionTree语句树StatementTree类型树TypeTree列表树ListTree模组命令树ModuleDirectiveTreeJava 9)和其它树。

  • boolean is(Kind... var1):判断这个节点的种类,比如Tree.Kind.METHODTree.Kind.CLASS等;一次可传入多个类型,如果有一个种类匹配则返回true

  • @Nullable Tree parent():获取父节点。

  • @Nullable SyntaxToken firstToken():获取这个节点的第一个SyntaxToken

  • @Nullable SyntaxToken lastToken():获取这个节点的最后一个SyntaxToken

  • Tree.Kind kind():获取这个节点的种类。

    这个方法很重要,因为它能精确地判断节点种类,即使这个节点的类型是TreeExpressionTree等大类。通过判断种类,可以对树进行强制类型转换。

表达式树ExpressionTree

父类:Tree

ExpressionTree有一系列不同的实现类,比如LiteralTreeMemberSelectExpressionTreeNewClassTreeMethodInvocationTree等等。

  • Type symbolType():表达式的值的类型

  • Optional<Object> asConstant():获取表达式的值。

  • <T> Optional<T> asConstant(Class<T> var1):获取表达式的值,以var1的类型返回。

注解树AnnotationTree

父类:ExpressionTreeModifierTree

之后会遇到功能相似的SymbolMetadataAnnotationTreeSymbolMetadata的区别在于,AnnotationTree的本质是树,可以以树的形式进行自定义的遍历(AnnotationTree的父节点是ModifierTree),而SymbolMetadata可以比较方便地获得注解参数的key和value。

  • SyntaxToken atToken():获取注解@字符的语法token

  • TypeTree annotationType():获取注解的类型树

  • Arguments arguments():获取注解的参数列表

数组元素访问表达式树ArrayAccessExpressionTree

父类:ExpressionTree

通过下标访问数组的元素的表达式,例如arr[1]

  • ExpressionTree expression():获取数组访问的表达式

  • ArrayDimensionTree dimension():获取数组的长度

赋值表达式树AssignmentExpressionTree

父类:ExpressionTree

注意在变量声明时的赋值表达式不算入赋值表达式树中,而算入变量树中。例如int a = 1中的a = 1不算赋值表达式。

  • ExpressionTree variable():获取赋值的左边的表达式

    这个表达式一定只含有变量名。

  • SyntaxToken operatorToken():获取赋值的运算符的语法token

  • ExpressionTree expression():获取赋值的右边的表达式

一元表达式树UnaryExpressionTree

父类:ExpressionTree

  • SyntaxToken operatorToken():获取运算符的语法token

  • ExpressionTree expression():获取运算符后的表达式。比如对于int a = -1,这个方法取到的是1

二元表达式树BinaryExpressionTree

父类:ExpressionTree

  • ExpressionTree leftOperand():获取左操作数的表达式

  • SyntaxToken operatorToken():获取运算符的语法token

  • ExpressionTree rightOperand():获取右操作数的表达式

条件表达式树ConditionalExpressionTree

父类:ExpressionTree

代表三元运算符?:的表达式,即a ? b : c

  • ExpressionTree condition():获取条件的表达式

  • SyntaxToken questionToken():获取问号的语法token

  • ExpressionTree trueExpression():获取条件为真时的表达式

  • SyntaxToken colonToken():获取冒号的语法token

  • ExpressionTree falseExpression():获取条件为假时的表达式

Instanceof树InstanceOfTree

父类:ExpressionTree

  • ExpressionTree expression():获取被判断的部分的表达式(即instanceof之前的部分)。

  • SyntaxToken instanceofToken():获取instanceof关键字的语法token

  • TypeTree type():获取判断的类型树(即instanceof之后的部分)。

Lambda表达式树LambdaExpressionTree

父类:ExpressionTree

  • @Nullable SyntaxToken openParenToken():获取左括号的语法token

  • @Nullable SyntaxToken closeParenToken():获取右括号的语法token

  • List<VariableTree> parameters():获取lambda表达式的参数列表,以一列变量树的方式列出。

  • SyntaxToken arrowToken():获取箭头的语法token

  • Tree body():获取lambda表达式的主体。

字面量树LiteralTree

父类:ExpressionTree

  • String value():获取常量的值。

  • SyntaxToken token():获取常量的语法token

成员访问树MemberSelectExpressionTree

父类:ExpressionTreeTypeTree

通过.访问成员的表达式,例如System.out.println()Arrays.asList()classInstance.field

注意如果有连续的.,那么每个.都会有一个MemberSelectExpressionTree。例如java.lang.annotation.*会被解析为三个MemberSelectExpressionTree,分别是java.lang.annotation.*java.lang.annotationjava.lang.越靠后的越优先解析)。在BaseTreeVisitor中则需要手动调用super.visitMemberSelectExpression()去获取前面部分的MemberSelectExpressionTree

  • ExpressionTree expression():获取访问成员的.之前的部分的表达式。这个表达式中如果不含.,就可以类型转换成标识符树;如果含.,则可以类型转换成成员访问树

    例如System.out.println()中,这个方法取到的是System.out

  • SyntaxToken operatorToken():获取.语法token。仅限成员名称前面的那个点。

  • IdentifierTree identifier():获取被访问的成员的标识符树

    例如System.out.println()中,这个方法取到的是println

    这个方法一律返回IdentifierTree,不会按照被访问的成员的类型来返回,例如System.out.println()System.out.println都会返回包含printlnIdentifierTree。如果需要区分类型,可以使用IdentifierTreesymbolType()方法。

调用方法树MethodInvocationTree

父类:ExpressionTree

  • @Nullable TypeArguments typeArguments():获取方法的泛型参数值列表

  • ExpressionTree methodSelect():获取调用方法的表达式

  • Arguments arguments():获取调用方法时输入的参数列表

  • Symbol symbol():获取被调用的方法的符号。注意符号中的类型是方法返回值的类型,而不是方法调用者的类型。

方法引用树MethodReferenceTree

父类:ExpressionTree

  • Tree expression():获取方法引用的表达式。

  • SyntaxToken doubleColon():获取::语法token

  • @Nullable TypeArguments typeArguments():获取方法的泛型参数值列表

  • IdentifierTree method():获取被引用的方法的标识符树

新建数组树NewArrayTree

表示创建一个新的数组。

父类:ExpressionTree

  • @Nullable TypeTree type():获取数组的类型树

  • @Nullable SyntaxToken newKeyword():获取new关键字的语法token

  • List<ArrayDimensionTree> dimensions():获取数组的长度

  • @Nullable SyntaxToken openBraceToken():获取左大括号的语法token

  • ListTree<ExpressionTree> initializers():获取数组的初始值,以列表树的方式列出。

  • @Nullable SyntaxToken closeBraceToken():获取右大括号的语法token

新建类对象树NewClassTree

父类:ExpressionTree

表示创建一个新的类对象。

  • @Nullable ExpressionTree enclosingExpression():如果创建对象是在某个语句里进行的(例如a.method(new MyClass())),获取外部的表达式

  • @Nullable SyntaxToken dotToken():获取.语法token。仅在这个新建对象的类是内部类时才会有。

  • @Nullable SyntaxToken newKeyword():获取new关键字的语法token

  • @Nullable TypeArguments typeArguments():获取新建对象时使用的泛型参数值列表

  • TypeTree identifier():获取类的类型树

    newClassTree.symbolType()(继承自ExpressionTree的方法)和newClassTree.identifier().symbolType()是等价的。

  • Arguments arguments():获取新建对象时使用的参数列表

  • @Nullable ClassTree classBody():如果新建的对象是一个匿名类,获取类的内容(类树)。

  • Symbol constructorSymbol():获取类的constructor符号

括号树ParenthesizedTree

父类:ExpressionTree

一个被括号包起来的表达式,例如(1 + 2) * 3中的(1 + 2)

注意仅限于表达式,方法调用、新建类对象、注解等传参使用的括号不包含在这个树中。

  • SyntaxToken openParenToken():获取左括号的语法token

  • ExpressionTree expression():获取括号中的表达式

  • SyntaxToken closeParenToken():获取右括号的语法token

类型转换树TypeCastTree

父类:ExpressionTree

  • SyntaxToken openParenToken():获取左括号的语法token

  • TypeTree type():获取转换的类型树

  • SyntaxToken closeParenToken():获取右括号的语法token

  • ExpressionTree expression():获取被转换的表达式

  • @Nullable SyntaxToken andToken():获取&语法token

    &用于交集类型。交集类型早在Java 8中就已经出现,但仅能用于泛型的边界(例如List<? extends A & B>)。在Java 10中,交集类型的适用范围才被扩展到了普通类型(定义变量、方法返回类型、类型转换等等)。

  • ListTree<Tree> bounds():如果转换到的类型中含有泛型,获取这些泛型的边界,用列表树列出。

Switch表达式树SwitchExpressionTree

父类:ExpressionTreeSwitchTree

无别的方法。

语句树StatementTree

父类:Tree

StatementTree本身并没有用,也没有Tree之外的方法。

它有一系列不同的子类,比如AssertStatementTreeReturnStatementTreeIfStatementTreeForEachStatementTree等等。

Assert语句树AssertStatementTree

父类:StatementTree

  • SyntaxToken assertKeyword():获取assert关键字的语法token

  • ExpressionTree condition():获取assert语句的条件表达式

  • @Nullable SyntaxToken colonToken():获取冒号的语法token

  • @Nullable ExpressionTree detail():获取assert语句的详细信息,即冒号后面的表达式。

  • SyntaxToken semicolonToken():获取分号的语法token

Break语句树BreakStatementTree

父类:StatementTree

  • SyntaxToken breakKeyword():获取break关键字的语法token

  • @Nullable IdentifierTree label():获取break语句的标签。

  • SyntaxToken semicolonToken():获取分号的语法token

类树ClassTree

父类:StatementTree

代表定义一个新类。

注意ClassTreeNewClassTree的区别。ClassTree是定义一个新类,而NewClassTree是创建一个已有的类的实例。

  • @Nullable SyntaxToken declarationKeyword():如果一个类不是匿名类,获取类定义时的classinterface关键字的语法token

  • @Nullable SyntaxToken simpleName():如果一个类不是匿名类,获取类名的语法token

  • TypeParameters typeParameters():获取类的泛型参数列表

  • ModifiersTree modifiers():获取类的修饰符列表,用多修饰符树的方式列出。

  • @Nullable TypeTree superClass():获取父类的类型树

  • Symbol.TypeSymbol symbol():获取类的类型符号

  • ListTree<TypeTree> superInterfaces():获取类的接口列表,以列表树的方式列出,元素类型是类型树

  • List<Tree> members():获取类的成员列表,以一列的方式列出。

  • SyntaxToken openBraceToken():获取左大括号的语法token

  • SyntaxToken closeBraceToken():获取右大括号的语法token

Continue语句树ContinueStatementTree

父类:StatementTree

  • SyntaxToken continueKeyword():获取continue关键字的语法token

  • @Nullable IdentifierTree label():获取continue语句的标签。

  • SyntaxToken semicolonToken():获取分号的语法token

Do-While语句树DoWhileStatementTree

父类:StatementTree

  • SyntaxToken doKeyword():获取do关键字的语法token

  • StatementTree statement():获取do语句的代码块(即do后面的语句)。

    这里把循环里的所有代码(包括大括号)当作一整个语句来处理,实际上返回BlockTree是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

  • SyntaxToken whileKeyword():获取while关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • ExpressionTree condition():获取while语句的条件表达式

  • SyntaxToken closeParenToken():获取右括号的语法token

  • SyntaxToken semicolonToken():获取分号的语法token

空语句树EmptyStatementTree

父类:StatementTreeImportClauseTree

  • SyntaxToken semicolonToken():获取分号的语法token

表达式语句树ExpressionStatementTree

父类:StatementTree

用于表达只含有单个表达式的语句,例如a += 1System.out.println()等等。

  • ExpressionTree expression():获取表达式

  • SyntaxToken semicolonToken():获取分号的语法token

For-Each语句ForEachStatement

父类:StatementTree

  • SyntaxToken forKeyword():获取for关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • VariableTree variable():获取for语句中循环用变量的变量树,即for (Type variable: expression)中的variable

  • SyntaxToken colonToken():获取冒号的语法token

  • ExpressionTree expression():获取for语句中被循环的表达式的表达式树,即for (Type variable: expression)中的expression

  • SyntaxToken closeParenToken():获取右括号的语法token

  • StatementTree statement():获取for语句后的代码块。

    这里把循环里的所有代码(包括大括号)当作一整个语句来处理,实际上返回BlockTree是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

For语句树ForStatementTree

父类:StatementTree

  • SyntaxToken forKeyword():获取for关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • ListTree<StatementTree> initializer():获取for语句中的初始化语句,以列表树的方式列出,元素类型是语句树

  • SyntaxToken firstSemicolonToken():获取第一个分号的语法token

  • @Nullable ExpressionTree condition():获取for语句中的条件表达式

  • SyntaxToken secondSemicolonToken():获取第二个分号的语法token

  • ListTree<StatementTree> update():获取for语句中的更新语句,以列表树的方式列出,元素类型是语句树

  • SyntaxToken closeParenToken():获取右括号的语法token

  • StatementTree statement():获取for语句后的代码块。

    这里把循环里的所有代码(包括大括号)当作一整个语句来处理,实际上返回BlockTree是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

If语句树IfStatementTree

父类:StatementTree

对于含有else if的语句,else if会被解析成嵌套在elseStatement里面的IfStatementTree

例:

if (state == 1) {
  ...
} else if (state == 2) {
  ...
} else if (state == 3) {
  ...
} else {
  ...
}

以上的代码会被解析成:

if (state == 1) {
  ...
} else {
  if (state == 2) {
    ...
  } else {
    if (state == 3) {
      ...
    } else {
      ...
    }
  }
}
  • SyntaxToken ifKeyword():获取if关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • SyntaxToken closeParenToken():获取右括号的语法token

  • ExpressionTree condition():获取if语句的条件表达式

  • StatementTree thenStatement():获取if语句的代码语句(即如果if条件为真时会执行的语句)。

    虽然返回值是StatementTree,但其实返回BlockTree更合理;这个返回的StatementTree把大括号中(包括大括号本身)所有的语句都当作一个语句来处理。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

  • @Nullable SyntaxToken elseKeyword():获取else关键字的语法token

  • @Nullable StatementTree elseStatement():获取else语句的代码语句(即如果if条件为假时会执行的语句)。

    虽然返回值是StatementTree,但其实返回BlockTree更合理;这个返回的StatementTree把大括号中(包括大括号本身)所有的语句都当作一个语句来处理。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

Labeled语句树LabeledStatementTree

父类:StatementTree

  • IdentifierTree label():获取标签的标识符树

  • SyntaxToken colonToken():获取冒号的语法token

  • StatementTree statement():获取标签后的语句

  • Symbol.LabelSymbol symbol():获取标签的类型符号

Return语句树ReturnStatementTree

父类:StatementTree

  • SyntaxToken returnKeyword():获取return关键字的语法token

  • @Nullable ExpressionTree expression():获取return语句的返回表达式

  • SyntaxToken semicolonToken():获取分号的语法token

Switch语句树SwitchStatementTree

父类:StatementTreeSwitchTree

本身没有方法,所用的方法全部来自于SwitchTree

Synchronized语句树SynchronizedStatementTree

父类:StatementTree

仅限于synchronized代码块,不包括synchronized方法。

  • SyntaxToken synchronizedKeyword():获取synchronized关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • ExpressionTree expression():获取synchronized语句括号里的表达式

  • SyntaxToken closeParenToken():获取右括号的语法token

  • BlockTree block():获取synchronized后的代码块树

Throw语句树ThrowStatementTree

父类:StatementTree

  • SyntaxToken throwKeyword():获取throw关键字的语法token

  • ExpressionTree expression():获取throw后面语句的表达式

  • SyntaxToken semicolonToken():获取分号的语法token

Try语句树TryStatementTree

父类:StatementTree

  • SyntaxToken tryKeyword():获取try关键字的语法token

  • @Nullable SyntaxToken openParenToken():获取左括号的语法token

  • @Nullable SyntaxToken closeParenToken():获取右括号的语法token

  • ListTree<Tree> resourceList():获取try语句括号中的资源列表,以列表树的方式列出。

  • BlockTree block():获取try后的代码块树

  • List<CatchTree> catches():获取try后的所有catch语句,以一列catch语句树的方式列出。

  • @Nullable SyntaxToken finallyKeyword():获取finally关键字的语法token

  • @Nullable BlockTree finallyBlock():获取try后的finally语句的代码块树

While语句树WhileStatementTree

父类:StatementTree

  • SyntaxToken whileKeyword():获取while关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • ExpressionTree condition():获取while语句的条件表达式

  • SyntaxToken closeParenToken():获取右括号的语法token

  • StatementTree statement():获取while语句后的代码语句

    这里把循环里的所有代码(包括大括号)当作一整个语句来处理,实际上返回BlockTree是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例

Yield语句树YieldStatementTree

父类:StatementTree

yield关键字是Java 13中引入、Java 14中正式确定的,用于在switch语句中返回值。详见Java 17更新文档

例:

String result = switch (expression) {
    case 1 -> yield "One";
    case 2 -> yield "Two";
    default -> yield "Other";
};

如果switch语句中所有的case都会返回值,那么yield关键字就可以被省略:

String result = switch (expression) {
    case 1 -> "One";
    case 2 -> "Two";
    default -> "Other";
};
  • @Nullable SyntaxToken yieldKeyword():获取yield关键字的语法token

  • ExpressionTree expression():获取yield语句的返回表达式

  • SyntaxToken semicolonToken():获取分号的语法token

变量树VariableTree

父类:StatementTree

  • ModifiersTree modifiers():获取变量的多修饰符树

  • TypeTree type():获取变量的类型树

  • IdentifierTree simpleName():获取变量名的标识符树

  • @Nullable SyntaxToken equalToken():如果变量在被声明时就赋值了,获取赋值语句中等号的语法token

  • @Nullable ExpressionTree initializer():如果变量在被声明时就赋值了,获取赋值语句中初始值的表达式

    注意在声明时的赋值表达式不算赋值表达式树

  • Symbol symbol():获取变量的符号

  • @Nullable SyntaxToken endToken():获取分号的语法token

块树BlockTree

父类:StatementTree

使用BlockTree的树和方法很少。

  • List<StatementTree> body():获取块的内容,以一列语句树的方式列出。

  • SyntaxToken openBraceToken():获取左大括号的语法token

  • SyntaxToken closeBraceToken():获取右大括号的语法token

静态初始化块树StaticInitializerTree

父类:BlockTree

代表一个类中的静态初始化代码块。

例:

class MyClass {
  private static final int a;
  // 静态初始化代码块
  static {
    a = 1;
  }
}
  • SyntaxToken staticKeyword():获取static关键字的语法token

方法树MethodTree

父类:Tree

  • ModifiersTree modifiers():获取方法的多修饰符树

  • TypeParameters typeParameters():获取这个方法的泛型参数列表

  • @Nullable TypeTree returnType():获取这个方法的返回值的类型树

    如果方法是构造方法或者方法语法有误,这个函数返回null

  • IdentifierTree simpleName():获取方法名的标识符树

  • List<VariableTree> parameters():获取这个方法的参数列表,以一列变量树的方式列出。

  • SyntaxToken throwsToken():获取这个方法的throws关键字的语法token

  • ListTree<TypeTree> throwsClauses():获取throws后面跟着的异常的类型树,以列表树的方式列出,元素类型是类型树

  • @Nullable BlockTree block():以块树的方式获取这个方法的内容。

  • @Nullable SyntaxToken defaultToken():获取这个方法的default关键字的语法token

  • @Nullable SyntaxToken openParenToken():获取这个方法的左括号的语法token

  • @Nullable SyntaxToken closeParenToken():获取这个方法的右括号的语法token

  • Symbol.MethodSymbol symbol():获取这个方法的方法符号

类型树TypeTree

父类:Tree

把节点的类型用树的方式存储。相比起类型,类型树包含了更丰富的信息。

貌似无法正确解析文件中新定义的类型,例如class A {}中使用this.method()this是新定义的A类,会解析成unknown的类型树(即)。

  • Type symbolType():获取类型树代表的类型

  • List<AnnotationTree> annotations():获取类型树的注解,注解以一列注解树的方式列出。

    不知为何,这个方法不能获取注解类的注解。例如自定义注解时需要使用的@Target@Retention等等,在检查自定义注解的时候是无法获取到的。

数组类型树ArrayTypeTree

父类:ExpressionTreeTypeTree

代表数组类型。注意方法参数中的可变参数也算作是数组类型。

  • TypeTree type():获取数组的类型树

  • @Nullable SyntaxToken openBracketToken():获取左中括号的语法token

  • @Nullable SyntaxToken closeBracketToken():获取右中括号的语法token

  • @Nullable SyntaxToken ellipsisToken():如果这个数组类型是可变参数,获取省略号的语法token

标识符树IdentifierTree

父类:ExpressionTreeTypeTree

  • SyntaxToken identifierToken():获取标识符的语法token

  • String name():标识符的名称。

  • Symbol symbol():标识符的符号

含参类型树ParameterizedTypeTree

父类:TypeTree

用于表示一个泛型。

原始类型树PrimitiveTypeTree

父类:ExpressionTreeTypeTree

  • SyntaxToken keyword():获取原始类型的语法token(例如intbyte等等)。

联合类型树UnionTypeTree

父类:TypeTree

UnionTypeJava 14中的新特性,表示一个联合类型,与TypeScript中的联合类型类似。详见UnionType文档

与TypeScript的联合类型不同的是,Java中的UnionType的使用范围非常局限,只能用于异常的catch语句中。通常来说,当需要捕获多个异常时,我们需要写多个catch语句,即使对这些异常的处理方式是一样的。而使用UnionType可以将多个异常的捕获写在一个catch语句中,使得代码看起来更加精简。注意UnionType中列出的异常类型必须是互不相交的,即不能有继承关系。

例:

public class MultiCatchExample {
  public static void main(String[] args) {
    try {
      int[] arr = {1, 2, 3};
      System.out.println(arr[5]);
    } catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
      System.out.println("Exception caught: " + e);
    }
  }
}

在上面的例子中,我们同时捕获了ArrayIndexOutOfBoundsExceptionNullPointerException,并且用同样的方式处理了这两个异常。

  • ListTree<TypeTree> typeAlternatives():获取联合类型中的所有类型,以列表树的方式列出。

var类型树VarTypeTree

父类:TypeTree

var关键字是Java 10中的新特性。当声明一个局部变量时,我们需要会指定它的类型。而在Java 10中,我们可以把局部变量的类型声明为var,让Java自己根据上下文去猜测这个变量的类型。详见Java 10更新描述

  • SyntaxToken varToken():获取var关键字的语法token

通配符树WildcardTree

父类:TypeTree

用于检测泛型中的通配符。

  • List<AnnotationTree> annotations():获取泛型的注解,注解以一列注解树的方式列出。

  • SyntaxToken queryToken():获取通配符?语法token

  • @Nullable SyntaxToken extendsOrSuperToken():获取extendssuper语法token

  • @Nullable TypeTree bound():获取通配符的边界的类型树

列表树ListTree

父类:TreeList

ListTree既能当作Tree使用,也能当作List使用。

  • List<SyntaxToken> separators():获取列表中的分隔符,分隔符以一列语法token的方式列出。

    例如ListTree中有abc三个元素,那么separators()返回的就是ab之间的分隔符以及bc之间的分隔符。

多修饰符树ModifiersTree

父类:ListTree(元素类型为ModifierTree

表示一系列的修饰符。

ModifiersTreeListTree的子类,可以直接当作一个包含ModifierTreeList来使用。

  • List<AnnotationTree> annotations():获取列表中的注解,注解以一列注解树的方式列出。

  • List<ModifierKeywordTree> modifiers():获取列表中的修饰符,修饰符以一列修饰符关键字树的方式列出。

    用这个方法和直接把ModifiersTree当作List使用的区别是List中元素的类型。这个方法返回的是ModifierKeywordTree,而直接当作List返回的则是ModifierTree

参数列表Arguments

父类:ListTree(元素类型为ExpressionTree

获取一系列参数的值。

  • @Nullable SyntaxToken openParenToken():获取左括号的语法token

  • @Nullable SyntaxToken closeParenToken():获取右括号的语法token

泛型参数值列表TypeArguments

父类:ListTree(元素类型为Tree

获取一系列泛型参数的值。元素类型实际上是表达式树,比如IdentifierTreeMemberSelectExpressionTree

TypeArgumentsTypeParameters的区别在于,TypeArguments中的泛型参数是具体的类型(即泛型参数值),而TypeParameters中的泛型参数是占位符的形式。

例:

public class Box<T extends Number> {
    private T value;

    public Box(T value) {
        this.value = value;
    }
}

public class TypeArgumentsVsTypeParameters {
  public static void main(String[] args) {
      Box<Integer> stringBox = new Box<>(123);
  }
}

这个例子中,Box<Integer>中的Integer是泛型参数值,属于TypeArguments,而Box<T extends Number>中的T extends Number是泛型参数,属于TypeParameters

  • SyntaxToken openBracketToken():获取泛型参数列表的左尖括号的语法token

  • SyntaxToken closeBracketToken():获取泛型参数列表的右尖括号的语法token

泛型参数列表TypeParameters

父类:ListTree(元素类型为TypeParameterTree

TypeParametersTypeArguments的区别见TypeArguments下的说明。

  • @Nullable SyntaxToken openBracketToken():获取泛型参数列表的左尖括号的语法token

  • @Nullable SyntaxToken closeBracketToken():获取泛型参数列表的右尖括号的语法token

模组名称树ModuleNameTree

父类:ListTree(元素类型为IdentifierTree

相比父类没有任何新增的方法。

关于模组的更多信息见ModuleDirectiveTree

修饰符树ModifierTree

父类:Tree

相比它的父类Tree没有任何新加的方法。

修饰符关键字树ModifierKeywordTree

父类:ModifierTree

  • Modifier modifier():获取修饰符的类型。

    Modifier是一个枚举类,包含了所有修饰符的类型。

  • SyntaxToken keyword():获取修饰符的语法token

泛型参数树TypeParameterTree

父类:Tree

泛型参数是用来表示泛型的占位符,例如MyClass<T extends Number>中的T extends Number

  • IdentifierTree identifier():获取泛型参数的标识符树

  • @Nullable SyntaxToken extendsToken():获取extends关键字的语法token

  • ListTree<Tree> bounds():获取泛型参数的边界,边界以列表树的方式列出。

模组命令树ModuleDirectiveTree

父类:Tree

模组moduleJava 9中的新特性,用于模块化Java程序。详见Java 9模组说明文章第三方教程

构建模组的文件结构如下:

模组名
├─module-info.java
├─package1
├─package2
└─...

module-info.java中需要声明这个模组以及它的依赖关系。module-info.java的内容如下:

module com.example.mymodule {
  // 模组导出的包,只有这里导出的包才能被别的模组使用
  exports com.example.package1;
  exports com.example.package2;
  exports ...

  // 模组的依赖
  // static表示这个依赖只在编译时需要,运行时不需要
  // transitive表示如果别的模组导入了这个模组(mymodule),这个依赖(dependency1)也会被别的模组作为依赖导入
  requires [static | transitive] com.example.dependency1;
  requires com.example.dependency2.service;
  requires com.example.dependency3.implementedservice;

  // 如果需要实现一个服务模组的接口
  provides com.example.dependency2.service.ServiceInterface with
    com.example.service.ServiceInterfaceImpl
   
  // 如果需要使用一个已经实现的服务模组
  uses com.example.dependency3.implementedservice.ServiceInterface

  // 如果需要用反射获取一个包中的所有成员,包括private成员
  opens com.example.dependency1;
}
  • SyntaxToken directiveKeyword():获取命令的语法token

  • SyntaxToken semicolonToken():获取分号的语法token

RequiresDirectiveTree

用于指定一个依赖的模组。

父类:ModuleDirectiveTree

  • ModifiersTree modifiers():获取require命令的多修饰符树

  • ModuleNameTree moduleName():获取require命令后的所有模组,以模组名称树的方式列出。

ExportsDirectiveTree

父类:ModuleDirectiveTree

用于指定一个包作为模组的导出项。

  • ExpressionTree packageName():获取exports命令后的包的表达式树

  • @Nullable SyntaxToken toKeyword():获取exports命令后的to关键字的语法token

    to关键字可以指定这个包只能被哪些模组使用。如果没有指定to关键字,那么这个包就可以被所有模组使用。

  • ListTree<ModuleNameTree> moduleNames():获取exports命令中to关键字后的所有模组,以列表树的方式列出,元素类型是模组名称树

OpensDirectiveTree

父类:ModuleDirectiveTree

用于指定一个包作为模组的开放项,这个包里的所有成员都可以在这个模组中用反射获取。在Java 8和更早版本中,反射总是能获取一个包中的所有成员,包括private成员。但是在Java 9和更高版本中,反射默认只能获取除private以外的成员,如果想要获取private成员,就需要使用opens命令。

  • ExpressionTree packageName():获取opens命令后的包的表达式树

  • @Nullable SyntaxToken toKeyword():获取opens命令后的to关键字的语法token

    to关键字可以指定这个包只能被哪些模组使用。如果没有指定to关键字,那么这个包就可以被所有模组使用。

  • ListTree<ModuleNameTree> moduleNames():获取opens命令中to关键字后的所有模组,以列表树的方式列出,元素类型是模组名称树

UsesDirectiveTree

父类:ModuleDirectiveTree

用于指定一个可供使用的服务模组接口。

  • TypeTree typeName():获取uses命令后的服务模组接口的类型树

ProvidesDirectiveTree

用于给一个服务模组接口提供一个实现。

父类:ModuleDirectiveTree

  • TypeTree typeName():获取provides命令后的服务模组接口的类型树

  • SyntaxToken withKeyword():获取provides命令后的with关键字的语法token

  • ListTree<TypeTree> typeNames():获取provides命令中with关键字后的所有服务模组接口的实现类,以列表树的方式列出,元素类型是类型树

编译单元树CompilationUnitTree

父类:Tree

编译单元是指一个Java源文件,例如MyClass.java,编译单元树是指这个源文件的语法树。编译单元树一般是所有树的最终父节点。

  • @Nullable PackageDeclarationTree packageDeclaration():获取编译单元中的包声明,以包声明树的方式获取。

  • List<ImportClauseTree> imports():获取编译单元中的所有导入语句,以一列导入项树的方式列出。

  • List<Tree> types():获取编译单元中的所有类型声明(比如类和注解的定义,即在这个文件中新建的类和注解),以一列的方式列出。

  • @Nullable ModuleDeclarationTree moduleDeclaration():获取编译单元中的模组声明,以模组声明树的方式获取。

  • SyntaxToken eofToken():获取编译单元的结束符的语法token

导入项树ImportClauseTree

父类:Tree

与父类相比没有新增的方法。

有时能类型转换成ImportTree

导入树ImportTree

父类:ImportClauseTree

注意在导入树中是无法识别导入的类的类型符号的。

  • boolean isStatic():判断这个导入项是否是静态导入。

  • SyntaxToken importKeyword():获取import关键字的语法token

  • @Nullable SyntaxToken staticKeyword():如果这个导入项是静态导入,获取static关键字的语法token

  • Tree qualifiedIdentifier():获取导入的包。

    这个地方的Tree可以转换为MemberSelectExpressionTree或者IdentifierTree

  • SyntaxToken semicolonToken():获取分号的语法token

模组声明树ModuleDeclarationTree

父类:Tree

关于模组的更多信息见ModuleDirectiveTree

  • List<AnnotationTree> annotations():获取模组声明的注解,注解以一列注解树的方式列出。

    注意:模组声明的注解只能是@Deprecated

  • @Nullable SyntaxToken openKeyword():获取open关键字的语法token

  • SyntaxToken moduleKeyword():获取module关键字的语法token

  • ModuleNameTree moduleName():获取模组名的模组名称树

  • SyntaxToken openBraceToken():获取左大括号的语法token

  • List<ModuleDirectiveTree> directives():获取模组中的所有命令,以一列模组命令树的方式列出。

  • SyntaxToken closeBraceToken():获取右大括号的语法token

包声明树PackageDeclarationTree

父类:Tree

  • List<AnnotationTree> annotations():获取包声明的注解,注解以一列注解树的方式列出。

    注意:包声明的注解只能是@Deprecated

  • SyntaxToken packageKeyword():获取package关键字的语法token

  • ExpressionTree packageName():获取包名的表达式树

  • SyntaxToken semicolonToken():获取分号的语法token

Switch树SwitchTree

父类:Tree

  • SyntaxToken switchKeyword():获取switch关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • ExpressionTree expression():获取switch语句的条件表达式

  • SyntaxToken closeParenToken():获取右括号的语法token

  • SyntaxToken openBraceToken():获取左大括号的语法token

  • List<CaseGroupTree> cases():获取switch语句的case组,以一列Case组树的方式列出。

  • SyntaxToken closeBraceToken():获取右大括号的语法token

Case组树CaseGroupTree

父类:Tree

  • List<CaseLabelTree> labels():获取case组的标签,以一列Case标签树的方式列出。

  • List<StatementTree> body():获取case组的内容,以一列语句树的方式列出。

Case标签树CaseLabelTree

父类:Tree

  • SyntaxToken caseOrDefaultKeyword():获取casedefault关键字的语法token

  • boolean isFallThrough():判断这一个case是否会穿透到下一个case

  • List<ExpressionTree> expression():获取case标签的所有条件表达式

  • SyntaxToken colonOrArrowToken():获取冒号或箭头的语法token

    一般来说,switch中的case语句的条件后会跟一个冒号。在Java 12或更高版本中,这个冒号可以使用箭头->来代替。使用箭头的case在执行完成后会自动break,避免因为忘记写break而导致的穿透问题。详见Java 12博客Java 17更新文档

Catch树CatchTree

父类:Tree

  • SyntaxToken catchKeyword():获取catch关键字的语法token

  • SyntaxToken openParenToken():获取左括号的语法token

  • VariableTree parameter():获取catch语句的参数,以变量树的方式获取。

  • SyntaxToken closeParenToken():获取右括号的语法token

  • BlockTree block():获取catch语句的内容,以块树的方式获取。

Enum常量树EnumConstantTree

父类:Tree

代表一个enum中的一个常量。

  • ModifiersTree modifiers():获取enum常量的多修饰符树

    不知道有什么用,一个enum常量是没有修饰符的。

  • IdentifierTree simpleName():获取enum常量的标识符树

  • NewClassTree initializer():获取enum常量的初始化语句,以新建类对象树的方式获取。

  • @Nullable SyntaxToken separatorToken():获取enum常量后的分隔符的语法token

    enum常量之间的分隔符是逗号,最后一个常量后的分隔符是分号。

数组长度树ArrayDimensionTree

父类:Tree

代表一个数组在一个维度上的长度。例如new int[5][10]中的[5][10]分别是一个数组长度树。

  • List<AnnotationTree> annotations():获取数组长度的注解,注解以一列注解树的方式列出。

  • SyntaxToken openBracketToken():获取左方括号的语法token

  • @Nullable ExpressionTree expression():获取数组长度的表达式树

  • SyntaxToken closeBracketToken():获取右方括号的语法token

类型Type

父类:无

类型是把Java中的类型转换为语法树元素得来的,内置了多种判断类型的方法。

注意

  1. 此处的类型TypeTree.Kind中的种类不是同一个东西:此处的类型是Java中的类型,Tree.Kind中的种类是语法树中的种类。

  2. import语句中的类型是无法获取的。例如import java.util.ListList是无法获取的。

Type下有Type.Primitives枚举类,包含了所有Java的基本类型。

  • boolean is(String var1):判断类型是否是var1var1是另一个类型的名称的字符串

    对比类型时要用这个方法,而不能直接对类型用==equals

    例:myType.is("java.lang.String")

  • boolean is...():对类型进行判断,比如isClass()isPrimitive()isArray()等等。

  • boolean isSubtypeOf(String var1):判断类型是否是var1的子类型,var1是另一个类型的名称的字符串。

  • boolean isSubtypeOf(Type var1):判断类型是否是var1的子类型,var1是另一个类型。

  • String fullyQualifiedName():获取类型的全名,例如java.lang.String

  • String name():获取类型的名称。

  • Symbol.TypeSymbol symbol():获取类型的类型符号

  • Type erasure():获取类型的擦除类型。

  • boolean isParameterized():判断类型是否是参数化类型(即泛型)。

  • List<Type> typeArguments():获取类型的参数类型列表。

数组类型ArrayType

父类:Type

  • Type elementType():获取数组元素的类型。

符号Symbol

父类:无

符号表示一个方法、类、变量等等的签名信息。符号是SonarQube中的符号化(symbolic)API的核心,它可以“假装运行”代码来理清代码逻辑和流向控制,追踪变量的使用和方法的调用等等。

Symbol有一个类似的类LabelSymbol,三个子类MethodSymbolVariableSymbolTypeSymbol,和一个相关的类SymbolMetadata

  • String name():获取符号的名称。

  • @Nullable Symbol owner():获取符号的拥有者,即符号所在的类或方法的符号。

  • Type type():获取符号的类型

  • boolean is...():判断符号的类型,比如isMethodSymbol()isVariableSymbol()isTypeSymbol()等等;也可以判断符号的属性,比如isPublic()isPrivate()isStatic()等等。

  • SymbolMetadata metadata():获取符号的符号元数据,可以获取符号的注解等信息。

  • @Nullable TypeSymbol enclosingClass():如果符号是一个内部类、方法、或者变量,获取符号被定义时所在的类的类型符号

    似乎不能很好地检测import的类中的方法。

  • List<IdentifierTree> usages():获取符号的使用列表,为一个标识符列表。

    仅能检测同代码块中的使用情况,不能检测import语句中出现的类。

  • Tree declaration():符号的声明,在各子类中有对应重载的方法。

方法符号MethodSymbol

父类:Symbol

  • List<Type> parameterTypes():获取方法的参数类型列表。

  • TypeSymbol returnType():获取方法的返回值类型符号

  • List<Type> thrownTypes():获取方法抛出的异常中的类型列表。

  • List<MethodSymbol> overriddenSymbols():获取方法的重写方法符号列表。

  • String signature():获取方法的签名。

  • @Nullable MethodTree declaration():以方法树的形式,获取方法的声明。

变量符号VariableSymbol

父类:Symbol

  • @Nullable VariableTree declaration():以变量树的形式,获取变量的声明。

类型符号TypeSymbol

父类:Symbol

用于类(class)的符号。

  • @CheckForNull Type superClass():获取类的父类型

  • List<Type> interfaces():获取类实现的接口类型列表。

  • Collection<Symbol> memberSymbols():获取类的成员符号列表。

  • Collection<Symbol> lookupSymbols(String var1);:在类的成员中搜索名称为var1符号并返回匹配。

  • @Nullable ClassTree declaration():以类树的形式,获取类的声明。

标签符号LabelSymbol

父类:无

注意LabelSymbol不是Symbol的子类,而是Symbol的另一个类似类,所以它无法使用Symbol的方法。

  • String name():获取标签的名称。

  • List<IdentifierTree> usages():获取标签的使用,为一个标识符列表。

  • @Nullable LabeledStatementTree declaration():获取标签的声明。

符号元数据SymbolMetadata

父类:无

表示符号的元数据,用于获取符号的注解信息。注意AnnotationTree同样可以获取注解信息,这两者的区别见AnnotationTree下的说明。

SymbolMetadata中定义了两个接口,AnnotationInstanceAnnotationValue,分别用于获取单个注解和单个注解的值。

  • boolean isAnnotatedWith(String annotationName):判断符号是否被annotationName注解了。

  • @CheckForNull List<AnnotationValue> valuesForAnnotation(String annotationName):获取用于注解这个符号的,名称是annotationName的注解的参数。

  • List<AnnotationInstance> annotations():获取符号的所有注解实例

注解实例AnnotationInstance

父类:无

表示一个注解。

  • Symbol symbol():获取注解的符号

  • List<AnnotationValue> values():获取注解值列表。

注解值AnnotationValue

父类:无

表示一个注解值(包括key和value)。

  • String name():获取注解值的键值(key)。

  • Object value():获取注解值的值(value)。

语法tokenSyntaxToken

父类:Tree

一个SyntaxToken表示一个词语或者符号,比如returnString==+{}等等。使用token可以有效检查代码格式,比如判断是否有空格、是否有换行等等。token也是SonarJava中获取代码源文本以及文本位置的唯一方式。

  • String text():token的原文本。

  • int line():token所在的行数。行数是从1开始的。

  • int column():token所在的列数。列数是从0开始的。

  • List<SyntaxTrivia>:token的语法trivia

语法triviaSyntaxTrivia

父类:Tree

代表一个语法token之前的空格、注释等等对程序执行没有影响的部分。只能获取在这个语法token和上一个有意义的语法token之间的trivia。

似乎只能获取文本注释,不能把获取空格、换行等别的trivia。只能获取这个语法token之前的注释。

例:

// 注释一
public class MyClass {
  /* 注释二
      第二行
   */
  private void myMethod() {
    System.out.println("Hello World!");
  }
}

在以上的代码中,如果我需要获取注释一,那么我需要先找到MyClasspublic关键字的语法token,再调用trivias()方法获取所有public关键字之前的语法trivia,然后再找到注释一语法trivia

如果我需要获取注释二,那么我需要先找到myMethodprivate关键字的语法token并重复以上步骤。private语法token能获取的语法trivia仅限private之前,MyClass的左大括号{之后的部分,获取到的// 注释二文本会忽略前面的空格,但会带有注释用的//,而且column()方法计算列数时会算入前面的空格。

对于多行注释,第一行前面的空格会被忽略,后面所有行的空格会被保留。获取到的注释文本是:

/* 注释二
      第二行
   */

注释开始的行数是3,列数是2。

在测试时,// Noncompliant注释会被忽略。

  • String comment():获取注释的原文本。

  • int startLine():注释开始的行数。

  • int column():注释开始的列数。

Helper和Util类

源码见SonarJava仓库。

ExpressionsHelper

用于处理关于表达式树的一些操作。

  • static String concatenate(@Nullable ExpressionTree tree):获取一个表达式树的原文本。

MethodMatchers(在另一个包中)

用于检查一个文件中的方法是否是某个特定的方法。

用法:

  1. MethodMatchers matcher = MethodMatchers.create().设置类型.names(方法名).设置参数.build()来构造一个MethodMatchers对象并设置想要匹配的方法。

  2. matcher.match(tree)来检测tree代表的方法是否符合matcher中设置的方法。

Javadoc

获取一个类或者方法的Javadoc注释并对检查这个注释是否符合Javadoc规范。

自定义规则

以下的文件中,规则Java文件描述性HTML和JSON文件是必须的,测试文件规则示例文件可选但强烈推荐。

插件项目结构

SonarQube的Java自定义规则是通过插件来实现的。插件是一个完整的maven项目,项目结构如下:

插件名
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── package路径  // 如com.example.java
    │   │       ├── checks  // 放入规则文件
    │   │       │   ├── 规则Java文件1.java
    │   │       │   ├── 规则Java文件2.java
    │   │       │   └── ...
    │   │       ├── MyJavaFileCheckRegistrar.java
    │   │       ├── MyJavaRulesDefinition.java
    │   │       ├── MyJavaRulesPlugin.java
    │   │       ├── package-info.java
    │   │       └── RulesList.java
    │   └── resources
    │       └── org.sonar.l10n.java.rules.java  // 这个路径是写死的,不要改动
    │           ├── 规则Java文件1.html
    │           ├── 规则Java文件1.json
    │           ├── 规则Java文件2.html
    │           ├── 规则Java文件2.json
    │           └── ...  // 每一个规则都有一个html和json文件
    └── test  // 可选
        ├── files
        │   ├── 规则示例文件1.java
        │   ├── 规则示例文件2.java
        │   └── ...
        └── java
            └── package路径  // 如com.example.java
                ├── checks  // 放入测试文件
                │   ├── 规则Java文件1Test.java
                │   ├── 规则Java文件2Test.java
                │   └── ...
                ├── MyJavaFileCheckRegistrarTest.java
                ├── MyJavaRulesDefinitionTest.java
                └── MyJavaRulesPluginTest.java

每新增一个规则,都需要在/src/main/java/package路径/RulesList.javagetJavaChecks()方法或getJavaTestChecks()方法中添加这个规则的类。如果有测试,则同时需要在/src/test/java/package路径/MyJavaFileCheckRegistrarTest.javacheckNumberRules()方法中修改规则的总数量。

完成规则构建后,用mvn clean package命令打包,把生成的jar包放到SonarQube的$SONAR_HOME/extensions/plugins目录下,重启SonarQube即可。

规则Java文件

IssuableSubscriptionVisitor

如果只需要检测少量几种语法树,而且不同语法树之间互不影响,则可以用IssuableSubscriptionVisitor,这个类只需要实现visitNodenodesToVisit方法即可。每个类型在nodesToVisit()中列出的节点会被刚好遍历一次,没有被列出的节点也会被遍历但不会有任何操作。

package package路径.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Collections;
import java.util.List;

@Rule(key = "自定义规则编码,用下面的类名即可")
public class 自定义规则类 extends IssuableSubscriptionVisitor {

  // 需要检查哪些类型的语法树
  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Arrays.asList(语法树种类例如Tree.Kind.METHOD可输入多个);
  }

  // 对单个语法树进行检测(比如单个方法、类等等)
  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    // 我们知道这个语法树的类型,所以可以强制转换;如果有多个类型,可以用tree.is(Kind kind)或者instanceof判断
    MethodTree methodTree = (MethodTree) tree;
    // 做一系列的检查,如果检查到问题,就调用reportIssue方法
    if (methodTree.parameters().size() == 1) {
      Symbol.MethodSymbol methodSymbol = methodTree.symbol();
      Type firstParamType = methodSymbol.parameterTypes().get(0);
      Type returnType = methodSymbol.returnType().type();
      if (returnType.is(firstParamType.fullyQualifiedName())) {
        // 第一个参数tree是需要报错的节点
        this.reportIssue(tree, "Parameter type must be different from return type");
      }
    }
  }

  @Override
  public void leaveNode(Tree tree) {
    // 如果需要在检测完语法树后再做一些操作,可以在这里写
  }

  // 指定扫描文件的方式(一般不会重载这个方法,使用默认的方法即可)
  @Override
  public void scanFile(JavaFileScannerContext context) {
    // 自定义扫描方式
  }

}

BaseTreeVisitor

如果需要更复杂的检测,则可以用BaseTreeVisitorJavaFileScanner,这个类需要实现scanFile方法并按需重载一些检测语法树的方法。注意使用这个类报告问题时,reportIssue()是由JavaFileScannerContext调用的,而不是BaseTreeVisitor。每个节点会被刚好遍历一次。

package package路径.checks;

import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;

import java.util.List;

@Rule(key = "自定义规则编码")
public class 自定义规则类 extends BaseTreeVisitor implements JavaFileScanner {

  // 这个变量必须有
  private JavaFileScannerContext context;

  // 如果规则有一些属性,可以在这里定义
  @RuleProperty(defaultValue = ..., description = ...)
  protected ...;

  // 指定扫描文件的方式(必须有),一般就写如下的方法
  @Override
  public void scanFile(JavaFileScannerContext context) {
    this.context = context;
    this.scan(context.getTree());
  }

  // 检测方法的语法树
  @Override
  public void visitMethod(MethodTree tree) {
    // 对方法的语法树进行检测;在这里检测是前序遍历
    // 在检测过程中,如果检测到问题,就调用reportIssue方法
    // 第二个参数tree是需要报错的节点
    this.context.reportIssue(this, tree, "Avoid declaring methods (don't ask why)");

    // 检测完成后,调用super的方法,继续检测方法内部的语法树
    super.visitMethod(tree);

    // 如果需要先检测方法内部的语法树,再检测方法的语法树,则把对语法树的检测放在这里,实现后序遍历
  }

  // 检测类的语法树
  @Override
  public void visitClass(ClassTree tree) {
    // ...
  }

  // 还可以检测语句、变量、数组等语法树
  // 例如visitAssertStatement、visitWhileStatement、visitTypeCast、visitAnnotation等等
}

自定义visitor

IssuableSubscriptionVisitorBaseTreeVisitor很方便,很多时候只使用一个就够了,但以下两种情况中,仅使用一种visitor是比较困难的。

  1. 节点被遍历的顺序是由IssuableSubscriptionVisitorBaseTreeVisitor自己决定的,但有时我们需要自定义的遍历顺序。

  2. IssuableSubscriptionVisitor中列出的节点或在BaseTreeVisitor中方法被重载的节点会被在整个文件中搜索并执行判断和操作,但有时我们仅仅想对部分满足条件的节点进行操作,比如仅检查方法的注解,仅检查while里面的语句等等。

这个时候就需要结合这两个类使用多个visitor。

public class 复杂规则 extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Arrays.asList(...);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    // ...
    // 手动放入visitor来自定义节点访问顺序,visitor只能检查childTree节点和它的所有子节点
    childTree.accept(new InnerVisitor());
    // ...
  }

  // 自定义内部visitor
  private class InnerVisitor extends BaseTreeVisitor {
    @Override
    public void visit...(...) {
      // ...
    }
  }
}

通过Treeaccept()方法,我们可以手动地放入一个visitor来立刻开始遍历这个树。这个visitor只能检查accept()方法中放入的节点和它的所有子节点,不能检查其他节点。这个visitor可以是IssuableSubscriptionVisitorBaseTreeVisitor的子类。visitNode()accept()后面的代码会等待遍历结束再执行。

有用的一些helper方法

拿取当前成员访问表达式的源文本
import org.sonar.java.checks.helpers.ExpressionsHelper;

String sourceCode = ExpressionsHelper.concatenate((ExpressionTree) tree);
拿取当前成员访问表达式的类型
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.*;

public Type getType(ExpressionTree expressionTree) {
  if (expressionTree.is(Tree.Kind.IDENTIFIER)) {
    IdentifierTree identifierTree = (IdentifierTree) expressionTree;
    Symbol symbol = identifierTree.symbol();
    return symbol.type();
  } else if (expressionTree.is(Tree.Kind.MEMBER_SELECT)) {
    MemberSelectExpressionTree memberSelect = (MemberSelectExpressionTree) expressionTree;
    return this.getType(memberSelect.expression());
  }
  return null;
}

描述性HTML和JSON文件

HTML

HTML文件是这个规则在SonarQube管理页面中展示的内容。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>规则名称</title>
  </head>
  <body>
    <p>规则描述</p>

    <h2>Noncompliant Code Example</h2>
    <pre>
    给一个不符合规则的代码的例子
    </pre>

    <h2>Compliant Solution</h2>
    <pre>
    给一个符合规则的代码的例子
    </pre>
  </body>
</html>

JSON

JSON文件描述这个规则的属性,比如类型、标签、严重性等等。

{
  "title": 规则标题或名称,
  "type": 规则类型,可选"BUG"、"VULNERABILITY"、"CODE_SMELL"、"SECURITY_HOTSPOT",
  "status": 规则状态,可选"ready"、"beta"、"deprecated"、"removed"、"experimental"、"external"、"test"、"changed",
  "remediation": {
    "func": 修复类型,
    "constantCost": 预计修复耗时
  },
  "tags": [
    规则标签
  ],
  "defaultSeverity": 严重性,可选"Minor"、"Major"、"Critical"、"Blocker",
}

规则示例文件(可选,若写测试则必需)

一个简单的示例文件,用于测试规则,里面会包含符合规则的代码和不符合规则的代码。

对于不符合规则的代码,需要在不符合规则的地方用注释// Noncompliant标记出来。这个注释必须跟随在代码之后。如果有多个地方不符合规则,则在每个地方都要标记。一个示例文件中至少要有一处不符合规则的代码。

符合规则的代码则不需要标记。

class 随便取类名 {
  int     foo3(int value) { return 0; } // Noncompliant {{解释不符合规则的原因(可选)}}
  Object  foo4(int value) { return null; }
  MyClass foo5(MyClass value) {return null; } // Noncompliant
  int     foo6(int value, String name) { return 0; }
  int     foo7(int ... values) { return 0;}
}

如果错误地标记了行,则测试时会报错java.lang.AssertionError

示例文件中的import语句只能导入Java自带的包,不能导入第三方包,否则SonarJava会无法识别。

测试文件(可选)

一个测试文件可以测试多个规则示例文件

package package路径.checks;

import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;

public class 测试文件名 {

  @Test
  void 测试方法名() {
    CheckVerifier.newVerifier()
      .onFile("src/test/files/规则示例文件.java")
      .withCheck(new 规则类())
      .verifyIssues();
  }

  @Test
  void 测试方法名2() {
    规则类 rule = new 规则类();
    // 可以设置规则的属性
    rule.name = ...;

    CheckVerifier.newVerifier()
      .onFile("src/test/files/规则示例文件.java")
      .withCheck(rule)
      .verifyIssues();
  }

}

SonarQube自定义规则示例

以下的规则仅做示例用,规则本身不一定有实际用途。

[Java开发手册] 1.1.1 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

由于命名规范多用于类、方法和变量的命名,这里考虑遍历这三者的语法树。这里需要遍历多种语法树,但语法树之间互不影响,所以BaseTreeVisitorIssuableSubscriptionVisitor都可以用。

使用BaseTreeVisitor的规则文件:

使用到的类和方法:

package org.sonar.samples.java.checks;

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.*;

import java.util.Objects;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends BaseTreeVisitor implements JavaFileScanner {

  private JavaFileScannerContext context;

  @Override
  public void scanFile(JavaFileScannerContext context) {
    this.context = context;
    this.scan(context.getTree());
  }

  @Override
  public void visitClass(ClassTree tree) {
    String className = Objects.requireNonNull(tree.simpleName()).name();
    this.checkIssue(tree, className);
    super.visitClass(tree);
  }

  @Override
  public void visitMethod(MethodTree tree) {
    String methodName = Objects.requireNonNull(tree.simpleName().name());
    this.checkIssue(tree, methodName);
    super.visitMethod(tree);
  }

  @Override
  public void visitVariable(VariableTree tree) {
    String variableName = Objects.requireNonNull(tree.simpleName().name());
    this.checkIssue(tree, variableName);
    super.visitVariable(tree);
  }
  
  private void checkIssue(Tree tree, String name) {
    if (name.startsWith("_") || name.startsWith("$") || name.endsWith("_") || name.endsWith("$")) {
      this.context.reportIssue(
        this,
        tree,
        String.format("Variable identifier %s starts or ends with underscore or dollar sign", name)
      );
    }
  }

}

使用IssuableSubscriptionVisitor的规则文件:

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Arrays.asList(Tree.Kind.CLASS, Tree.Kind.METHOD, Tree.Kind.VARIABLE);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    String name;
    if (tree instanceof ClassTree) {
      ClassTree classTree = (ClassTree) tree;
      name = Objects.requireNonNull(classTree.simpleName()).name();

    } else if (tree instanceof MethodTree) {
      MethodTree methodTree = (MethodTree) tree;
      name = Objects.requireNonNull(methodTree.simpleName()).name();
    } else {
      VariableTree variableTree = (VariableTree) tree;
      name = Objects.requireNonNull(variableTree.simpleName()).name();
    }
    if (name.startsWith("_") || name.startsWith("$") || name.endsWith("_") || name.endsWith("$")) {
      this.context.reportIssue(
        this,
        tree,
        String.format("Identifier %s starts or ends with underscore or dollar sign", name)
      );
    }
  }
}

[Java开发手册] 1.1.6 【强制】常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

检测单词间是否用下划线隔开比较困难,需要识别单个单词,这里只检测常量(即final修饰的变量)命名是否全部大写。由于只检测变量,这里选择使用IssuableSubscriptionVisitor

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Collections.singletonList(Tree.Kind.VARIABLE);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    VariableTree variableTree = (VariableTree) tree;
    String name = Objects.requireNonNull(variableTree.simpleName()).name();
    List<Modifier> modifierTrees = variableTree.modifiers()
      .modifiers()
      .stream()
      .map(ModifierKeywordTree::modifier)
      .collect(Collectors.toList());
    if (modifierTrees.contains(Modifier.FINAL) && !name.toUpperCase().equals(name)) {
      this.context.reportIssue(
        this,
        tree,
        String.format("Final variable %s's identifier must be in upper case", name)
      );
    }

  }
}

[Java开发手册] 1.1.11 【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。

由于只遍历类树,这里选择使用IssuableSubscriptionVisitor

注意SonarJava并不支持查找类的子类,查找父类时也仅支持取得父类的类型(即类名),所以需要自己实现子类的父类的对应关系。这里选择使用两个MapCLASS_FIELDS存储一个类的名字和它的所有类变量名,PARENT_TO_CHILDREN存储一个类的名字和它的所有子类的名字。

如果需要测试这个规则,在规则示例中,// Noncompliant注解需要写在类定义的后面,因为这里是在类树中reportIssue()

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;
import java.util.stream.Collectors;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  private final Map<String, Set<String>> CLASS_FIELDS;

  private final Map<String, List<String>> PARENT_TO_CHILDREN;

  public JavaDevRuleCheck() {
    super();
    this.CLASS_FIELDS = new HashMap<>();
    this.PARENT_TO_CHILDREN = new HashMap<>();
  }

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Collections.singletonList(Tree.Kind.CLASS);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    ClassTree classTree = (ClassTree) tree;
    // 取得所有的类变量
    Set<String> fields = classTree.members()
      .stream()
      .filter(member -> member.is(Tree.Kind.VARIABLE))
      .map(member -> {
        VariableTree variableTree = (VariableTree) member;
        return variableTree.simpleName().name();
      })
      .collect(Collectors.toSet());
    // 取得类名
    String className = classTree.symbol().name();
    // 把类名和类变量的映射放入classFields
    this.CLASS_FIELDS.put(className, fields);
    // 如果这个类有子类,检查是否与任何一个子类有重复的变量
    // 如果parentToChild中有这个类,说明至少一个子类是被检查过了的
    if (this.PARENT_TO_CHILDREN.containsKey(className)) {
      for (String childClassName: this.PARENT_TO_CHILDREN.get(className)) {
        Set<String> childFields = this.CLASS_FIELDS.get(childClassName);
        Set<String> commonFields = new HashSet<>(fields);
        commonFields.retainAll(childFields);
        if (!commonFields.isEmpty()) {
          this.context.reportIssue(
            this,
            classTree,
            String.format("Parent class %s and child class %s contain duplicate fields", className, childClassName)
          );
        }
      }
    }
    // 如果这个类有父类,把它的父类和它的关系存入parentToChild
    if (classTree.superClass() != null) {
      String parentClassName = classTree.superClass().symbolType().name();
      if (this.PARENT_TO_CHILDREN.containsKey(parentClassName)) {
        this.PARENT_TO_CHILDREN.get(parentClassName).add(className);
      } else {
        List<String> children = new ArrayList<>();
        children.add(className);
        this.PARENT_TO_CHILDREN.put(parentClassName, children);
      }
      // 如果父类在classFields里面,则检查是否和父类有重复的变量
      Set<String> parentFields = this.CLASS_FIELDS.get(parentClassName);
      Set<String> commonFields = new HashSet<>(fields);
      commonFields.retainAll(parentFields);
      if (!commonFields.isEmpty()) {
        this.context.reportIssue(
          this,
          classTree,
          String.format("Parent class %s and child class %s contain duplicate fields", parentClassName, className)
        );
      }
    }
  }
}

[Java开发手册] 1.2.2 【强制】long 或 Long 赋值时,数值后使用大写 L,不能是小写 l,小写容易跟数字混淆,造成误解。

因为我们只关心long类型的字面量,所以这里选择使用IssuableSubscriptionVisitor,并把nodesToVisit()中的类型列表设为Tree.Kind.LONG_LITERAL。这样只会对long类型的字面量进行检测。

如果使用BaseTreeVisitor,则需要自行在visitLiteral()方法中判断字面量的类型,如果是long类型,则进行检测。

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Collections.singletonList(Tree.Kind.LONG_LITERAL);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    LiteralTree literalTree = (LiteralTree) tree;
    if (literalTree.value().endsWith("l")) {
      this.reportIssue(tree, "Long literal should end with \"L\", not \"l\"");
    }
  }
}

[Java开发手册] 1.3.2 【强制】左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格。

此处仅检查小括号的使用,对大括号的检查是类似的。

这个规则比较麻烦。Java中的括号会出现在不同地方,比如while语句、for循环、数学运算表达式等等。为了让SonarQube能够检测所有的这些情况,我们需要列出所有小括号可能出现的地方,然后在这些地方检测小括号的使用。因为需要检测多种语句,所以这里选择使用BaseTreeVisitor

规则的核心思想是取得左右小括号的语法token,再取得括号中的语句的第一个和最后一个语法token,然后比较这些token的位置。

由于完整的规则文件过长,这里仅展示部分方法,其它方法的构造是类似的。

使用到的类和方法:

  • BaseTreeVisitorvisitLambdaExpression()visitParenthesized()visitTypeCast()visitDoWhileStatement()visitForEachStatement()visitForStatement()visitIfStatement()visitSynchronizedStatement()visitTryStatement()

  • SyntaxTokencolumn()identifierToken()

  • TreefirstToken()lastToken()

  • LambdaExpressionTreeopenParenToken()closeParenToken()parameters()

  • VariableTreesimpleName()

  • ParenthesizedTreeopenParenToken()closeParenTokenexpression()

  • TypeCastTreeopenParenToken()closeParenToken()type()

  • DoWhileStatementTreeopenParenToken()closeParenToken()condition()

  • ForEachStatementopenParenToken()closeParenToken()variable()expression()

  • ForStatementTreeopenParenToken()closeParenToken()initializer()update()

  • IfStatementTreeopenParenToken()closeParenToken()condition()

  • SynchronizedStatementTreeopenParenToken()closeParenToken()expression()

  • TryStatementTreeopenParenToken()closeParenToken()resourceList()

package org.sonar.samples.java.checks;

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.*;

import java.util.List;
import java.util.Objects;

@Rule(key = "JavaDevRuleCheckBaseVisitor")
public class JavaDevRuleCheckBaseVisitor extends BaseTreeVisitor implements JavaFileScanner {

  private JavaFileScannerContext context;

  @Override
  public void scanFile(JavaFileScannerContext context) {
    this.context = context;
    this.scan(context.getTree());
  }

  @Override
  public void visitLambdaExpression(LambdaExpressionTree tree) {
    if (tree.openParenToken() != null) {
      int openParenCol = Objects.requireNonNull(tree.openParenToken()).column();
      int closeParenCol = Objects.requireNonNull(tree.closeParenToken()).column();
      List<VariableTree> parameters = tree.parameters();
      if (parameters.isEmpty()) {
        if (openParenCol + 1 != closeParenCol) {
          this.context.reportIssue(this, tree, "No space inside empty parentheses");
        }
      } else {
        SyntaxToken firstVarToken = parameters.get(0).simpleName().identifierToken();
        SyntaxToken lastVarToken = parameters.get(parameters.size()-1).simpleName().identifierToken();
        this.reportIssue(firstVarToken, lastVarToken, openParenCol, closeParenCol, tree);
      }
    }
    super.visitLambdaExpression(tree);
  }

  @Override
  public void visitParenthesized(ParenthesizedTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.expression().firstToken();
    SyntaxToken lastToken = tree.expression().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitParenthesized(tree);
  }

  @Override
  public void visitTypeCast(TypeCastTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.type().firstToken();
    SyntaxToken lastToken = tree.type().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitTypeCast(tree);
  }

  @Override
  public void visitDoWhileStatement(DoWhileStatementTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.condition().firstToken();
    SyntaxToken lastToken = tree.condition().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitDoWhileStatement(tree);
  }

  @Override
  public void visitForEachStatement(ForEachStatement tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.variable().firstToken();
    SyntaxToken lastToken = tree.expression().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitForEachStatement(tree);
  }

  @Override
  public void visitForStatement(ForStatementTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.initializer().firstToken();
    SyntaxToken lastToken = tree.update().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitForStatement(tree);
  }

  @Override
  public void visitIfStatement(IfStatementTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.condition().firstToken();
    SyntaxToken lastToken = tree.condition().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitIfStatement(tree);
  }

  @Override
  public void visitSynchronizedStatement(SynchronizedStatementTree tree) {
    int openParenCol = tree.openParenToken().column();
    int closeParenCol = tree.closeParenToken().column();
    SyntaxToken firstToken = tree.expression().firstToken();
    SyntaxToken lastToken = tree.expression().lastToken();
    this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    super.visitSynchronizedStatement(tree);
  }

  @Override
  public void visitTryStatement(TryStatementTree tree) {
    if (tree.openParenToken() != null) {
      int openParenCol = Objects.requireNonNull(tree.openParenToken()).column();
      int closeParenCol = Objects.requireNonNull(tree.closeParenToken()).column();
      SyntaxToken firstToken = tree.resourceList().firstToken();
      SyntaxToken lastToken = tree.resourceList().lastToken();
      this.reportIssue(firstToken, lastToken, openParenCol, closeParenCol, tree);
    }
    super.visitTryStatement(tree);
  }

  private void reportIssue(SyntaxToken firstToken, SyntaxToken lastToken, int openParenCol, int closeParenCol, Tree tree) {
    if (firstToken != null && firstToken.column() > openParenCol + 1) {
      this.context.reportIssue(this, tree, "No space between open parenthesis and expression");
    } else if (lastToken != null && lastToken.column() + lastToken.text().length() < closeParenCol) {
      this.context.reportIssue(this, tree, "No space between closing parenthesis and expression");
    }
  }

}

for循环和for-each循环里不能嵌套if

由于ForStatementTreeForEachStatement都会把循环里的代码块当作一整个语句来处理(而不是当作代码块),我们需要自己定义一个树的遍历器去检查循环里的代码块的内容。

检查的逻辑是:只检查循环里的代码块,在循环里只要遇到if语句,不论在哪里遇到都报告错误。

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Arrays.asList(Tree.Kind.FOR_STATEMENT, Tree.Kind.FOR_EACH_STATEMENT);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    // ForStatementTree和ForEachStatement都有statement()方法,
    // 所以可以定义一个统一的StatementTree变量来代表循环里的代码块
    StatementTree codeBlock;
    // tree只能是ForStatementTree或者ForEachStatement,这里用is()判断种类
    if (tree.is(Tree.Kind.FOR_STATEMENT)) {
      ForStatementTree forTree = (ForStatementTree) tree;
      codeBlock = forTree.statement();
    } else {
      ForEachStatement forEachTree = (ForEachStatement) tree;
      codeBlock = forEachTree.statement();
    }
    // 手动放入visitor来自定义节点访问顺序,visitor只能检查codeBlock节点和它的所有子节点
    codeBlock.accept(new StatementVisitor());
  }

  // 自定义一个检查循环里的代码的visitor
  private class StatementVisitor extends BaseTreeVisitor {
    // 因为只会检查循环里的代码,所以只要遇到if语句就报告错误。循环外的if语句不会被检查
    @Override
    @ParametersAreNonnullByDefault
    public void visitIfStatement(IfStatementTree tree) {
      JavaDevRuleCheck.this.reportIssue(tree, "Should not nest if statement inside for loop");
      // (可选)如果if里有嵌套的语句,也要检查
      super.visitIfStatement(tree);
    }
  }
}

final修饰的变量不能被重新赋值。

由于我们是检查变量是否被final修饰以及是否被重新赋值,我们只会遍历变量树(可能还会遍历赋值表达式树)所以这里选择使用IssuableSubscriptionVisitor

检查的逻辑是:对于每一个final变量,检查它是否在声明时就已经被赋值了,如果没有,则允许一次赋值(allowedAssignments设为1),否则一次赋值都不允许(allowedAssignments设为0)。然后对于每一个使用了这个变量的地方,检查是否为赋值语句。如果final变量是被赋值的一方,那么赋值次数assignments就加一;如果assignments大于允许的赋值次数(allowedAssignments),则报告错误。

这里的诀窍是,如果这个final变量是被赋值的一方,**它的标识符的父节点一定是赋值表达式树。**如果是赋值给别的变量,则它的父节点会是别的树,比如变量树表达式树等。通过这个方法可以方便地检验final变量是否被重新赋值。

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Collections.singletonList(Tree.Kind.VARIABLE);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    VariableTree variableTree = (VariableTree) tree;
    // 取得变量的修饰符
    List<ModifierKeywordTree> modifiers = variableTree.modifiers().modifiers();
    for (ModifierKeywordTree modifier: modifiers) {
      // 如果变量的修饰符中有final
      if (modifier.modifier() == Modifier.FINAL) {
        // 先检查final变量是否在声明时就已经被赋值了
        // 如果没有,则应该允许第一次赋值
        int allowedAssignments = variableTree.initializer() == null ? 1 : 0;
        int assignments = 0;
        // 对每一个使用了这个变量的地方进行检查
        for (IdentifierTree identifierTree: variableTree.symbol().usages()) {
          // 如果这个变量被重新赋值了
          if (identifierTree.parent() instanceof AssignmentExpressionTree) {
            assignments ++;
            if (assignments > allowedAssignments) {
              this.reportIssue(identifierTree, "Cannot reassign final variables");
            }
          }
        }
        // 修饰符中只会有一个final,所以检查到final就可以停止了
        break;
      }
    }
  }
}

不能出现没有在文件中使用的import语句。

一个很直接的思路就是先找到所有的import语句中包含的类,再去检查文件其它地方是否使用了这些类。如果有没有被使用到的类,则说明这个import是多余的。

找到所有import语句有两种办法:遍历所有ImportTree,和使用CompilationUnitTreeimports()方法。单独使用时,以上两种方法是等价的。但现在我们的需求中包含一个先后顺序:先找到所有的import语句,再去检查文件其它地方。如果使用遍历ImportTree的办法,则无法控制这个先后顺序,语法树可能会同时遍历ImportTree和文件其它节点(语法树是有一个内置的遍历顺序的,但为何要去冒这个险呢)。

所以,这里我们选择使用CompilationUnitTreeCompilationUnitTree是整个文件的父节点,因此我们可以在这个节点上手动规定遍历的顺序,即先拿到所有的import语句,再进入节点进行遍历。

需要导入包的地方可能是变量类型、类实例、注解、使用枚举类、extendsimplements语句、泛型参数值,以及方法参数和返回值类型。总结起来我们需要检查这些地方:变量方法类实例化注解成员访问泛型参数值

这里没有检测静态导入和Javadoc中出现的类。

在以下的规则代码中,我们先通过CompilationUnitTreeimports方法取得所有的ImportTree并存入一个Map,再定义一个只检查新建类实例变量的类型的visitor来进入CompilationUnitTree节点。对每个检查到的类型,如果在Map中有对应的import的类,则从Map中删除这个类。Map中最后剩下的类就是没有被使用到的类。

这个例子中使用了SonarJava的一个工具类ExpressionsHelper来取得完整的import语句后的类名,源代码在这里

使用到的类和方法:

package org.sonar.samples.java.checks;

import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.*;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;

@Rule(key = "JavaDevRuleCheck")
public class JavaDevRuleCheck extends IssuableSubscriptionVisitor {

  // 用这个HashMap来存import的类和它对应的ImportTree,ImportTree用于寻找报错位置
  private final Map<String, ImportTree> imports = new HashMap<>();

  @Override
  public List<Tree.Kind> nodesToVisit() {
    // 需要从整个文件的树入手,所以选择CompilationUnitTree
    return Collections.singletonList(Tree.Kind.COMPILATION_UNIT);
  }

  @Override
  @ParametersAreNonnullByDefault
  public void visitNode(Tree tree) {
    CompilationUnitTree compilationUnitTree = (CompilationUnitTree) tree;
    // 取得所有的import语句树并加入imports中
    compilationUnitTree.imports()
      .stream()
      .filter(importTree -> importTree.is(Tree.Kind.IMPORT))
      .map(ImportTree.class::cast)
      .forEach(importTree -> {
        // 取得完整的import语句后的类名
        String importName = ExpressionsHelper.concatenate((ExpressionTree) importTree.qualifiedIdentifier());
        this.imports.put(importName, importTree);
      });
    tree.accept(new TypeVisitor());
  }

  // 在遍历完整个文件后,对每个没有使用到的import语句报错
  @Override
  @ParametersAreNonnullByDefault
  public void leaveNode(Tree tree) {
    this.imports.forEach((unusedImport, syntaxNode) -> {
      this.reportIssue(syntaxNode, "Remove unused import " + unusedImport);
    });
  }

  // 自定义一个visitor来检查变量的类
  private class TypeVisitor extends BaseTreeVisitor {
    // 检查新建类实例时使用的类
    @Override
    public void visitNewClass(NewClassTree tree) {
      String typeName = tree.symbolType().fullyQualifiedName();
      JavaDevRuleCheck.this.imports.remove(typeName);
      super.visitNewClass(tree);
    }

    // 检查所有变量的类型
    // 这里包括了方法参数里的变量
    @Override
    public void visitVariable(VariableTree tree) {
      String typeName = tree.type().symbolType().fullyQualifiedName();
      this.removeUsedImport(typeName);
      super.visitVariable(tree);
    }

    // 检查注解
    @Override
    public void visitAnnotation(AnnotationTree tree) {
      String typeName = tree.symbolType().fullyQualifiedName();
      this.removeUsedImport(typeName);
      super.visitAnnotation(tree);
    }

    // 检查方法的返回值的类
    @Override
    public void visitMethod(MethodTree tree) {
      TypeTree returnType = tree.returnType();
      if (returnType != null) {
        String typeName = tree.returnType().symbolType().fullyQualifiedName();
        this.removeUsedImport(typeName);
      }
      super.visitMethod(tree);
    }

    // 检查类的父类和接口
    @Override
    public void visitClass(ClassTree tree) {
      List<TypeTree> typeTrees = new ArrayList<>();
      if (tree.superClass() != null) {
        typeTrees.add(tree.superClass());
      }
      typeTrees.addAll(tree.superInterfaces());
      typeTrees.forEach(typeTree -> {
        this.removeUsedImport(typeTree.symbolType().fullyQualifiedName());
      });
      super.visitClass(tree);
    }

    // 检查所有变量的类
    @Override
    public void visitVariable(VariableTree tree) {
      String typeName = tree.type().symbolType().fullyQualifiedName();
      JavaDevRuleCheck.this.imports.remove(typeName);
      super.visitVariable(tree);
    }

    // 检查成员访问时成员的类
    @Override
    public void visitMemberSelectExpression(MemberSelectExpressionTree tree) {
      Type type = this.getType(tree.expression());
      String typeName = Objects.requireNonNull(type).fullyQualifiedName();
      this.removeUsedImport(typeName);
      super.visitMemberSelectExpression(tree);
    }

    // 检查泛型参数值
    @Override
    @ParametersAreNonnullByDefault
    public void visitTypeArguments(TypeArguments listTree) {
      listTree.forEach(tree -> {
        Type type = this.getType((ExpressionTree) tree);
        if (type != null) {
          this.removeUsedImport(type.fullyQualifiedName());
        }
      });
      super.visitTypeArguments(listTree);
    }

    // 从imports中删除已经使用的import
    private void removeUsedImport(String typeName) {
      JavaDevRuleCheck.this.imports.remove(typeName);
      if (typeName.contains(".")) {
        JavaDevRuleCheck.this.imports.remove(typeName.substring(0, typeName.lastIndexOf(".")));
      }
    }

    // 取得类型以便对照imports
    private Type getType(ExpressionTree expressionTree) {
      if (expressionTree.is(Tree.Kind.IDENTIFIER)) {
        // 用于单层成员访问,如Class.Member,取得Class的类型
        IdentifierTree identifierTree = (IdentifierTree) expressionTree;
        Symbol symbol = identifierTree.symbol();
        return symbol.type();
      } else if (expressionTree.is(Tree.Kind.MEMBER_SELECT)) {
        // 用于多层成员访问,如OuterClass.InnerClass.Member,取得OuterClass的类型
        MemberSelectExpressionTree memberSelect = (MemberSelectExpressionTree) expressionTree;
        return this.getType(memberSelect.expression());
      }
      return null;
    }
  }

}

About

A quick tutorial for writing custom Java rules using SonarJava plugin for SonarQube

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published