checkstyle + gradle + git pre-commit 实现代码提交前对代码规范的检查

我们的目的是想在代码commit之前去做这个检查,把不符合规范的代码标记出来,直到修改完成之后才允许提交。脚本涉及到几个重要的文件:1.pre-commit, 这个是git的一个hook文件,可以帮助我们在commit前去做一些事情,比如,调用我们第二步定义的checkstyle。2.checkstyle.gradle,这里面主要包含gradle的一个task,通过依赖checkstyle插件,然后定义我们自己的checkstyle task 去获取commit前的一些待提交文件,再运用我们定义的规则去做检查。3.checkstyle.xml文件,这个就是定义规则的文件,参考了google编码规范和华为编码规范。4.suppressions.xml,这个是过滤选项,也即是可以排除某些指定文件不受代码规范的检查,比如协议文件。

由于现在AndroidStudio已经支持了checkstyle插件,所以我们只要在.gradle文件里面引用就可以了。(我们这里采用的是checkstyle 6.5)

1.通常,我们会包装自己的checkstyle脚本,将其命名为checkstyle.gradle。代码如下:

apply plugin: 'checkstyle'

dependencies {

    //也可依赖于生成的jar包,以后再说
    //compile fileTree(dir: 'libs', include: ['*.jar'])
    //checkstyle files('../custom-checkstyle/custom-checkstyle.jar')

    //checkstyle task 依赖 custom-checkstyle module, custom-checkstyle moudle有自定义的Check
    checkstyle project(':custom-checkstyle')

    // for using compiled version
    //checkstyle 'com.puppycrawl.tools:checkstyle:6.5'

}

def reportsDir = "${project.buildDir}/reports"

checkstyle {
    //工具版本
    toolVersion '6.5'
    //配置文件路径
    configFile file("${project.rootDir}/checkstyle.xml")
    //filter路径
    configProperties.checkstyleSuppressionsPath = file("${project.rootDir}/suppressions.xml").absolutePath
}

task checkstyle(type: Checkstyle, group: 'verification') {

    try {//try一下,即使发生异常也不影响正常编译

        def isCheck = true //是否打开代码规范检查的开关
        def isCommit = project.hasProperty('checkCommit') //是否是提交前检查
        if (isCheck) {
            if (isCommit) {
                //检测代码路径
                 //source project.rootDir //--- 检查项目中所有的文件, 比较慢, 下面分模块检查, 主要是src下面的java文件
                 source 'xxx/src'
                source 'lib-xxx/src'
                source 'src' //submodules的检查

                //排除项
                 exclude '**/gen/**'
                exclude '**/test/**'
                exclude '**/res/**'
                exclude '**/androidTest/**'
                exclude '**/R.java'
                exclude '**/BuildConfig.java'
                
                //判断是否是git pre-commit hook触发的checkstyle
                //如果是,只检测要提交的java文件,否则检测路径下的所有java文件
                if (project.hasProperty('checkCommit') && project.property("checkCommit")) {
                    def ft = filterCommitter(getChangeFiles())
                    def includeList = new ArrayList<String>()
                    for (int i = 0; i < ft.size(); i++) {
                        String spliter = ft.getAt(i)
                        String[] spliterlist = spliter.split("/")
                        String fileName = spliterlist[spliterlist.length - 1]
                        includeList.add("**/" + fileName)
                    }
                    if (includeList.size() == 0) {
                        exclude '**/*.java'
                    } else {
                        println("includeList==" + includeList)
                        include includeList
                    }
                } else {
                    include '**/*.java'
                }
                classpath = files()

                reports { // 支持htmlxml两种报告形式,可以任选其一(html更具有可读性)
                    xml.enabled = false
                    html.enabled = true
                    xml {
                        destination file("$reportsDir/checkstyle/checkstyle.xml")
                    }
                    html {
                        destination file("$reportsDir/checkstyle/checkstyle.html")
                    }
                }
            } else { //如果不是提交触发的,也就是对项目进行构建,那么需要对pre-commit文件进行copy
                def forceCopy = false //如有需要,可以强制去更新客户端的pre-commit文件
                try {
                    copyPreCommitFile(forceCopy)
                    //copySubmodulesPreCommitFile(forceCopy)
                } catch (Exception e) {
                    println(e)
                }
            }
        }

    }catch (Exception e){
        println("checkstyle catch an exception.")
        e.printStackTrace()
    }
}

