工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~
背景
大家都知道静态代码检查工具有很多,譬如阿里的 p3c、sonar 挂钩的一堆插件等。但是这些东西对于一个已存在的项目不够友好,因为旧代码一扫描会出现一堆问题,修复带来的成本又很高,所以这些工具都比较适合新项目或者初期介入,对于老项目就显得很蛋疼了。
因此有必要做到增量检查;一种就是针对版本控制的 changed 进行增量,这种情况会涉及到老文件修改一处全部问责的问题;另一种是针对新增文件进行增量,这种情况保证了从此刻开始新文件的约束。
由于单纯靠大家的自觉性是很难保证规范化的,即便 review 代码也总是会有疏忽,所以静态代码检查就有必要做到方便开发的同时也方便测试验收,故而需要二次自动化把关,所以经过调研最终选择了 checkstyle 进行定制。
经过定制后可以做到本地 IDE 开发实时新增文件规范提示,CI 构建机通过 gradle task 生成报告;一次主动规范化和一次强制报告检测就做到了严格执行,直到修复完才允许进入测试流程。
checkstyle
关于它是什么?怎么用?能干啥?这里不再详细介绍,重点关注其源码及配置可以在哪里获取。
通过分析上面主要流程和文档,我们进行了简单二次定制来适应新增加文件的规范检查,主要新增了 gradle 新增文件检查和 IDEA 插件新增文件检查,具体如下文。
gradle 脚本增量定制
gradle 自身是内置了 checkstyle 插件的,我们要做的就是新增文件增量检查,具体核心 task 如下:
......
apply plugin: 'checkstyle'
def checkStyleFile = rootProject.file("checks.xml")
def reportHtmlFile = rootProject.file(project.buildDir.path + "/checkStyle/result.html")
def reportXmlFile = rootProject.file(project.buildDir.path + "/checkStyle/result.xml")
checkstyle {
toolVersion "8.12"
configFile checkStyleFile
showViolations = true
ignoreFailures = true
}
tasks.withType(Checkstyle) {
classpath = files()
source "${project.rootDir}"
exclude '**/build/**'
exclude '**/androidTest/**'
exclude "**/test/**"
reports {
html {
enabled true
destination reportHtmlFile
}
xml {
enabled true
destination reportXmlFile
}
}
}
task checkstyleAll(type: Checkstyle) {
}
task checkstyleVcsChanged(type: Checkstyle) {
def changedFiles = getFilterFiles('dr')
if (changedFiles.isEmpty()) {
enabled = false
}
include changedFiles
}
task checkstyleVcsNew(type: Checkstyle) {
def changedFiles = getFilterFiles('A')
if (changedFiles.isEmpty()) {
enabled = false
}
include changedFiles
}
def getFilterFiles(diffFilter) {
def files = getChangedFiles(diffFilter)
if (files.isEmpty()) {
return files
}
List<String> filterFiles = new ArrayList<>()
for (file in files) {
if (file.endsWith(".java") || file.endsWith(".xml")) {
filterFiles.add(file)
}
}
println("\nDiff need checkstyle filter "+diffFilter+"->"+filterFiles)
return filterFiles
}
def getChangedFiles(diffFilter) {
def targetBranch = System.getenv("ghprbTargetBranch")
def sourceBranch = System.getenv("ghprbSourceBranch")
println("Target branch: $targetBranch")
println("Source branch: $sourceBranch")
List<String> files = new ArrayList<>()
if (targetBranch.isEmpty()) {
return files
}
def systemOutStream = new ByteArrayOutputStream()
def command = "git diff --name-status --diff-filter=$diffFilter...$targetBranch $sourceBranch"
command.execute().waitForProcessOutput(systemOutStream, System.err)
def allFiles = systemOutStream.toString().trim().split('\n')
systemOutStream.close()
Pattern statusPattern = Pattern.compile("(\\w)\\t+(.+)")
for (file in allFiles) {
Matcher matcher = statusPattern.matcher(file)
if (matcher.find()) {
files.add(matcher.group(2))
}
}
println("\nDiff filter "+diffFilter+"->"+files)
files
}
......
有了上面的 task 我们就可以通过命令行生成检查报告了。在 Android 开发中,我们可以直接将这个 task 自动介入 build 流程,具体如下:
......
project.afterEvaluate {
preBuild.dependsOn 'checkstyleVcsNew' //增量任务名
}
......
由此就可以在构建机上自动进行报告产出,以便测试进行督促修复。
IDEA 插件增量定制
幸运的是有人针对 checkstyle 开发了 IDEA 插件,而 Android Studio 是基于 IDEA 定制的,所以我们可以对这个插件进行二次开发定制,以便支持新增文件增量检查,具体定制代码如下:
/**
* 老项目中已存在大量代码已经无法一次性修复所有格式检查,故而对所有新增 java 文件进行严格格式检查。
* 仅支持 VCS 版本控制项目。
* 本文件实现参照 checkstyle-IDEA 插件源码的 CheckStyleInspection.java 类
*/
public class CheckStyleVcsNewInspection extends LocalInspectionTool {
private static final Logger LOG = Logger.getInstance(CheckStyleVcsNewInspection.class);
private static final List<Problem> NO_PROBLEMS_FOUND = Collections.emptyList();
private final CheckStyleInspectionPanel configPanel = new CheckStyleInspectionPanel();
private CheckStylePlugin plugin(final Project project) {
final CheckStylePlugin checkStylePlugin = project.getComponent(CheckStylePlugin.class);
if (checkStylePlugin == null) {
throw new IllegalStateException("Couldn't get checkstyle plugin");
}
return checkStylePlugin;
}
@Nullable
public JComponent createOptionsPanel() {
return configPanel;
}
@Override
public ProblemDescriptor[] checkFile(@NotNull final PsiFile psiFile,
@NotNull final InspectionManager manager,
final boolean isOnTheFly) {
if (isVcsNewFile(psiFile)) {
final Module module = moduleOf(psiFile);
return asProblemDescriptors(asyncResultOf(() -> inspectFile(psiFile, module, manager), NO_PROBLEMS_FOUND), manager);
}
return null;
}
//重点,自己需要别的增量形式时自己修改这里吧!!!
private boolean isVcsNewFile(PsiFile psiFile) {
if (psiFile == null) {
return false;
}
final FileStatus fileStatus = FileStatusManager.getInstance(psiFile.getProject()).getStatus(psiFile.getVirtualFile());
boolean ret = fileStatus == FileStatus.UNKNOWN || fileStatus == FileStatus.ADDED;
LOG.debug(" VCS file "+psiFile.getName()+" status: "+ret+", fileStatus="+fileStatus.getText());
return ret;
}
@Nullable
private Module moduleOf(@NotNull final PsiFile psiFile) {
return ModuleUtil.findModuleForPsiElement(psiFile);
}
@Nullable
public List<Problem> inspectFile(@NotNull final PsiFile psiFile,
@Nullable final Module module,
@NotNull final InspectionManager manager) {
LOG.debug("Inspection has been invoked.");
final CheckStylePlugin plugin = plugin(manager.getProject());
ConfigurationLocation configurationLocation = null;
final List<ScannableFile> scannableFiles = new ArrayList<>();
try {
configurationLocation = plugin.getConfigurationLocation(module, null);
if (configurationLocation == null || configurationLocation.isBlacklisted()) {
return NO_PROBLEMS_FOUND;
}
scannableFiles.addAll(ScannableFile.createAndValidate(singletonList(psiFile), plugin, module));
return checkerFactory(psiFile.getProject())
.checker(module, configurationLocation)
.map(checker -> checker.scan(scannableFiles, plugin.configurationManager().getCurrent().isSuppressErrors()))
.map(results -> results.get(psiFile))
.map(this::dropIgnoredProblems)
.orElse(NO_PROBLEMS_FOUND);
} catch (ProcessCanceledException | AssertionError e) {
LOG.debug("Process cancelled when scanning: " + psiFile.getName());
return NO_PROBLEMS_FOUND;
} catch (CheckStylePluginParseException e) {
LOG.debug("Parse exception caught when scanning: " + psiFile.getName(), e);
return NO_PROBLEMS_FOUND;
} catch (Throwable e) {
handlePluginException(e, psiFile, plugin, configurationLocation, manager.getProject());
return NO_PROBLEMS_FOUND;
} finally {
scannableFiles.forEach(ScannableFile::deleteIfRequired);
}
}
private List<Problem> dropIgnoredProblems(final List<Problem> problems) {
return problems.stream()
.filter(problem -> problem.severityLevel() != SeverityLevel.Ignore)
.collect(toList());
}
private void handlePluginException(final Throwable e,
final @NotNull PsiFile psiFile,
final CheckStylePlugin plugin,
final ConfigurationLocation configurationLocation,
final @NotNull Project project) {
if (e.getCause() != null && e.getCause() instanceof FileNotFoundException) {
disableActiveConfiguration(plugin, project);
} else if (e.getCause() != null && e.getCause() instanceof IOException) {
showWarning(project, message("checkstyle.file-io-failed"));
blacklist(configurationLocation);
} else {
LOG.warn("CheckStyle threw an exception when scanning: " + psiFile.getName(), e);
showException(project, e);
blacklist(configurationLocation);
}
}
private void disableActiveConfiguration(final CheckStylePlugin plugin, final Project project) {
plugin.configurationManager().disableActiveConfiguration();
showWarning(project, message("checkstyle.configuration-disabled.file-not-found"));
}
private void blacklist(final ConfigurationLocation configurationLocation) {
if (configurationLocation != null) {
configurationLocation.blacklist();
}
}
@NotNull
private ProblemDescriptor[] asProblemDescriptors(final List<Problem> results, final InspectionManager manager) {
return ofNullable(results)
.map(problems -> problems.stream()
.map(problem -> problem.toProblemDescriptor(manager))
.toArray(ProblemDescriptor[]::new))
.orElseGet(() -> ProblemDescriptor.EMPTY_ARRAY);
}
private CheckerFactory checkerFactory(final Project project) {
return ServiceManager.getService(project, CheckerFactory.class);
}
}
接着在 plugin 配置文件新增加如下配置:
<localInspection implementationClass="org.infernus.idea.checkstyle.CheckStyleVcsNewInspection"
bundle="org.infernus.idea.checkstyle.CheckStyleBundle"
key="inspection.cvs.display-name"
groupKey="inspection.group"
level="WARNING"
enabledByDefault="true"/>
重新编译发布插件即可,想要再改别的也可以自行修改定制。关于 IDEA 插件如何开发,后面有机会再介绍吧。
接着安装插件,我们就可以做到增量新增文件的规范检查,如下设置:
然后我们就可以愉快的使用了,具体效果参见原 checkstyle idea plugin,只是多了增量操作。
总结
至此就完成了一套完整的增量式代码规范检查的流程,而规范规则可以自己继续依照 checkstyle 的 xml 进行编写,不过一定要注意 xml 中属性的版本兼容性问题,具体可以参考官方文档。
当然,你如果觉得仅对新增文件检查不合适,则可以对上面核心代码的版本 changed 部分进行自己的配置修改,譬如对 changed 的文件都进行检查,或者是指定白名单目录等,这些都完全取决于你自己的团队需求。
至此就落地了代码规范检查,其中踩坑问题主要是 git 基线分支名获取问题,除过标准 CI 外参照了很多方式和官方文档都没有找到好的解决方案,找到的方案都只能在一般的分支结构工作,结构复杂后就乱套了。不知道哪位大佬有好的建议,欢迎指点一二。