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()
                    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 filterList = new ArrayList()
    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"?>
module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.2//EN" "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">

name="Checker">

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

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

   
   name="TreeWalker">

   
      
      
         
         
            
      

      
      name="EmptyCatchBlock" />

      
      name="LeftCurly">
         name="option" value="eol"/>
      

      
      name="NeedBraces"/>

      
      name="RightCurly">
         name="id" value="RightCurlySame"/>
         name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/>
      
      name="RightCurly">
         name="id" value="RightCurlyAlone"/>
         name="option" value="alone"/>
         name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT"/>
      

   
      
      name="FinalClass" />

      
      

      
      name="OneTopLevelClass" />

      
      name="com.xxx.customcheckstyle.ProhibitExtendCheck">
         name="subClassWhiteList" value="XXWebViewClient" />
         name="superClassSet" value="WebViewClient" />
      

      
      
         
      

   
      
      name="DefaultComesLast" />

      
      name="EmptyStatement" />

      
      name="EqualsAvoidNull" />

      
      name="EqualsHashCode" />

      
      name="FallThrough" />

      
      
         
         
         
      

      
      name="MissingSwitchDefault" />

      
      

      
      name="MultipleVariableDeclarations" />

      
      name="NestedIfDepth">
         name="max" value="5"/>
      

      
      name="NestedTryDepth">
         name="max" value="2"/>
      

      
      name="OneStatementPerLine" />

      
      

      
      

      
      name="SimplifyBooleanExpression"/>

      
      name="StringLiteralEquality" />

      
      name="SuperClone" />

      
      name="SuperFinalize" />

      
      name="UnnecessaryParentheses" />

      
      name="LineLength">
         name="max" value="200" />
      

      
      
         
      

      
      name="ModifierOrder" />

      
      name="RedundantModifier" />

      
      name="ParameterNumber">
         name="max" value="9" />
      

      
      name="UpperEll"/>

      
      name="TodoComment"/>

      
      name="ArrayTypeStyle"/>

   
      
      name="AvoidStarImport" />

      
      name="IllegalImport"/> 


      
      name="RedundantImport" />

      
      name="UnusedImports" />

      
      
         
         
         
         
         
         
         
         
         
      

      
      
         
         
      

      
      

   
      
      name="LocalFinalVariableName" />

      
      name="LocalVariableName" />

      
      name="StaticVariableName">
         name="format" value="(^s[A-Z][a-zA-Z0-9]{0,39}$)" />
      

      
      name="PackageName">
         name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />
      

      
      name="TypeName">
         name="format" value="(^[A-Z][a-zA-Z0-9]{0,39}$)"/>
         name="tokens" value="CLASS_DEF"/>
      

      
      name="TypeName">
         name="format" value="^I[A-Z][a-zA-Z0-9]*$"/>
         name="tokens" value="INTERFACE_DEF"/>
      

      
      name="MethodName">
         name="format" value="(^[a-z][a-zA-Z0-9]{0,39}$)" />
      

      
      name="MemberName">
         name="format" value="(^m[A-Z][a-zA-Z0-9]{0,39}$)" />
      

      
      name="ConstantName">
         name="format" value="(^[A-Z0-9_]{0,39}$)" />
      

   

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

xml version="1.0"?>

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





    checks="[a-zA-Z0-9]*" files="R.java" />
    checks="[a-zA-Z0-9]*" files="BuildConfig.java" />
    checks="[a-zA-Z0-9]*" files="Test" />
    checks="[a-zA-Z0-9]*" files="Dagger*" />
    checks="[a-zA-Z0-9]*" files=".*_.*Factory.java" />
    checks="[a-zA-Z0-9]*" files=".*ViewInjector.java" />
    checks="[a-zA-Z0-9]*" files=".*_MembersInjector.java" />
    checks="[a-zA-Z0-9]*" files=".*_ViewBinding.java" />
    checks="[a-zA-Z0-9]*Check" files=".*ResProtocal.java" />
    checks="[a-zA-Z0-9]*Check" files=".*ReqProtocal.java" />
    checks="[a-zA-Z0-9]*Check" files="[\\/]protocol[\\/]" /> 
    checks="[a-zA-Z0-9]*Check" files="[\\/]proto[\\/]" /> 

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的定义如下,

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

更多请参考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 mSubClassWhiteList = new HashSet<>();

    /**
     * 需要检查继承关系的父类的集合
     */
    private Set 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 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了。









你可能感兴趣的:(小技巧)