//src是一个文件路径,target是一个目录路径
def copyFile(boolean forceUpdate, String src, String target){
    def fileName = "pre-commit"
    def targetFile = file(target + "/" + fileName)

    if(targetFile.exists() && targetFile.isFile() && !forceUpdate){ //目标文件存在且没有强制更新,不需要copy操作
        println(targetFile.absolutePath + " exist.")
    }else {
        //targetFile.delete()
        def srcFile = file(src)
        if (srcFile.isFile()) {
            copy {
                from srcFile
                into target
            }
        }
    }

    //targetFile = file(target + "/" + fileName)
    if(targetFile.isFile()) {
        if (!targetFile.canExecute()) {
            targetFile.setExecutable(true)
        }
        if (!targetFile.canWrite()) {
            targetFile.setWritable(true)
        }
    }
}

//把根目录下的pre-commit文件复制到.git-->hooks目录
def copyPreCommitFile(boolean forceUpdate){

    def src = "${project.rootDir}/pre-commit"
    def target = "${project.rootDir}/.git/hooks"
    copyFile(forceUpdate, src, target)

    println("copyPreCommitFile")
}

//submodules目录下的pre-commit文件复制到.git-->modules-->submodules-->XXXmoudles-->hooks 目录
def copySubmodulesPreCommitFile(boolean forceUpdate){

    def src = "${project.rootDir}/submodules/pre-commit"
    def submodulesDir = "${project.rootDir}/.git/modules/submodules"
    File file = new File(submodulesDir)
    File[] fileList = file.listFiles()
    if(fileList != null && fileList.length > 0) {
        def size = fileList.length
        for (int i = 0; i < size; i++) {
            if (fileList[i].isDirectory()) {
                //target = "${project.rootDir}/.git/modules/submodules/XXX/hooks"
                def target = submodulesDir + "/" + fileList[i].getName() + "/hooks"
                copyFile(forceUpdate, src, target)
            }
        }
    }

    println("copySubmodulesPreCommitFile")
}

//过滤java文件
def filterCommitter(String gitstatusinfo) {
    ArrayList<String> filterList = new ArrayList<String>()
    String[] lines = gitstatusinfo.split("\\n")
    for (String line : lines) {
        if (!line.startsWith("D ") && line.contains(".java") ) {
            String[] spliters = line.trim().split(" ")
            for (String str : spliters) {
                if (str.contains(".java")) {
                    filterList.add(str)
                }
            }
        }
    }
    return filterList
}

//获取git commit待提交的文件列表
def getChangeFiles() {
    try {
        String changeInfo = 'git status -s'.execute(null, project.rootDir).text.trim()
        return changeInfo == null ? "" : changeInfo
    } catch (Exception e) {
        return ""
    }
}

2.定制自己的checkstyle.xml文件,就是代码检查需要应用的哪些规则,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.2//EN" "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">

