51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 1108|回复: 0
打印 上一主题 下一主题

浅谈Sonar 自定义规则开发

[复制链接]
  • TA的每日心情
    无聊
    昨天 11:40
  • 签到天数: 943 天

    连续签到: 2 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-11-17 11:22:44 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    前言
      前一段时间学习了 SonarQube 以及如何开发自定义规则,在一顿网上搜索骚操作之后,搭建好了本地的 SonarQube ,并且用 Sonar 自带的规则扫描自己的项目时发现了一些问题。就在我准备更进一步去学习如何进行自定义规则开发的时候,遇到坑了。
      网上关于 Sonar 自定义规则开发的资料其内容都大同小异,介绍如何搭建 SonarQube 环境几乎占了 80% 或者更高,只有少数是进行最简单的开发环境 Demo 介绍(其中包括如何创建规则,以及规则的一些相应配置), 更深入的如何去开发一个规则以及内部原理的文章则更少,于是便有了这篇探索记录文章,希望对于初学 Sonar 自定义规则开发的小伙伴有一定的帮助。
      深入理解 Sonar 自定义规则开发
      Step1: 小白阶段
      当我第一次看到 Sonar 规则,一脸懵逼。。。基于有一定的编程能力,通过规则对应的单元测试进行一步步的 debug 调试(当时每个方法里面都被我打上了断点┑( ̄▽  ̄)┍)
      在一步步的调试下,终于大致看懂了一个规则的数据流向,于是准备开始敲自己的代码,刚打完规则名后就卡住了。。。因为原来代码里面的各种 nodesToVisit、 visitNode 方法都没弄清楚为什么要这么写,更不知道该从哪里开始。于是又重新开始 ( ̄﹏ ̄)。
      Step2: 进阶 ing
      直接拿 Sonar 规则官网[1] 的BadMethodNameCheck规则进行后面的解释,我们就根据 debug 的路径一步步开始讲解。
      Test JavaCheckVerifier.verify ("Sonar 前置")
    @Test
      public void test() {
          JavaCheckVerifier.verify("src/test/files/checks/naming/BadMethodName.java", new BadMethodNameCheck());
      }



    在上面的代码中,测试用例里面调用了类JavaCheckVerifier的verify方法,传入一个文件路径(需要被扫描的测试代码)以及一个BadMethodNameCheck 对象(自定义的规则),那么里面具体做了什么呢?接着看下面在这个过程中的简略版流程图。

     传入了文件以及规则后,Sonar 内部进行了一系列的 scanFile 操作,大多数是进行一些前置准备。其中对理解比较重要的是 visitorsBridge、astScanner、ast 等对象。
      visitorsBridge 对象
      用于保存通过规则对被测代码扫描后的结果。
      astScanner 对象
      提供扫描被测代码的解析功能并生成抽象语法树--Tree ast 对象。
      简单的来说,在这一阶段就是进行各种初始化,生成保存结果集的变量,并且将被测代码解析为抽象语法树。
      通过维基上定义的 抽象语法树[2],这里就不多介绍了, 也可以通过 Idea 下载插件 JDT AST 工具[3] (可能需要翻墙) 生成抽象语法树帮助理解。知道了什么是语法树后,基本上自定义开发规则的进展就完成了 50%。(但实际 Sonar 生成的语法树在格式上与 JDT 生成的会不太一致,使用时候需要注意下)
      下面是我们的被扫描代码内容(* 为了内容简短,删除了部分代码 *):
    1.  class BadMethodName {
    2.       private String id;
    3.         public BadMethodName() {}
    4.         void Bad() { // Noncompliant [[sc=8;ec=11]] {{Rename this method name to match the regular expression '^[a-z][a-zA-Z0-9]*

    5. 通过 AST 工具生成的抽象语法树。
    6.   全局的语法树如下:可以获取代码中的该类是否是接口、类名以及类中定义的类变量以及方法。

    7. 针对类变量的语法树如下:可以获取到变量名称、类型、修饰类型。

    8. 针对方法生成的语法树如下:可以获取到方法名、返回值、修饰符、是否是构造函数和代码块,所有在 { code } 内的又会被解析成一个 blockTree。

    9. 所以,通过 Sonar 的前置操作解析后,我们就拿到了一套标准化的语法树。
    10. [b]  用户代码[/b]
    11.   自定义规则开发的大致思路就是通过过滤顶层 Tree 拿到想要的节点 Tree(如MethodTree、ClassTree、BlockTree、ExpressionStatementTree等),然后根据自己开发逻辑代码实现校验功能。现在来说明下前面说到的 Sonar 在用户代码里面常用的一些内部方法。
    12. [b]  nodesToVisit()[/b]
    13. [indent]public List<Tree.Kind> nodesToVisit() {
    14.       return ImmutableList.of(Tree.Kind.METHOD);
    15.   }[/indent]


    16. 指定要扫描的节点 (也就是树的分支),并在 visitNode 方法中获取到指定的节点,像上面代码就是返回 Tree 中的所有 MethodTree。
    17.   [b]visitNode()[/b]
    18. [indent]public void visitNode(Tree tree) {
    19.       MethodTree methodTree = (MethodTree) tree;
    20.       if (isNotOverriden(methodTree) && pattern.matcher(methodTree.simpleName().name()).matches()) {
    21.           reportIssue(methodTree.simpleName(), "Rename this method name to match the regular expression '" + format + "'.");
    22.       }
    23.   }[/indent]


    24. 获取到 nodesToVisit 中的过滤后的所有节点 Tree,并且按照用户指定的逻辑进行校验。
    25. [b]  setContext()[/b]
    26. [indent]public void setContext(JavaFileScannerContext context) {
    27.       if (pattern == null) {
    28.             pattern = Pattern.compile(format, Pattern.DOTALL);
    29.       }
    30.       super.setContext(context);
    31.   }[/indent]


    32.  super.setContext(context) 获取第一步中初始化的 visitorsBridge 对象,用于存储测试结果。
    33.   本阶段的难点就在于如何保证自己的校验代码不出现漏查或者误查,避免出现扫描检测出来的结果有误,这块类似功能实现,具体的实现方式就不细说了。
    34. [b]  单测结果校验[/b]
    35. [indent]reportIssue(methodTree.simpleName(), "Rename this method name to match the regular expression '" + format + "'.");
    36.   public void reportIssue(Tree tree, String message) {
    37.       context.reportIssue(this, tree, message);
    38.   }[/indent]


    39.  一般在用户规则中,当触犯规则后需要进行一个错误校验,来看下大致的流程图。

    40. 从流程图可以很清楚的发现,开始也是一系列 reportIssues 结果收集,一直到VistorsBridgeForTests 中才开始进行触发规则的内容解析操作,获取了触犯规则的代码行数以及字符串具体的位置,记录在 textSpan 变量中,结果保存在 issues 列表中。
    41. [code]  void Bad2() { // Noncompliant
    复制代码


    通过最初扫描代码记录了标记 Noncompliant 的行数以及字符串位置为预期值,最后根据 CheckVerifer.validateIssue 方法进行预期与实际 issues 存储的结果进行比较。至此单测自定义规则的整个流程就结束了。
      在结果校验中很容易出一些错误, 大家可能在最初都会遇到,在这里大概说明一下:
      At least one issue expected
      这是通过规则进行文件扫描后,未发现任何一个触发了 reportIssue 方法,最后的 issues 的长度为 0 导致报错。
      Expected {3=[{}]}, Unexpected at [11]
      预期的标记 Noncompliant 位置的代码行数与实际扫描出来的结果不一致
      总结
      了解上面的基本逻辑后,整个 Sonar 的自定义规则开发就十分的简单了。后续的深入就是怎么写好用户校验代码,保证校验的结果正确性以及减少误报率。









    .}}
            }
          void good(String id) {
              System.out.println("Test");
          }
      }[/code]

    通过 AST 工具生成的抽象语法树。
      全局的语法树如下:可以获取代码中的该类是否是接口、类名以及类中定义的类变量以及方法。

    针对类变量的语法树如下:可以获取到变量名称、类型、修饰类型。

    针对方法生成的语法树如下:可以获取到方法名、返回值、修饰符、是否是构造函数和代码块,所有在 { code } 内的又会被解析成一个 blockTree。

    所以,通过 Sonar 的前置操作解析后,我们就拿到了一套标准化的语法树。
      用户代码
      自定义规则开发的大致思路就是通过过滤顶层 Tree 拿到想要的节点 Tree(如MethodTree、ClassTree、BlockTree、ExpressionStatementTree等),然后根据自己开发逻辑代码实现校验功能。现在来说明下前面说到的 Sonar 在用户代码里面常用的一些内部方法。
      nodesToVisit()
    public List<Tree.Kind> nodesToVisit() {
          return ImmutableList.of(Tree.Kind.METHOD);
      }



    指定要扫描的节点 (也就是树的分支),并在 visitNode 方法中获取到指定的节点,像上面代码就是返回 Tree 中的所有 MethodTree。
      visitNode()
    public void visitNode(Tree tree) {
          MethodTree methodTree = (MethodTree) tree;
          if (isNotOverriden(methodTree) && pattern.matcher(methodTree.simpleName().name()).matches()) {
              reportIssue(methodTree.simpleName(), "Rename this method name to match the regular expression '" + format + "'.");
          }
      }



    获取到 nodesToVisit 中的过滤后的所有节点 Tree,并且按照用户指定的逻辑进行校验。
      setContext()
    public void setContext(JavaFileScannerContext context) {
          if (pattern == null) {
                pattern = Pattern.compile(format, Pattern.DOTALL);
          }
          super.setContext(context);
      }



     super.setContext(context) 获取第一步中初始化的 visitorsBridge 对象,用于存储测试结果。
      本阶段的难点就在于如何保证自己的校验代码不出现漏查或者误查,避免出现扫描检测出来的结果有误,这块类似功能实现,具体的实现方式就不细说了。
      单测结果校验
    reportIssue(methodTree.simpleName(), "Rename this method name to match the regular expression '" + format + "'.");
      public void reportIssue(Tree tree, String message) {
          context.reportIssue(this, tree, message);
      }



     一般在用户规则中,当触犯规则后需要进行一个错误校验,来看下大致的流程图。

    从流程图可以很清楚的发现,开始也是一系列 reportIssues 结果收集,一直到VistorsBridgeForTests 中才开始进行触发规则的内容解析操作,获取了触犯规则的代码行数以及字符串具体的位置,记录在 textSpan 变量中,结果保存在 issues 列表中。
    1.   void Bad2() { // Noncompliant
    复制代码


    通过最初扫描代码记录了标记 Noncompliant 的行数以及字符串位置为预期值,最后根据 CheckVerifer.validateIssue 方法进行预期与实际 issues 存储的结果进行比较。至此单测自定义规则的整个流程就结束了。
      在结果校验中很容易出一些错误, 大家可能在最初都会遇到,在这里大概说明一下:
      At least one issue expected
      这是通过规则进行文件扫描后,未发现任何一个触发了 reportIssue 方法,最后的 issues 的长度为 0 导致报错。
      Expected {3=[{}]}, Unexpected at [11]
      预期的标记 Noncompliant 位置的代码行数与实际扫描出来的结果不一致
      总结
      了解上面的基本逻辑后,整个 Sonar 的自定义规则开发就十分的简单了。后续的深入就是怎么写好用户校验代码,保证校验的结果正确性以及减少误报率。









    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-8 02:57 , Processed in 0.063151 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表