我们的目的是想在代码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 { // 支持html和xml两种报告形式,可以任选其一(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 fi5.在最外层的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 Set10.到此全部OK了。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; } }