<module name="Checker">

   <property name="charset" value="UTF-8"/>
   <property name="severity" value="error"/>

   <module name="SuppressionCommentFilter"/>
   <module name="SuppressionFilter">
      <property name="file" value="${checkstyleSuppressionsPath}"/>
   </module>

   <!--代码规范-->
   <module name="TreeWalker">

   <!--1.代码块检查-->
      <!--检查空白块 option, 需要去掉空白块, 选项 textstmt default stmt -->
      <!--<module name="EmptyBlock">-->
         <!--<property name="option" value="text"/>-->
         <!--<property name="tokens"-->
            <!--value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>-->
      <!--</module>-->

      <!--Empty catch block. 检查空的 catch, 需要把异常堆栈打印出来, 方便定位原因 -->
      <module name="EmptyCatchBlock" />

      <!--option: 定义左大括号'{'显示位置,eol在同一行显示,nl在下一行显示
          maxLineLength: 大括号'{'所在行行最多容纳的字符数
          tokens: 该属性适用的类型,例:CLASS_DEF,INTERFACE_DEF,METHOD_DEF,CTOR_DEF -->
      <module name="LeftCurly">
         <property name="option" value="eol"/>
      </module>

      <!-- NeedBraces 检查是否应该使用括号的地方没有加括号, 主要在if, else时有这样的情况 tokens: 定义检查的类型 -->
      <module name="NeedBraces"/>

      <!-- Checks the placement of right curly braces ('}') for  else, try, and catch tokens. The policy to verify is specified using property  option.
          option: 右大括号是否单独一行显示
          tokens: 定义检查的类型  -->
      <module name="RightCurly">
         <property name="id" value="RightCurlySame"/>
         <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/>
      </module>
      <module name="RightCurly">
         <property name="id" value="RightCurlyAlone"/>
         <property name="option" value="alone"/>
         <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT"/>
      </module>

   <!--2.类检查-->
      <!--Checks that a class which has only private constructors is declared as final.只有私有构造器的类必须声明为final-->
      <module name="FinalClass" />

      <!--Fields and methods should be before inner classes. 内部类检查,确保内部类在外部类的底部-->
      <!--<module name="InnerTypeLast" />-->

      <!--检查每个java文件是否只含有一个顶层类-->
      <module name="OneTopLevelClass" />

      <!--自定义Check规则-->
      <module name="com.xxx.customcheckstyle.ProhibitExtendCheck">
         <property name="subClassWhiteList" value="XXWebViewClient" />
         <property name="superClassSet" value="WebViewClient" />
      </module>

      <!-- Anonymous inner class length is {0,number,integer} lines (max allowed is {1,number,integer}).匿名类的最大行数,缺省为20 -->
      <!--<module name="AnonInnerLength">-->
         <!--<property name="max" value="200" />-->
      <!--</module>-->

   <!--3.代码检查-->
      <!--Default should be last label in the switch. switch default的位置检查,在所有case下面-->
      <module name="DefaultComesLast" />

      <!--空行检查  一行 ; -->
      <module name="EmptyStatement" />

      <!--String literal expressions should be on the left side of an equals comparison. 使用 equals 时 避免出现null   str.equls("xx") 正确使用 "xx".equls(str) -->
      <module name="EqualsAvoidNull" />

      <!--检查 重写equals方法时是否有重写hashCode方法-->
      <module name="EqualsHashCode" />

      <!--检查 switch 语句 中 case 代码块是否包含 break, return, throw or continue-->
      <module name="FallThrough" />

      <!--检查局部变量,参数 是否跟全局变量同名
            ignoreConstructorParameter 忽略构造方法的参数 default false
            ignoreSetter 忽略setter default false
            setterCanReturnItsClass 忽略返回 this 类型的setter default false
        -->
      <!--<module name="HiddenField">-->
         <!--<property name="ignoreConstructorParameter" value="true" />-->
         <!--<property name="ignoreSetter" value="true" />-->
         <!--<property name="setterCanReturnItsClass" value="true" />-->
      <!--</module>-->

      <!--检查switch 是否包含 default-->
      <module name="MissingSwitchDefault" />

      <!--检查同个文件中是否出现多个同样的字面量-->
      <!--<module name="MultipleStringLiterals" />-->

      <!-- 每一行只能定义一个变量  如 Button b1,b2,b3; -->
      <module name="MultipleVariableDeclarations" />

      <!-- if-else嵌套语句个数 最多5-->
      <module name="NestedIfDepth">
         <property name="max" value="5"/>
      </module>

      <!-- try-catch 嵌套语句个数 最多2-->
      <module name="NestedTryDepth">
         <property name="max" value="2"/>
      </module>

      <!--一行只允许一条语句,即末尾一个 ; -->
      <module name="OneStatementPerLine" />

      <!--重载方法是否在同个地方-->
      <!--<module name="OverloadMethodsDeclarationOrder" />-->

      <!--Assignment of parameter 'phone' is not allowed. 参数中不允许有赋值语句-->
      <!--<module name="ParameterAssignment" />-->

      <!--  Checks for overly complicated boolean expressions. Currently finds code like  if (b == true), b || true, !false, etc.
          检查boolean值是否冗余的地方.Expression can be simplified.
          Rationale: Complex boolean logic makes code hard to understand and maintain. -->
      <module name="SimplifyBooleanExpression"/>

      <!--字符串比较检查,字符串比较用equals-->
      <module name="StringLiteralEquality" />

      <!--检查 重写的 clone 方法是否调用了 super.clone()-->
      <module name="SuperClone" />

      <!--检查 重写的 finalize 方法是否调用了 super.finalize()-->
      <module name="SuperFinalize" />

      <!--Unnecessary parentheses around expression. 检查多余的圆括号-->
      <module name="UnnecessaryParentheses" />

      <!-- 每行字符数 -->
      <module name="LineLength">
         <property name="max" value="200" />
      </module>

      <!-- Checks for long methods and constructors. max default 150. max=500 设置长度500 -->
      <!--<module name="MethodLength">-->
         <!--<property name="max" value="500"/>-->
      <!--</module>-->

      <!-- ModifierOrder 检查修饰符的顺序,默认是 public,protected,private,abstract,static,final,transient,volatile,synchronized,native -->
      <module name="ModifierOrder" />

      <!-- 检查是否有多余的修饰符,例如:接口中的方法不必使用publicabstract修饰  -->
      <module name="RedundantModifier" />

      <!-- Checks the number of parameters of a method or constructor. max default 7. -->
      <module name="ParameterNumber">
         <property name="max" value="9" />
      </module>

      <!--  Checks that long constants are defined with an upper ell. That is ' L' and not 'l'. This is in accordance to the Java Language Specification,  Section 3.10.1.
          检查是否在long类型是否定义了大写的L.字母小写l和数字1(一)很相似。Should use uppercase 'L'.
          looks a lot like 1. -->
      <module name="UpperEll"/>

      <!-- A check for TODO: comments. Actually it is a generic regular expression matcher on Java comments. To check for other patterns in Java comments, set property format.
           检查是否存在TODO(待处理) TODOjavaIDE自动生成的。一般代码写完后要去掉。Comment matches to-do format 'TODO:'.
         -->
      <module name="TodoComment"/>

      <!-- Checks the style of array type definitions. Some like Java-style: public static void main(String[] args) and some like C-style: public static void main(String args[])
          检查再定义数组时,采用java风格还是c风格,例如:int[] numjava风格,int num[]c风格。默认是java风格-->
      <module name="ArrayTypeStyle"/>

   <!--4.导包检查-->
      <!-- 必须导入类的完整路径,即不能使用*导入所需的类 -->
      <module name="AvoidStarImport" />

      <!-- 检查是否从非法的包中导入了类 illegalPkgs: 定义非法的包名称-->
      <module name="IllegalImport"/> <!-- defaults to sun.* packages -->


      <!-- 检查是否导入了不必显示导入的类-->
      <module name="RedundantImport" />

      <!-- 检查是否导入的包没有使用-->
      <module name="UnusedImports" />

      <!-- 检查方法的javadoc的注释
         scope: 可以检查的方法的范围,例如:public只能检查public修饰的方法,private可以检查所有的方法
         allowMissingParamTags: 是否忽略对参数注释的检查
         allowMissingThrowsTags: 是否忽略对throws注释的检查
         allowMissingReturnTag: 是否忽略对return注释的检查 -->
      <!--<module name="JavadocMethod">-->
         <!--<property name="scope" value="public"/>-->
         <!--<property name="allowMissingParamTags" value="false"/>-->
         <!--<property name="allowMissingThrowsTags" value="false"/>-->
         <!--<property name="allowMissingReturnTag" value="false"/>-->
         <!--<property name="tokens" value="METHOD_DEF"/>-->
         <!--<property name="allowUndeclaredRTE" value="true"/>-->
         <!--<property name="allowThrowsTagsForSubclasses" value="true"/>-->
         <!--&lt;!&ndash;允许get set 方法没有注释&ndash;&gt;-->
         <!--<property name="allowMissingPropertyJavadoc" value="true"/>-->
      <!--</module>-->

      <!-- 检查类和接口的javadoc 默认不检查author version tags
          authorFormat: 检查author标签的格式
          versionFormat: 检查version标签的格式
        scope: 可以检查的类的范围,例如:public只能检查public修饰的类,private可以检查所有的类
        excludeScope: 不能检查的类的范围,例如:publicpublic的类将不被检查,但访问权限小于public的类仍然会检查,其他的权限以此类推
        tokens: 该属性适用的类型,例如:CLASS_DEF,INTERFACE_DEF -->
      <!--<module name="JavadocType">-->
         <!--<property name="scope" value="public"/>-->
         <!--<property name="tokens" value="CLASS_DEF,INTERFACE_DEF"/>-->
      <!--</module>-->

      <!--First sentence should be present. 检查是否包含不允许使用的词汇-->
      <!--<module name="SummaryJavadocCheck" />-->

   <!--5.命名规范检查-->
      <!-- local, final variables, including catch parameters -->
      <module name="LocalFinalVariableName" />

      <!-- local, non-final variables, including catch parameters-->
      <module name="LocalVariableName" />

      <!-- 静态变量命名  不能有小写字母,长度(0,39) -->
      <module name="StaticVariableName">
         <property name="format" value="(^s[A-Z][a-zA-Z0-9]{0,39}$)" />
      </module>

      <!-- 包命名  小写字母开头 -->
      <module name="PackageName">
         <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />
      </module>

      <!-- 类的命名,匹配规则默认:(^[A-Z][a-zA-Z0-9]*$),必须以大写字母开始 -->
      <module name="TypeName">
         <property name="format" value="(^[A-Z][a-zA-Z0-9]{0,39}$)"/>
         <property name="tokens" value="CLASS_DEF"/>
      </module>

      <!-- 接口命名,匹配规则:(^I[A-Z][a-zA-Z0-9]*$),必须以大写I开始 -->
      <module name="TypeName">
         <property name="format" value="^I[A-Z][a-zA-Z0-9]*$"/>
         <property name="tokens" value="INTERFACE_DEF"/>
      </module>

      <!-- 方法命名  小写字母开头,长度(0,39) -->
      <module name="MethodName">
         <property name="format" value="(^[a-z][a-zA-Z0-9]{0,39}$)" />
      </module>

      <!-- 成员变量命名  小写字母开头,长度(0,39) -->
      <module name="MemberName">
         <property name="format" value="(^m[A-Z][a-zA-Z0-9]{0,39}$)" />
      </module>

      <!-- 常量命名  不能有小写字母,长度(0,39) -->
      <module name="ConstantName">
         <property name="format" value="(^[A-Z0-9_]{0,39}$)" />
      </module>

   </module>

