让 CheckStyle 支持增量检查的一次落地经验总结

959 阅读5分钟

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

背景

大家都知道静态代码检查工具有很多,譬如阿里的 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 外参照了很多方式和官方文档都没有找到好的解决方案,找到的方案都只能在一般的分支结构工作,结构复杂后就乱套了。不知道哪位大佬有好的建议,欢迎指点一二。

在这里插入图片描述

【工匠若水 未经允许严禁转载,请尊重作者劳动成果。+微信 yanbo373131686 联系我】