更新于2023-8-4
Sonar通过遍历语法树的形式来进行代码检查。
官方的sonar-java插件里已经内置了很多规则,可以在这里看到源码。
整个文件的语法树中的一个节点,表示一个语句、一个表达式、一个变量、一个方法、一个类等等。所有的树类型的组件都可以使用Tree
类的方法。
Tree
下有Tree.Kind
枚举类,包含了所有树节点的种类。注:后面会遇到类型Type
,和Tree.Kind
不是同一个东西。为做区分,将Tree.Kind
称为种类,将Type
称为类型。
Tree
可以大致分为表达式树ExpressionTree
、语句树StatementTree
、类型树TypeTree
、列表树ListTree
、模组命令树ModuleDirectiveTree
(Java 9)和其它树。
-
boolean is(Kind... var1)
:判断这个节点的种类,比如Tree.Kind.METHOD
、Tree.Kind.CLASS
等;一次可传入多个类型,如果有一个种类匹配则返回true
。 -
@Nullable Tree parent()
:获取父节点。 -
@Nullable SyntaxToken firstToken()
:获取这个节点的第一个SyntaxToken
。 -
@Nullable SyntaxToken lastToken()
:获取这个节点的最后一个SyntaxToken
。 -
Tree.Kind kind()
:获取这个节点的种类。这个方法很重要,因为它能精确地判断节点种类,即使这个节点的类型是
Tree
、ExpressionTree
等大类。通过判断种类,可以对树进行强制类型转换。
父类:Tree
ExpressionTree
有一系列不同的实现类,比如LiteralTree
、MemberSelectExpressionTree
、NewClassTree
、MethodInvocationTree
等等。
-
Type symbolType()
:表达式的值的类型。 -
Optional<Object> asConstant()
:获取表达式的值。 -
<T> Optional<T> asConstant(Class<T> var1)
:获取表达式的值,以var1
的类型返回。
父类:ExpressionTree
、ModifierTree
之后会遇到功能相似的SymbolMetadata
。AnnotationTree
和SymbolMetadata
的区别在于,AnnotationTree
的本质是树,可以以树的形式进行自定义的遍历(AnnotationTree
的父节点是ModifierTree
),而SymbolMetadata
可以比较方便地获得注解参数的key和value。
-
SyntaxToken atToken()
:获取注解@
字符的语法token。 -
TypeTree annotationType()
:获取注解的类型树。 -
Arguments arguments()
:获取注解的参数列表。
通过下标访问数组的元素的表达式,例如arr[1]
。
注意在变量声明时的赋值表达式不算入赋值表达式树中,而算入变量树中。例如int a = 1
中的a = 1
不算赋值表达式。
-
ExpressionTree variable()
:获取赋值的左边的表达式。这个表达式一定只含有变量名。
-
SyntaxToken operatorToken()
:获取赋值的运算符的语法token。 -
ExpressionTree expression()
:获取赋值的右边的表达式。
-
SyntaxToken operatorToken()
:获取运算符的语法token。 -
ExpressionTree expression()
:获取运算符后的表达式。比如对于int a = -1
,这个方法取到的是1
。
-
ExpressionTree leftOperand()
:获取左操作数的表达式。 -
SyntaxToken operatorToken()
:获取运算符的语法token。 -
ExpressionTree rightOperand()
:获取右操作数的表达式。
代表三元运算符?:
的表达式,即a ? b : c
。
-
ExpressionTree condition()
:获取条件的表达式。 -
SyntaxToken questionToken()
:获取问号的语法token。 -
ExpressionTree trueExpression()
:获取条件为真时的表达式。 -
SyntaxToken colonToken()
:获取冒号的语法token。 -
ExpressionTree falseExpression()
:获取条件为假时的表达式。
-
ExpressionTree expression()
:获取被判断的部分的表达式(即instanceof
之前的部分)。 -
SyntaxToken instanceofToken()
:获取instanceof
关键字的语法token。 -
TypeTree type()
:获取判断的类型树(即instanceof
之后的部分)。
-
@Nullable SyntaxToken openParenToken()
:获取左括号的语法token。 -
@Nullable SyntaxToken closeParenToken()
:获取右括号的语法token。 -
List<VariableTree> parameters()
:获取lambda表达式的参数列表,以一列变量树的方式列出。 -
SyntaxToken arrowToken()
:获取箭头的语法token。 -
Tree body()
:获取lambda表达式的主体。
-
String value()
:获取常量的值。 -
SyntaxToken token()
:获取常量的语法token。
通过.
访问成员的表达式,例如System.out.println()
、Arrays.asList()
、classInstance.field
。
注意如果有连续的.
,那么每个.
都会有一个MemberSelectExpressionTree
。例如java.lang.annotation.*
会被解析为三个MemberSelectExpressionTree
,分别是java.lang.annotation.*
、java.lang.annotation
和java.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
都会返回包含println
的IdentifierTree
。如果需要区分类型,可以使用IdentifierTree
的symbolType()
方法。
-
@Nullable TypeArguments typeArguments()
:获取方法的泛型参数值列表。 -
ExpressionTree methodSelect()
:获取调用方法的表达式。 -
Arguments arguments()
:获取调用方法时输入的参数列表。
-
Tree expression()
:获取方法引用的表达式。 -
SyntaxToken doubleColon()
:获取::
的语法token。 -
@Nullable TypeArguments typeArguments()
:获取方法的泛型参数值列表。 -
IdentifierTree method()
:获取被引用的方法的标识符树。
表示创建一个新的数组。
-
@Nullable TypeTree type()
:获取数组的类型树。 -
@Nullable SyntaxToken newKeyword()
:获取new
关键字的语法token。 -
List<ArrayDimensionTree> dimensions()
:获取数组的长度。 -
@Nullable SyntaxToken openBraceToken()
:获取左大括号的语法token。 -
ListTree<ExpressionTree> initializers()
:获取数组的初始值,以列表树的方式列出。 -
@Nullable SyntaxToken closeBraceToken()
:获取右大括号的语法token。
表示创建一个新的类对象。
-
@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
的符号。
一个被括号包起来的表达式,例如(1 + 2) * 3
中的(1 + 2)
。
注意仅限于表达式,方法调用、新建类对象、注解等传参使用的括号不包含在这个树中。
-
SyntaxToken openParenToken()
:获取左括号的语法token。 -
ExpressionTree expression()
:获取括号中的表达式。 -
SyntaxToken closeParenToken()
:获取右括号的语法token。
-
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()
:如果转换到的类型中含有泛型,获取这些泛型的边界,用列表树列出。
无别的方法。
父类:Tree
StatementTree
本身并没有用,也没有Tree
之外的方法。
它有一系列不同的子类,比如AssertStatementTree
、ReturnStatementTree
、IfStatementTree
,ForEachStatementTree
等等。
-
SyntaxToken assertKeyword()
:获取assert关键字的语法token。 -
ExpressionTree condition()
:获取assert语句的条件表达式。 -
@Nullable SyntaxToken colonToken()
:获取冒号的语法token。 -
@Nullable ExpressionTree detail()
:获取assert语句的详细信息,即冒号后面的表达式。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
-
SyntaxToken breakKeyword()
:获取break
关键字的语法token。 -
@Nullable IdentifierTree label()
:获取break
语句的标签。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
代表定义一个新类。
注意ClassTree
和NewClassTree
的区别。ClassTree
是定义一个新类,而NewClassTree
是创建一个已有的类的实例。
-
@Nullable SyntaxToken declarationKeyword()
:如果一个类不是匿名类,获取类定义时的class
或interface
关键字的语法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。
-
SyntaxToken continueKeyword()
:获取continue
关键字的语法token。 -
@Nullable IdentifierTree label()
:获取continue
语句的标签。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
-
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。
父类:StatementTree
、ImportClauseTree
SyntaxToken semicolonToken()
:获取分号的语法token。
用于表达只含有单个表达式的语句,例如a += 1
、System.out.println()
等等。
-
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
是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例。
-
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
是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例。
对于含有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
把大括号中(包括大括号本身)所有的语句都当作一个语句来处理。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例。
-
IdentifierTree label()
:获取标签的标识符树。 -
SyntaxToken colonToken()
:获取冒号的语法token。 -
StatementTree statement()
:获取标签后的语句。 -
Symbol.LabelSymbol symbol()
:获取标签的类型符号。
-
SyntaxToken returnKeyword()
:获取return
关键字的语法token。 -
@Nullable ExpressionTree expression()
:获取return
语句的返回表达式。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
本身没有方法,所用的方法全部来自于SwitchTree
。
仅限于synchronized
代码块,不包括synchronized
方法。
-
SyntaxToken synchronizedKeyword()
:获取synchronized
关键字的语法token。 -
SyntaxToken openParenToken()
:获取左括号的语法token。 -
ExpressionTree expression()
:获取synchronized
语句括号里的表达式。 -
SyntaxToken closeParenToken()
:获取右括号的语法token。 -
BlockTree block()
:获取synchronized
后的代码块树。
-
SyntaxToken throwKeyword()
:获取throw
关键字的语法token。 -
ExpressionTree expression()
:获取throw
后面语句的表达式。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
-
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
语句的代码块树。
-
SyntaxToken whileKeyword()
:获取while
关键字的语法token。 -
SyntaxToken openParenToken()
:获取左括号的语法token。 -
ExpressionTree condition()
:获取while
语句的条件表达式。 -
SyntaxToken closeParenToken()
:获取右括号的语法token。 -
StatementTree statement()
:获取while
语句后的代码语句。这里把循环里的所有代码(包括大括号)当作一整个语句来处理,实际上返回
BlockTree
是更合理的做法。我们依然可以通过自定义遍历器的方式访问代码块的内容,详见自定义规则范例。
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。
-
ModifiersTree modifiers()
:获取变量的多修饰符树。 -
TypeTree type()
:获取变量的类型树。 -
IdentifierTree simpleName()
:获取变量名的标识符树。 -
@Nullable SyntaxToken equalToken()
:如果变量在被声明时就赋值了,获取赋值语句中等号的语法token。 -
@Nullable ExpressionTree initializer()
:如果变量在被声明时就赋值了,获取赋值语句中初始值的表达式。注意在声明时的赋值表达式不算赋值表达式树。
-
Symbol symbol()
:获取变量的符号。 -
@Nullable SyntaxToken endToken()
:获取分号的语法token。
使用BlockTree
的树和方法很少。
-
List<StatementTree> body()
:获取块的内容,以一列语句树的方式列出。 -
SyntaxToken openBraceToken()
:获取左大括号的语法token。 -
SyntaxToken closeBraceToken()
:获取右大括号的语法token。
父类:BlockTree
代表一个类中的静态初始化代码块。
例:
class MyClass {
private static final int a;
// 静态初始化代码块
static {
a = 1;
}
}
SyntaxToken staticKeyword()
:获取static
关键字的语法token。
父类: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()
:获取这个方法的方法符号。
父类:Tree
把节点的类型用树的方式存储。相比起类型,类型树包含了更丰富的信息。
貌似无法正确解析文件中新定义的类型,例如class A {}
中使用this.method()
,this
是新定义的A
类,会解析成unknown
的类型树(即)。
-
Type symbolType()
:获取类型树代表的类型。 -
List<AnnotationTree> annotations()
:获取类型树的注解,注解以一列注解树的方式列出。不知为何,这个方法不能获取注解类的注解。例如自定义注解时需要使用的
@Target
、@Retention
等等,在检查自定义注解的时候是无法获取到的。
代表数组类型。注意方法参数中的可变参数也算作是数组类型。
-
TypeTree type()
:获取数组的类型树。 -
@Nullable SyntaxToken openBracketToken()
:获取左中括号的语法token。 -
@Nullable SyntaxToken closeBracketToken()
:获取右中括号的语法token。 -
@Nullable SyntaxToken ellipsisToken()
:如果这个数组类型是可变参数,获取省略号的语法token。
父类:TypeTree
用于表示一个泛型。
SyntaxToken keyword()
:获取原始类型的语法token(例如int
、byte
等等)。
父类:TypeTree
UnionType
是Java 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);
}
}
}
在上面的例子中,我们同时捕获了ArrayIndexOutOfBoundsException
和NullPointerException
,并且用同样的方式处理了这两个异常。
ListTree<TypeTree> typeAlternatives()
:获取联合类型中的所有类型,以列表树的方式列出。
父类:TypeTree
var
关键字是Java 10中的新特性。当声明一个局部变量时,我们需要会指定它的类型。而在Java 10中,我们可以把局部变量的类型声明为var
,让Java自己根据上下文去猜测这个变量的类型。详见Java 10更新描述。
SyntaxToken varToken()
:获取var
关键字的语法token。
父类:TypeTree
用于检测泛型中的通配符。
-
List<AnnotationTree> annotations()
:获取泛型的注解,注解以一列注解树的方式列出。 -
SyntaxToken queryToken()
:获取通配符?
的语法token。 -
@Nullable SyntaxToken extendsOrSuperToken()
:获取extends
或super
的语法token。 -
@Nullable TypeTree bound()
:获取通配符的边界的类型树。
父类:Tree
、List
ListTree
既能当作Tree
使用,也能当作List
使用。
-
List<SyntaxToken> separators()
:获取列表中的分隔符,分隔符以一列语法token的方式列出。例如
ListTree
中有a
、b
、c
三个元素,那么separators()
返回的就是a
和b
之间的分隔符以及b
和c
之间的分隔符。
父类:ListTree
(元素类型为ModifierTree)
表示一系列的修饰符。
ModifiersTree
是ListTree
的子类,可以直接当作一个包含ModifierTree的List
来使用。
-
List<AnnotationTree> annotations()
:获取列表中的注解,注解以一列注解树的方式列出。 -
List<ModifierKeywordTree> modifiers()
:获取列表中的修饰符,修饰符以一列修饰符关键字树的方式列出。用这个方法和直接把
ModifiersTree
当作List
使用的区别是List
中元素的类型。这个方法返回的是ModifierKeywordTree
,而直接当作List
返回的则是ModifierTree
。
父类:ListTree
(元素类型为ExpressionTree
)
获取一系列参数的值。
-
@Nullable SyntaxToken openParenToken()
:获取左括号的语法token。 -
@Nullable SyntaxToken closeParenToken()
:获取右括号的语法token。
获取一系列泛型参数的值。元素类型实际上是表达式树,比如IdentifierTree
和MemberSelectExpressionTree
。
TypeArguments
与TypeParameters
的区别在于,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。
父类:ListTree
(元素类型为TypeParameterTree
)
TypeParameters
与TypeArguments
的区别见TypeArguments
下的说明。
-
@Nullable SyntaxToken openBracketToken()
:获取泛型参数列表的左尖括号的语法token。 -
@Nullable SyntaxToken closeBracketToken()
:获取泛型参数列表的右尖括号的语法token。
父类:ListTree
(元素类型为IdentifierTree
)
相比父类没有任何新增的方法。
关于模组的更多信息见ModuleDirectiveTree
。
父类:Tree
相比它的父类Tree
没有任何新加的方法。
父类:ModifierTree
-
Modifier modifier()
:获取修饰符的类型。Modifier
是一个枚举类,包含了所有修饰符的类型。 -
SyntaxToken keyword()
:获取修饰符的语法token。
父类:Tree
泛型参数是用来表示泛型的占位符,例如MyClass<T extends Number>
中的T extends Number
。
-
IdentifierTree identifier()
:获取泛型参数的标识符树。 -
@Nullable SyntaxToken extendsToken()
:获取extends
关键字的语法token。 -
ListTree<Tree> bounds()
:获取泛型参数的边界,边界以列表树的方式列出。
父类:Tree
模组module
是Java 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;
}
用于指定一个依赖的模组。
-
ModifiersTree modifiers()
:获取require
命令的多修饰符树。 -
ModuleNameTree moduleName()
:获取require
命令后的所有模组,以模组名称树的方式列出。
用于指定一个包作为模组的导出项。
-
ExpressionTree packageName()
:获取exports
命令后的包的表达式树。 -
@Nullable SyntaxToken toKeyword()
:获取exports
命令后的to
关键字的语法token。to
关键字可以指定这个包只能被哪些模组使用。如果没有指定to
关键字,那么这个包就可以被所有模组使用。 -
ListTree<ModuleNameTree> moduleNames()
:获取exports
命令中to
关键字后的所有模组,以列表树的方式列出,元素类型是模组名称树。
用于指定一个包作为模组的开放项,这个包里的所有成员都可以在这个模组中用反射获取。在Java 8和更早版本中,反射总是能获取一个包中的所有成员,包括private
成员。但是在Java 9和更高版本中,反射默认只能获取除private
以外的成员,如果想要获取private
成员,就需要使用opens
命令。
-
ExpressionTree packageName()
:获取opens
命令后的包的表达式树。 -
@Nullable SyntaxToken toKeyword()
:获取opens
命令后的to
关键字的语法token。to
关键字可以指定这个包只能被哪些模组使用。如果没有指定to
关键字,那么这个包就可以被所有模组使用。 -
ListTree<ModuleNameTree> moduleNames()
:获取opens
命令中to
关键字后的所有模组,以列表树的方式列出,元素类型是模组名称树。
用于指定一个可供使用的服务模组接口。
TypeTree typeName()
:获取uses
命令后的服务模组接口的类型树。
用于给一个服务模组接口提供一个实现。
-
TypeTree typeName()
:获取provides
命令后的服务模组接口的类型树。 -
SyntaxToken withKeyword()
:获取provides
命令后的with
关键字的语法token。 -
ListTree<TypeTree> typeNames()
:获取provides
命令中with
关键字后的所有服务模组接口的实现类,以列表树的方式列出,元素类型是类型树。
父类:Tree
编译单元是指一个Java源文件,例如MyClass.java
,编译单元树是指这个源文件的语法树。编译单元树一般是所有树的最终父节点。
-
@Nullable PackageDeclarationTree packageDeclaration()
:获取编译单元中的包声明,以包声明树的方式获取。 -
List<ImportClauseTree> imports()
:获取编译单元中的所有导入语句,以一列导入项树的方式列出。 -
List<Tree> types()
:获取编译单元中的所有类型声明(比如类和注解的定义,即在这个文件中新建的类和注解),以一列树的方式列出。 -
@Nullable ModuleDeclarationTree moduleDeclaration()
:获取编译单元中的模组声明,以模组声明树的方式获取。 -
SyntaxToken eofToken()
:获取编译单元的结束符的语法token。
父类:Tree
与父类相比没有新增的方法。
有时能类型转换成ImportTree
。
-
boolean isStatic()
:判断这个导入项是否是静态导入。 -
SyntaxToken importKeyword()
:获取import
关键字的语法token。 -
@Nullable SyntaxToken staticKeyword()
:如果这个导入项是静态导入,获取static
关键字的语法token。 -
Tree qualifiedIdentifier()
:获取导入的包。这个地方的
Tree
可以转换为MemberSelectExpressionTree
或者IdentifierTree
。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
父类: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。
父类:Tree
-
List<AnnotationTree> annotations()
:获取包声明的注解,注解以一列注解树的方式列出。注意:包声明的注解只能是
@Deprecated
。 -
SyntaxToken packageKeyword()
:获取package
关键字的语法token。 -
ExpressionTree packageName()
:获取包名的表达式树。 -
SyntaxToken semicolonToken()
:获取分号的语法token。
父类: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。
父类:Tree
-
List<CaseLabelTree> labels()
:获取case
组的标签,以一列Case标签树的方式列出。 -
List<StatementTree> body()
:获取case
组的内容,以一列语句树的方式列出。
父类:Tree
-
SyntaxToken caseOrDefaultKeyword()
:获取case
或default
关键字的语法token。 -
boolean isFallThrough()
:判断这一个case
是否会穿透到下一个case
。 -
List<ExpressionTree> expression()
:获取case
标签的所有条件表达式。 -
SyntaxToken colonOrArrowToken()
:获取冒号或箭头的语法token。一般来说,
switch
中的case
语句的条件后会跟一个冒号。在Java 12或更高版本中,这个冒号可以使用箭头->
来代替。使用箭头的case
在执行完成后会自动break
,避免因为忘记写break
而导致的穿透问题。详见Java 12博客和Java 17更新文档。
父类:Tree
-
SyntaxToken catchKeyword()
:获取catch
关键字的语法token。 -
SyntaxToken openParenToken()
:获取左括号的语法token。 -
VariableTree parameter()
:获取catch
语句的参数,以变量树的方式获取。 -
SyntaxToken closeParenToken()
:获取右括号的语法token。 -
BlockTree block()
:获取catch
语句的内容,以块树的方式获取。
父类:Tree
代表一个enum
中的一个常量。
-
ModifiersTree modifiers()
:获取enum
常量的多修饰符树。不知道有什么用,一个
enum
常量是没有修饰符的。 -
IdentifierTree simpleName()
:获取enum
常量的标识符树。 -
NewClassTree initializer()
:获取enum
常量的初始化语句,以新建类对象树的方式获取。 -
@Nullable SyntaxToken separatorToken()
:获取enum
常量后的分隔符的语法token。enum
常量之间的分隔符是逗号,最后一个常量后的分隔符是分号。
父类:Tree
代表一个数组在一个维度上的长度。例如new int[5][10]
中的[5]
和[10]
分别是一个数组长度树。
-
List<AnnotationTree> annotations()
:获取数组长度的注解,注解以一列注解树的方式列出。 -
SyntaxToken openBracketToken()
:获取左方括号的语法token。 -
@Nullable ExpressionTree expression()
:获取数组长度的表达式树。 -
SyntaxToken closeBracketToken()
:获取右方括号的语法token。
父类:无
类型是把Java中的类型转换为语法树元素得来的,内置了多种判断类型的方法。
注意
-
此处的类型
Type
和Tree.Kind
中的种类不是同一个东西:此处的类型是Java中的类型,Tree.Kind
中的种类是语法树中的种类。 -
import
语句中的类型是无法获取的。例如import java.util.List
,List
是无法获取的。
Type
下有Type.Primitives
枚举类,包含了所有Java的基本类型。
-
boolean is(String var1)
:判断类型是否是var1
,var1
是另一个类型的名称的字符串对比类型时要用这个方法,而不能直接对类型用
==
或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()
:获取类型的参数类型列表。
父类:Type
Type elementType()
:获取数组元素的类型。
父类:无
符号表示一个方法、类、变量等等的签名信息。符号是SonarQube中的符号化(symbolic)API的核心,它可以“假装运行”代码来理清代码逻辑和流向控制,追踪变量的使用和方法的调用等等。
Symbol
有一个类似的类LabelSymbol
,三个子类MethodSymbol
、VariableSymbol
、TypeSymbol
,和一个相关的类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()
:符号的声明,在各子类中有对应重载的方法。
父类:Symbol
-
List<Type> parameterTypes()
:获取方法的参数类型列表。 -
TypeSymbol returnType()
:获取方法的返回值类型符号。 -
List<Type> thrownTypes()
:获取方法抛出的异常中的类型列表。 -
List<MethodSymbol> overriddenSymbols()
:获取方法的重写方法符号列表。 -
String signature()
:获取方法的签名。 -
@Nullable MethodTree declaration()
:以方法树的形式,获取方法的声明。
父类:Symbol
@Nullable VariableTree declaration()
:以变量树的形式,获取变量的声明。
父类:Symbol
用于类(class
)的符号。
-
@CheckForNull Type superClass()
:获取类的父类型。 -
List<Type> interfaces()
:获取类实现的接口类型列表。 -
Collection<Symbol> memberSymbols()
:获取类的成员符号列表。 -
Collection<Symbol> lookupSymbols(String var1);
:在类的成员中搜索名称为var1
的符号并返回匹配。 -
@Nullable ClassTree declaration()
:以类树的形式,获取类的声明。
父类:无
注意LabelSymbol
不是Symbol
的子类,而是Symbol
的另一个类似类,所以它无法使用Symbol
的方法。
-
String name()
:获取标签的名称。 -
List<IdentifierTree> usages()
:获取标签的使用,为一个标识符列表。 -
@Nullable LabeledStatementTree declaration()
:获取标签的声明。
父类:无
表示符号的元数据,用于获取符号的注解信息。注意AnnotationTree
同样可以获取注解信息,这两者的区别见AnnotationTree
下的说明。
SymbolMetadata
中定义了两个接口,AnnotationInstance
和AnnotationValue
,分别用于获取单个注解和单个注解的值。
-
boolean isAnnotatedWith(String annotationName)
:判断符号是否被annotationName
注解了。 -
@CheckForNull List<AnnotationValue> valuesForAnnotation(String annotationName)
:获取用于注解这个符号的,名称是annotationName
的注解的参数。 -
List<AnnotationInstance> annotations()
:获取符号的所有注解实例。
父类:无
表示一个注解。
父类:无
表示一个注解值(包括key和value)。
-
String name()
:获取注解值的键值(key)。 -
Object value()
:获取注解值的值(value)。
父类:Tree
一个SyntaxToken
表示一个词语或者符号,比如return
、String
、==
、+
、{
、}
等等。使用token可以有效检查代码格式,比如判断是否有空格、是否有换行等等。token也是SonarJava中获取代码源文本以及文本位置的唯一方式。
-
String text()
:token的原文本。 -
int line()
:token所在的行数。行数是从1开始的。 -
int column()
:token所在的列数。列数是从0开始的。 -
List<SyntaxTrivia>
:token的语法trivia。
父类:Tree
代表一个语法token之前的空格、注释等等对程序执行没有影响的部分。只能获取在这个语法token和上一个有意义的语法token之间的trivia。
似乎只能获取文本注释,不能把获取空格、换行等别的trivia。只能获取这个语法token之前的注释。
例:
// 注释一
public class MyClass {
/* 注释二
第二行
*/
private void myMethod() {
System.out.println("Hello World!");
}
}
在以上的代码中,如果我需要获取注释一
,那么我需要先找到MyClass
的public
关键字的语法token,再调用trivias()
方法获取所有public
关键字之前的语法trivia,然后再找到注释一
的语法trivia。
如果我需要获取注释二
,那么我需要先找到myMethod
的private
关键字的语法token并重复以上步骤。private
的语法token能获取的语法trivia仅限private
之前,MyClass
的左大括号{
之后的部分,获取到的// 注释二
文本会忽略前面的空格,但会带有注释用的//
,而且column()
方法计算列数时会算入前面的空格。
对于多行注释,第一行前面的空格会被忽略,后面所有行的空格会被保留。获取到的注释文本是:
/* 注释二
第二行
*/
注释开始的行数是3,列数是2。
在测试时,// Noncompliant
注释会被忽略。
-
String comment()
:获取注释的原文本。 -
int startLine()
:注释开始的行数。 -
int column()
:注释开始的列数。
源码见SonarJava仓库。
用于处理关于表达式树的一些操作。
static String concatenate(@Nullable ExpressionTree tree)
:获取一个表达式树的原文本。
用于检查一个文件中的方法是否是某个特定的方法。
用法:
-
用
MethodMatchers matcher = MethodMatchers.create().设置类型.names(方法名).设置参数.build()
来构造一个MethodMatchers
对象并设置想要匹配的方法。 -
用
matcher.match(tree)
来检测tree
代表的方法是否符合matcher
中设置的方法。
获取一个类或者方法的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.java
的getJavaChecks()
方法或getJavaTestChecks()
方法中添加这个规则的类。如果有测试,则同时需要在/src/test/java/package路径/MyJavaFileCheckRegistrarTest.java
的checkNumberRules()
方法中修改规则的总数量。
完成规则构建后,用mvn clean package
命令打包,把生成的jar包放到SonarQube的$SONAR_HOME/extensions/plugins
目录下,重启SonarQube即可。
如果只需要检测少量几种语法树,而且不同语法树之间互不影响,则可以用IssuableSubscriptionVisitor
,这个类只需要实现visitNode
和nodesToVisit
方法即可。每个类型在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
和JavaFileScanner
,这个类需要实现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等等
}
IssuableSubscriptionVisitor
和BaseTreeVisitor
很方便,很多时候只使用一个就够了,但以下两种情况中,仅使用一种visitor是比较困难的。
-
节点被遍历的顺序是由
IssuableSubscriptionVisitor
和BaseTreeVisitor
自己决定的,但有时我们需要自定义的遍历顺序。 -
在
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...(...) {
// ...
}
}
}
通过Tree
的accept()
方法,我们可以手动地放入一个visitor来立刻开始遍历这个树。这个visitor只能检查accept()
方法中放入的节点和它的所有子节点,不能检查其他节点。这个visitor可以是IssuableSubscriptionVisitor
或BaseTreeVisitor
的子类。visitNode()
中accept()
后面的代码会等待遍历结束再执行。
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文件是这个规则在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文件描述这个规则的属性,比如类型、标签、严重性等等。
{
"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();
}
}
以下的规则仅做示例用,规则本身不一定有实际用途。
[Java开发手册] 1.1.1 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
由于命名规范多用于类、方法和变量的命名,这里考虑遍历这三者的语法树。这里需要遍历多种语法树,但语法树之间互不影响,所以BaseTreeVisitor
和IssuableSubscriptionVisitor
都可以用。
使用BaseTreeVisitor
的规则文件:
使用到的类和方法:
-
BaseTreeVisitor
:visitClass()
、visitMethod()
、visitVariable()
-
JavaFileScannerContext
:reportIssue()
、getTree()
-
ClassTree
:simpleName()
-
MethodTree
:simpleName()
-
VariableTree
:simpleName()
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
的规则文件:
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
ClassTree
:simpleName()
-
MethodTree
:simpleName()
-
VariableTree
:simpleName()
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
。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
VariableTree
:simpleName()
、modifiers()
-
ModifiersTree
:modifiers()
-
ModifierKeywordTree
:modifier()
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并不支持查找类的子类,查找父类时也仅支持取得父类的类型(即类名),所以需要自己实现子类的父类的对应关系。这里选择使用两个Map
,CLASS_FIELDS
存储一个类的名字和它的所有类变量名,PARENT_TO_CHILDREN
存储一个类的名字和它的所有子类的名字。
如果需要测试这个规则,在规则示例中,// Noncompliant
注解需要写在类定义的后面,因为这里是在类树中reportIssue()
。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
ClassTree
:symbol()
、members()
、superClass()
-
VariableTree
:simpleName()
-
TypeTree
:symbolType()
-
Symbol
:name()
-
Type
:name()
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
类型,则进行检测。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
LiteralTree
:value()
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的位置。
由于完整的规则文件过长,这里仅展示部分方法,其它方法的构造是类似的。
使用到的类和方法:
-
BaseTreeVisitor
:visitLambdaExpression()
、visitParenthesized()
、visitTypeCast()
、visitDoWhileStatement()
、visitForEachStatement()
、visitForStatement()
、visitIfStatement()
、visitSynchronizedStatement()
、visitTryStatement()
-
SyntaxToken
:column()
、identifierToken()
-
Tree
:firstToken()
、lastToken()
-
LambdaExpressionTree
:openParenToken()
、closeParenToken()
、parameters()
-
VariableTree
:simpleName()
-
ParenthesizedTree
:openParenToken()
、closeParenToken
、expression()
-
TypeCastTree
:openParenToken()
、closeParenToken()
、type()
-
DoWhileStatementTree
:openParenToken()
、closeParenToken()
、condition()
-
ForEachStatement
:openParenToken()
、closeParenToken()
、variable()
、expression()
-
ForStatementTree
:openParenToken()
、closeParenToken()
、initializer()
、update()
-
IfStatementTree
:openParenToken()
、closeParenToken()
、condition()
-
SynchronizedStatementTree
:openParenToken()
、closeParenToken()
、expression()
-
TryStatementTree
:openParenToken()
、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");
}
}
}
由于ForStatementTree
和ForEachStatement
都会把循环里的代码块当作一整个语句来处理(而不是当作代码块),我们需要自己定义一个树的遍历器去检查循环里的代码块的内容。
检查的逻辑是:只检查循环里的代码块,在循环里只要遇到if
语句,不论在哪里遇到都报告错误。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
BaseTreeVisitor
:visitIfStatement()
-
Tree
:accept()
-
ForStatementTree
:statement()
-
ForEachStatement
:statement()
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
修饰以及是否被重新赋值,我们只会遍历变量树(可能还会遍历赋值表达式树)所以这里选择使用IssuableSubscriptionVisitor
。
检查的逻辑是:对于每一个final
变量,检查它是否在声明时就已经被赋值了,如果没有,则允许一次赋值(allowedAssignments
设为1
),否则一次赋值都不允许(allowedAssignments
设为0
)。然后对于每一个使用了这个变量的地方,检查是否为赋值语句。如果final
变量是被赋值的一方,那么赋值次数assignments
就加一;如果assignments
大于允许的赋值次数(allowedAssignments
),则报告错误。
这里的诀窍是,如果这个final
变量是被赋值的一方,**它的标识符的父节点一定是赋值表达式树。**如果是赋值给别的变量,则它的父节点会是别的树,比如变量树、表达式树等。通过这个方法可以方便地检验final
变量是否被重新赋值。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、nodesToVisit()
、reportIssue()
-
Tree
:parent()
-
VariableTree
:modifiers()
、symbol()
、initializer()
-
ModifiersTree
:modifiers()
-
ModifierKeywordTree
:modifier()
-
Symbol
:usages()
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
语句有两种办法:遍历所有ImportTree
,和使用CompilationUnitTree
的imports()
方法。单独使用时,以上两种方法是等价的。但现在我们的需求中包含一个先后顺序:先找到所有的import
语句,再去检查文件其它地方。如果使用遍历ImportTree
的办法,则无法控制这个先后顺序,语法树可能会同时遍历ImportTree
和文件其它节点(语法树是有一个内置的遍历顺序的,但为何要去冒这个险呢)。
所以,这里我们选择使用CompilationUnitTree
。CompilationUnitTree
是整个文件的父节点,因此我们可以在这个节点上手动规定遍历的顺序,即先拿到所有的import
语句,再进入节点进行遍历。
需要导入包的地方可能是变量类型、类实例、注解、使用枚举类、extends
和implements
语句、泛型参数值,以及方法参数和返回值类型。总结起来我们需要检查这些地方:变量、方法、类、类实例化、注解、成员访问和泛型参数值。
这里没有检测静态导入和Javadoc中出现的类。
在以下的规则代码中,我们先通过CompilationUnitTree
的imports
方法取得所有的ImportTree
并存入一个Map
,再定义一个只检查新建类实例和变量的类型的visitor来进入CompilationUnitTree
节点。对每个检查到的类型,如果在Map
中有对应的import
的类,则从Map
中删除这个类。Map
中最后剩下的类就是没有被使用到的类。
这个例子中使用了SonarJava的一个工具类ExpressionsHelper
来取得完整的import
语句后的类名,源代码在这里。
使用到的类和方法:
-
IssuableSubscriptionVisitor
:visitNode()
、leaveNode()
、nodesToVisit()
、reportIssue()
-
Tree
:accept()
-
CompilationUnitTree
:imports()
-
ImportTree
:is()
、qualifiedIdentifier()
-
VariableTree
:type()
-
TypeTree
:symbolType()
-
Type
:fullyQualifiedName()
-
NewClassTree
:symbolType()
-
ExpressionsHelper
:concatenate()
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;
}
}
}