</module>

3.支持添加过滤条件,也就是可以过滤某些文件不受这个规则的检查,定义suppression.xml文件

<?xml version="1.0"?>

<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN"  "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">

<!--checkstyle排除项-->
<!--需要过滤协议文件, 注解文件等等-->
<!--也可以对某个java文件的某段代码过滤某个规则的检查
  <suppress checks="JavadocStyleCheck" files="GeneratedObject.java" lines="50-9999"/>
-->
<suppressions>
    <suppress checks="[a-zA-Z0-9]*" files="R.java" />
    <suppress checks="[a-zA-Z0-9]*" files="BuildConfig.java" />
    <suppress checks="[a-zA-Z0-9]*" files="Test" />
    <suppress checks="[a-zA-Z0-9]*" files="Dagger*" />
    <suppress checks="[a-zA-Z0-9]*" files=".*_.*Factory.java" />
    <suppress checks="[a-zA-Z0-9]*" files=".*ViewInjector.java" />
    <suppress checks="[a-zA-Z0-9]*" files=".*_MembersInjector.java" />
    <suppress checks="[a-zA-Z0-9]*" files=".*_ViewBinding.java" />
    <suppress checks="[a-zA-Z0-9]*Check" files=".*ResProtocal.java" />
    <suppress checks="[a-zA-Z0-9]*Check" files=".*ReqProtocal.java" />
    <suppress checks="[a-zA-Z0-9]*Check" files="[\\/]protocol[\\/]" /> <!--过滤整个包名下面的协议文件-->
    <suppress checks="[a-zA-Z0-9]*Check" files="[\\/]proto[\\/]" /> <!--过滤整个包名下面的协议文件-->
