高校服务地方专题网站建设,家政类网站开发成本,界面设计排版,pinterest设计网站前言
最近做项目#xff0c;定制sonar规则#xff0c;提高Java代码质量#xff0c;在编写的sonar规则#xff0c;做验证时#xff0c;使用单元测试有一些简单的心得感悟#xff0c;分享出来。
自定义规则模式
sonar的自定义规则很简单#xff0c;一般而言有2种模式可…前言
最近做项目定制sonar规则提高Java代码质量在编写的sonar规则做验证时使用单元测试有一些简单的心得感悟分享出来。
自定义规则模式
sonar的自定义规则很简单一般而言有2种模式可以使用
1. 自定义扫描代码逻辑并对分类的Tree的结构处理
2. 使用已扫描的分类对分好类的Tree进行分析
BaseTreeVisitorJavaFileScanner
extends BaseTreeVisitor implements JavaFileScanner
继承Tree的访问器实现Java文件扫描器 TreeVisitor定义了很多Tree的读取过程当然我们也可以扩展这个过程Tree是哪里来的呢scan的接口实现的 IssuableSubscriptionVisitor
extends IssuableSubscriptionVisitor
继承发布订阅访问器意思是sonar的sdk默认扫描出来我们需要定义那些是我们需要的就订阅处理相关的Tree处理推荐使用这种方式开发便捷性能可以得到保障毕竟扫描文件的算法如果写的性能差扫描会很久。
比如我们写一个扫描检查Java代码里的SQL的drop table语句
Rule(key DropTableSQLCheck)
public class DropTableSQLCheck extends IssuableSubscriptionVisitor {private static final String RECOMMENDATION_DROP_MESSAGE Make sure that drop table \%s\ is expected here instead of sql platform.;private static final String DROP_TABLE_STR DROP TABLE;//告诉sonar插件我需要访问这个这里定义了字符串插件定义了很多如果不符合要求就需要自定义扩展同时需要扩展扫描器Overridepublic ListTree.Kind nodesToVisit() {return Collections.singletonList(Tree.Kind.STRING_LITERAL);}// 访问我们刚刚要求的类型已经安装Kind过滤了扫描文件归类的逻辑已经sonar的API实现了如果不符合要求才需要扩展Overridepublic void visitNode(Tree tree) {if (!tree.is(Tree.Kind.STRING_LITERAL)) return;SyntaxToken syntaxToken tree.firstToken();if (syntaxToken null) return;String str syntaxToken.text();if (StringUtils.isBlank(str)) return;String originStr str;str str.trim().toUpperCase();if (str.contains(DROP_TABLE_STR)) {reportIssue(tree, String.format(RECOMMENDATION_DROP_MESSAGE, originStr));}}
}
这里没有考虑xml的SQL包括字符串多个空格的考虑理论上应该按照分析drop然后为index逐个字符读取如果是空格就读取下一个如果是非t就return如果下一个是非a就return当然也可以使用正则。这里使用简化算法做一个Demo 加上规则类仿造写sonarqube的元数据 到此规则编写完成后面分析单元测试逻辑
源码分析
简单分析下代码执行逻辑类似Java agent插件 从main的class执行 上下文加扩展这里是一个注册扫描规则一个注册扫描元数据当然也可以合二为一架构建议分离所以Demo是分离的
元数据规则是sonarqube server启动实例化启动就可以看到检查类别检查类规则是代码分析时实例化 注册规则类这里又分了是否用于test上面的手写规则需要注入这个里面 元数据信息是路径扫描读取的所以只要放在那个路径就行通过Rule的名称匹配文件名 这里的repository_key是sonarqube显式的规则名称同时这里也可以扫描其他语言比如xml
这里写好后如果需要本地验证则需要单元测试。
单元测试
编写单元测试代码即使是不能编译的代码也可以扫描
class A {private final String SQL drop table xxx;void foo() {String SQL1 drop table xxx; // NoncompliantdoSomething(SQL1);}SQL(drop table xxx)public void doSomething(String SQL){// do something}
} 编写test
public class DropTableSQLCheckTest {Testvoid test() {((InternalCheckVerifier) CheckVerifier.newVerifier()).onFile(src/test/files/DropTableSQLCheck.java).withCheck(new DropTableSQLCheck()).withQuickFixes().verifyIssues();}
}
载入需要扫描的文件和使用那个规则类检查
单元测试分析
单元测试写好后出现这个错误 这并不是规则写的有问题或者规则没命中而是一种断言sonar的单元测试jar定义 如果扫描出问题那么需要对有问题的行打上
// Noncompliant
开头的注释否则断言不通过
规则来源于org.sonar.java.checks.verifier.internal.Expectations的
collectExpectedIssues
方法中 private static final String NONCOMPLIANT_FLAG Noncompliant;private final SetSyntaxTrivia visitedComments new HashSet();private Pattern nonCompliantComment Pattern.compile(//\\s NONCOMPLIANT_FLAG);private void collectExpectedIssues(String comment, int line) {// 此处断言收集断言有问题的代码行if (nonCompliantComment.matcher(comment).find()) {ParsedComment parsedComment parseIssue(comment, line);if (parsedComment.issue.get(QUICK_FIXES) ! null !collectQuickFixes) {throw new AssertionError(Add \.withQuickFixes()\ to the verifier. Quick fixes are expected but the verifier is not configured to test them.);}// 放进预估issues.computeIfAbsent(LINE.get(parsedComment.issue), k - new ArrayList()).add(parsedComment.issue);parsedComment.flows.forEach(f - flows.computeIfAbsent(f.id, k - newFlowSet()).add(f));}if (FLOW_COMMENT.matcher(comment).find()) {parseFlows(comment, line).forEach(f - flows.computeIfAbsent(f.id, k - newFlowSet()).add(f));}if (collectQuickFixes) {parseQuickFix(comment, line);}}
同理如果一个问题都没用那么验证不了规则直接断言失败
org.sonar.java.checks.verifier.internal.InternalCheckVerifier private void assertMultipleIssues(SetAnalyzerMessage issues, MapTextSpan, ListJavaQuickFix quickFixes) throws AssertionError {if (issues.isEmpty()) { // 没有错误示例就断言失败throw new AssertionError(No issue raised. At least one issue expected);}ListInteger unexpectedLines new LinkedList();Expectations.RemediationFunction remediationFunction Expectations.remediationFunction(issues.iterator().next());MapInteger, ListExpectations.Issue expected expectations.issues;for (AnalyzerMessage issue : issues) {validateIssue(expected, unexpectedLines, issue, remediationFunction);}// expected就是我们通过注释断言的sonar需要扫描出来的问题如果有部分没有预计断言出来就会if (!expected.isEmpty() || !unexpectedLines.isEmpty()) {Collections.sort(unexpectedLines);ListInteger expectedLines expected.keySet().stream().sorted().collect(Collectors.toList());throw new AssertionError(new StringBuilder() //这里抛出断言缺失的错误或者错误断言的错误.append(expectedLines.isEmpty() ? : String.format(Expected at %s, expectedLines)).append(expectedLines.isEmpty() || unexpectedLines.isEmpty() ? : , ).append(unexpectedLines.isEmpty() ? : String.format(Unexpected at %s, unexpectedLines)).toString());}assertSuperfluousFlows();if (collectQuickFixes) {new QuickFixesVerifier(expectations.quickFixes(), quickFixes).accept(issues);}}
总结
sonar扫描实际上现在已经非常完善了尤其是心得API的提供很大程度不需要我们写什么东西专注核心的扫描算法与Tree的解析甚至大部分扫描算法都不需要写了使用发布订阅即可得益于心得API的提供目前使用的很多API还是Beta注解但是开发效率极大的提升了毕竟以前旧的sonar版本需要我们自己写各种扫描与分析逻辑。不过sonar的单元测试与传统的单元测试是有一定区别的sonar的测试逻辑是必须有一个问题测试案例且需要打上
// Noncompliant
注释注释的行既是命中的行sonar使用行号作为map的key这样sonar才认为是命中规则的。如果断言错误或者断言的数量不对那么也一样会单元测试失败。