Java编写SonarQube自定义规则:String外部参数switch需做空值检查
SonarQube自定义规则实现:String类型外部参数的switch语句必须做空值检查
需求说明
要实现的规则:当switch语句括号中的变量类型为String且该变量为外部方法参数时,必须先对其进行空值检查。
示例代码
class AvoidSwitchNullRule{ public void aMethod(String a){ switch (a){ // 不合规:String类型的外部参数在switch前未做空值检查 case "1":{ System.out.println(1); break; } case "2":{ System.out.println(2); break; } default:{ System.out.println("default"); } } } public void bMethod(String a) { if(StringUtils.isEmpty(a)){ return; } switch (a){ // 合规:已提前做空值检查 case "1":{ System.out.println(1); break; } case "2":{ System.out.println(2); break; } default:{ System.out.println("default"); } } } }
未完成的规则代码
@Rule(key = "AvoidSwitchNullRule") public class AvoidSwitchNullRule extends IssuableSubscriptionVisitor { @Override public List<Tree.Kind> nodesToVisit() { return Arrays.asList(Tree.Kind.SWITCH_STATEMENT); } @Override @ParametersAreNonnullByDefault public void visitNode(Tree tree) { SwitchStatementTree switchStatementTree = (SwitchStatementTree) tree; IdentifierTree identifierTree = (IdentifierTree) switchStatementTree.expression(); if(identifierTree.symbolType().is("java.lang.String") && identifierTree.name().isEmpty()) { this.context.reportIssue(this,tree, "Switch Parameter must not be null"); } } }
完整实现代码
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.Arrays; import java.util.List; @Rule(key = "AvoidSwitchNullRule") public class AvoidSwitchNullRule extends IssuableSubscriptionVisitor { @Override public List<Tree.Kind> nodesToVisit() { return Arrays.asList(Tree.Kind.SWITCH_STATEMENT); } @Override @ParametersAreNonnullByDefault public void visitNode(Tree tree) { SwitchStatementTree switchStmt = (SwitchStatementTree) tree; Tree expr = switchStmt.expression(); // 仅处理直接使用变量的情况 if (!(expr instanceof IdentifierTree)) { return; } IdentifierTree paramIdent = (IdentifierTree) expr; Symbol symbol = paramIdent.symbol(); // 判断是否是方法参数且类型为String if (!symbol.isParameter() || !paramIdent.symbolType().isString()) { return; } // 检查switch前是否已做空值检查 if (!hasNullCheckBeforeSwitch(switchStmt, paramIdent.name())) { reportIssue(switchStmt, "当switch语句使用String类型的外部参数时,必须先进行空值检查"); } } /** * 判断switch语句之前是否存在目标参数的空值检查 */ private boolean hasNullCheckBeforeSwitch(SwitchStatementTree switchStmt, String paramName) { BlockTree enclosingBlock = findEnclosingBlock(switchStmt); if (enclosingBlock == null) { return false; } List<? extends StatementTree> statements = enclosingBlock.body(); int switchIndex = statements.indexOf(switchStmt); if (switchIndex <= 0) { return false; } // 遍历switch之前的所有语句 for (int i = 0; i < switchIndex; i++) { StatementTree stmt = statements.get(i); if (isNullCheckStatement(stmt, paramName)) { return true; } } return false; } /** * 查找switch所在的最内层代码块 */ private BlockTree findEnclosingBlock(Tree tree) { Tree parent = tree.parent(); while (parent != null) { if (parent instanceof BlockTree) { return (BlockTree) parent; } parent = parent.parent(); } return null; } /** * 判断当前语句是否是目标参数的空值检查 */ private boolean isNullCheckStatement(StatementTree stmt, String paramName) { if (stmt instanceof IfStatementTree) { IfStatementTree ifStmt = (IfStatementTree) stmt; return isNullCheckCondition(ifStmt.condition(), paramName); } // 可扩展处理其他语句类型,比如三元表达式赋值 return false; } /** * 判断条件表达式是否是目标参数的空值检查 */ private boolean isNullCheckCondition(ExpressionTree condition, String paramName) { // 处理直接null判断:a == null 或 null == a if (condition instanceof BinaryExpressionTree) { BinaryExpressionTree binaryExpr = (BinaryExpressionTree) condition; if (binaryExpr.is(Tree.Kind.EQUAL_TO)) { return (isIdentifier(binaryExpr.leftOperand(), paramName) && isNullLiteral(binaryExpr.rightOperand())) || (isIdentifier(binaryExpr.rightOperand(), paramName) && isNullLiteral(binaryExpr.leftOperand())); } } // 处理工具类方法:StringUtils.isEmpty(a)、Objects.isNull(a)等 if (condition instanceof MethodInvocationTree) { MethodInvocationTree methodInv = (MethodInvocationTree) condition; Symbol.MethodSymbol methodSymbol = methodInv.methodSymbol(); boolean isEmptyMethod = methodSymbol.name().equals("isEmpty") && (methodSymbol.owner().name().equals("StringUtils") || methodSymbol.owner().name().equals("Strings")); boolean isNullMethod = methodSymbol.name().equals("isNull") && methodSymbol.owner().name().equals("Objects"); if ((isEmptyMethod || isNullMethod) && !methodInv.arguments().isEmpty()) { ExpressionTree arg = methodInv.arguments().get(0); return isIdentifier(arg, paramName); } } return false; } private boolean isIdentifier(ExpressionTree expr, String name) { return expr instanceof IdentifierTree && ((IdentifierTree) expr).name().equals(name); } private boolean isNullLiteral(ExpressionTree expr) { return expr instanceof LiteralTree && ((LiteralTree) expr).is(Tree.Kind.NULL_LITERAL); } }
关键实现说明
- 参数身份与类型校验:通过
symbol.isParameter()确认变量是方法参数,symbolType().isString()快速判断类型为String。 - 空值检查识别:
- 支持直接的
null判断语句(如a == null); - 支持常见工具类的空检查方法(如
StringUtils.isEmpty(a)、Objects.isNull(a)),可根据项目实际使用的工具类扩展判断逻辑;
- 支持直接的
- 范围限定:仅检查switch所在代码块中、位于switch之前的语句,避免误判其他作用域的检查逻辑。
内容的提问来源于stack exchange,提问作者Alex Halawin
相关产品推荐
相关产品推荐