</suppressions>

4.编写git提交前的脚本文件,此文件需要位于.git--->hooks目录下面,所以我们在上面的task中会做一个自动的拷贝,pre-commit

#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
   against=HEAD
else
   # Initial commit: diff against an empty tree object
   against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

echo "start checkstyle task"

SCRIPT_DIR=$(dirname "$0")
SCRIPT_ABS_PATH=`cd "$SCRIPT_DIR"; pwd`
$SCRIPT_ABS_PATH/../../gradlew  -PcheckCommit="true" checkstyle
if [ $? -eq 0   ]; then
    echo "checkstyle OK"
   exit 0
else
   echo "checkstyle fail, for details see /build/reports/checkstyle/checkstyle.html"
    exit 1
fi
5.在最外层的build.gradle文件中引用我们自定义的checkstyle.gradle脚本,
apply from: 'checkstyle.gradle'

6.把这些文件的路径再调整一下,然后就大功告成了,提交代码时执行 git commit就会有代码规范的提示了。检查结果是,符合规范的代码可正常提交,不符合规范的代码会统一放到 /build/reports/checkstyle/checkstyle.html文件中,可用浏览器打开,直至按照规范修改完成之后才能提交。

7.如果checkstyle.xml里面的规则满足不了你的需要,那么需要自定义Check规则,那么需要学习Check的语法树,比如,一个class的定义如下,

* <pre>
* +--CLASS_DEF
*     |
*     +--MODIFIERS
*         |
*         +--LITERAL_PUBLIC (public)
*     +--LITERAL_CLASS (class)
*     +--IDENT (MyClass)
*     +--EXTENDS_CLAUSE
*     +--IMPLEMENTS_CLAUSE
*         |
*         +--IDENT (Serializable)
*     +--OBJBLOCK
*         |
*         +--LCURLY ({)
*         +--RCURLY (})
* </pre>

更多请参考checkstyle插件的源码 TokenTypes类。

8.在项目中新建一个module,命名为custom-checkstyle,修改其build.gradle文件为

apply plugin: 'java'

//jar {
//    destinationDir rootProject.file('custom-checkstyle')
//}

//sourceCompatibility = JavaVersion.VERSION_1_7
//targetCompatibility = JavaVersion.VERSION_1_7

dependencies {
    //compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.puppycrawl.tools:checkstyle:6.5'
}

9.编写自定义Check规则的代码,需要继承自Check(6.5版本是这个,更高的版本应该是Abstract***Check),比如,我写了一个禁止继承自某个类的Check

package com.bigo.customcheckstyle;

import com.google.common.base.Joiner;
import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 *
 * 禁止继承某个类(除白名单子类以外)
 */

public class ProhibitExtendCheck extends Check {

    /**
     * log中对应的key
     */
    public static final String MSG_KEY = "Class ''{0}'' should not extend class ''{1}'' directly, looking for help by lianzhan or caikaiwu.";

    /**
     * Character separate package names in qualified name of java class.
     */
    public static final String PACKAGE_SEPARATOR = ".";

    /**
     * 子类白名单, 对继承没有限制
     */
    private Set<String> mSubClassWhiteList = new HashSet<>();

    /**
     * 需要检查继承关系的父类的集合
     */
    private Set<String> mSuperClassSet = new HashSet<>();

    @Override
    public int[] getDefaultTokens() {
        return getAcceptableTokens();
    }

    @Override
    public int[] getAcceptableTokens() {
        return new int[] {TokenTypes.CLASS_DEF};
    }

    @Override
    public int[] getRequiredTokens() {
        return getAcceptableTokens();
    }

    /**
     * checkstyle.xml中对应的属性调用的setter方法
     * @param names
     */
    public void setSubClassWhiteList(final String[] names) {
        if (names != null && names.length > 0) {
            for (String name : names) {
                mSubClassWhiteList.add(name);
            }
        }
    }

    /**
     * checkstyle.xml中对应的属性调用的setter方法
     * @param names
     */
    public void setSuperClassSet(final String[] names) {
        if (names != null && names.length > 0) {
            for (String name : names) {
                mSuperClassSet.add(name);
            }
        }
    }

    /**
     * 遍历语法树中的每个结点, 结点信息参考{@link TokenTypes.CLASS_DEF}
     * @param ast
     */
    @Override
    public void visitToken(DetailAST ast) {
        DetailAST currentNode = ast;
        while (currentNode != null) {
            if (currentNode.getType() == TokenTypes.CLASS_DEF) {
                String subClassName = currentNode.findFirstToken(TokenTypes.IDENT).getText(); //获取子类的名字
                if (!mSubClassWhiteList.contains(subClassName)) {//不在白名单中
                    String superClassName = null;
                    DetailAST extendNode = currentNode.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
                    if (extendNode != null) {
                        superClassName = extendNode.findFirstToken(TokenTypes.IDENT).getText(); //获取父类的名字
                    }
                    if (mSuperClassSet.contains(superClassName)) {
                        log(currentNode.getLineNo(), MSG_KEY, subClassName, superClassName);
                    }
                }

            }
            currentNode = currentNode.getNextSibling();
        }
    }

    /**
     * Get super class name of given class.
     * @param classAst class
     * @return super class name or null if super class is not specified
     */
    private String getSuperClassName(DetailAST classAst) {
        String superClassName = null;
        final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
        if (classExtend != null) {
            superClassName = extractQualifiedName(classExtend);
        }
        return superClassName;
    }

    /**
     * Get name of class(with qualified package if specified) in extend clause.
     * @param classExtend extend clause to extract class name
     * @return super class name
     */
    private static String extractQualifiedName(DetailAST classExtend) {
        final String className;

        if (classExtend.findFirstToken(TokenTypes.IDENT) == null) {
            // Name specified with packages, have to traverse DOT
            final DetailAST firstChild = classExtend.findFirstToken(TokenTypes.DOT);
            final List<String> qualifiedNameParts = new LinkedList<>();

            qualifiedNameParts.add(0, firstChild.findFirstToken(TokenTypes.IDENT).getText());
            DetailAST traverse = firstChild.findFirstToken(TokenTypes.DOT);
            while (traverse != null) {
                qualifiedNameParts.add(0, traverse.findFirstToken(TokenTypes.IDENT).getText());
                traverse = traverse.findFirstToken(TokenTypes.DOT);
            }
            className = Joiner.on(PACKAGE_SEPARATOR).join(qualifiedNameParts);
        }
        else {
            className = classExtend.findFirstToken(TokenTypes.IDENT).getText();
        }

        return className;
    }
}
10.到此全部OK了。









猜你喜欢

转载自blog.csdn.net/u012571415/article/details/80625193