通过这两个参数执行以下一系列命令来获取与远程目标分支的差异文件.
- Git remoteaddupstream${
- upstreamGitUrl
- }
- Git fetch upstream${
- targetBranch
- }
- Git diff --name-only--diff-filter=dr$sourceCommitHashupstream/$targetBranch
配置远程分支别名为 UpStream, 其中 upstreamGitUrl 可以在插件提供的配置属性中设置;
获取远程目标分支的更新;
比较分支差异获取文件路径. 通过以上方式, 我们找到了增量修改文件集.
Lint 扫描原理分析
在分析 Lint 增量扫描原理之前, 先介绍一下 Lint 扫描的工作流程:
App Source Files
项目中的源文件, 包括 Java,xml, 资源文件, proGuard 等.
lint.xml
用于配置希望排除的任何 Lint 检查以及自定义问题严重级别, 一般各个项目都会根据自身项目情况自定义的 lint.xml 来排除一些检查项.
lint Tool
一套完整的扫描工具用于对 Android 的代码结构进行分析, 可以通过命令行, IDEA,Gradle 命令三种方式运行 lint 工具.
lint Output>
Lint 扫描的输出结果.
从上面可以看出, Lint Tool 就像一个加工厂, 对投入进来的原料 (源代码) 进行加工处理(各种检测器分析), 得到最终的产品(扫描结果).Lint Tool 作为一个扫描工具集, 有多种使用方式. Android 为我们提供了三种运行方式, 分别是命令行, IDEA,Gradle 任务. 这三种方式最终都殊途同归, 通过 LintDriver 来实现扫描. 如下图所示:
为了方便查看源码, 新建一个工程, 在 build.gradle 脚本中, 添加如下依赖:
- compile'com.android.tools.build:gradle:3.1.1'
- compile'com.android.tools.lint:lint-gradle:26.1.1'
我们可以得到如下所示的依赖:
lint-API-26.1.1
Lint 工具集的一个封装, 实现了一组 API 接口, 用于启动 Lint.
lint-checks-26.1.1
一组内建的检测器, 用于对这种描述好 Issue 进行分析处理.
lint-26.1.1
可以看做是依赖上面两个 jar 形成的一个基于命令行的封装接口形成的脚手架工程, 我们的命令行, Gradle 任务都是继承自这个 jar 包中相关类来做的实现.
lint-gradle-26.1.1
可以看做是针对 Gradle 任务这种运行方式, 基于 lint-26.1.1 做了一些封装类.
lint-gradle-API-26.1.1
真正 Gradle Lint 任务在执行时调用的入口.
在理解清楚了以上几个 jar 的关系和作用之后, 我们可以发现 Lint 的核心库其实是前三个依赖. 后面两个其实是基于脚手架, 对 Gradle 这种运行方式做的封装. 最核心的逻辑在 LintDriver 的 Analyze 方法中.
funanalyze(){
... 省略部分代码...
- for (projectinprojects) {
- fireEvent(EventType.REGISTERED_PROJECT,project=project)
- }
- registerCustomDetectors(projects)
... 省略部分代码...
- try{
- for (projectinprojects) {
- phase =1
- valmain = request.getMainProject(project)
- // The set of available detectors varies between projects
- computeDetectors(project)
- if(applicableDetectors.isEmpty()) {
- // No detectors enabled in this project: skip it
- continue
- }
- checkProject(project,main)
- if(isCanceled) {
- break
- }
- runExtraPhases(project,main)
- }
- } catch (throwable: Throwable) {
- // Process canceled etc
- if(!handleDetectorError(null,this,throwable)) {
- cancel()
- }
- }
... 省略部分代码...
}
主要是以下三个重要步骤:
registerCustomDetectors(projects)
Lint 为我们提供了许多内建的检测器, 除此之外我们还可以自定义一些检测器, 这些都需要注册进 Lint 工具用于对目标文件进行扫描. 这个方法主要做以下几件事情:
遍历每一个 Project 和它的依赖 Library 工程, 通过 client.findRuleJars 来找出自定义的 jar 包;
通过 client.findGlobalRuleJars 找出全局的自定义 jar 包, 可以作用于每一个 Android 工程;
从找到的 jarFiles 列表中, 解析出自定义的规则, 并与内建的 Registry 一起合并为 CompositeIssueRegistry; 需要注意的是, 自定义的 Lint 的 jar 包存放位置是 build/intermediaters/lint 目录, 如果是需要每一个工程都生效, 则存放位置为 ~/.Android/lint/ .
computeDetectors(project)
这一步主要用来收集当前工程所有可用的检测器. checkProject(project, main) 接下来这一步是最为关键的一步. 在此方法中, 调用 runFileDetectors 来进行文件扫描. Lint 支持的扫描文件类型很多, 因为是官方支持, 所以针对 Android 工程支持的比较友好. 一次 Lint 任务运行时, Lint 的扫描范围主要由 Scope 来描述. 具体表现在:
- fun infer(projects: Collection<Project>?): EnumSet<Scope> {
- if(projects ==null|| projects.isEmpty()) {
- returnScope.ALL
- }
- // Infer the scope
- var scope = EnumSet.noneOf(Scope::class.java)
- for(projectinprojects) {
- val subset = project.subset
- if(subset !=null) {
- for(fileinsubset) {
- valname= file.name
- if(name== ANDROID_MANIFEST_XML) {
- scope.add(MANIFEST)
- }elseif(name.endsWith(DOT_XML)) {
- scope.add(RESOURCE_FILE)
- }elseif(name.endsWith(DOT_JAVA) ||name.endsWith(DOT_KT)) {
- scope.add(JAVA_FILE)
- }elseif(name.endsWith(DOT_CLASS)) {
- scope.add(CLASS_FILE)
- }elseif(name.endsWith(DOT_GRADLE)) {
- scope.add(GRADLE_FILE)
- }elseif(name== OLD_PROGUARD_FILE ||name== FN_PROJECT_PROGUARD_FILE) {
- scope.add(PROGUARD_FILE)
- }elseif(name.endsWith(DOT_PROPERTIES)) {
- scope.add(PROPERTY_FILE)
- }elseif(name.endsWith(DOT_PNG)) {
- scope.add(BINARY_RESOURCE_FILE)
- }elseif(name== RES_FOLDER || file.parent == RES_FOLDER) {
- scope.add(ALL_RESOURCE_FILES)
- scope.add(RESOURCE_FILE)
- scope.add(BINARY_RESOURCE_FILE)
- scope.add(RESOURCE_FOLDER)
- }
- }
- }else{
- // Specified afullproject: just use thefullproject scope
- scope = Scope.ALL
- break
- }
- }
- }
可以看到, 如果 Project 的 Subset 为 Null,Scope 就为 Scope.ALL, 表示本次扫描会针对能检测的所有范围, 相应地在扫描时也会用到所有全部的 Detector 来扫描文件;
如果 Project 的 Subset 不为 Null, 就遍历 Subset 的集合, 找出 Subset 中的文件分别对应哪些范围. 其实到这里我们已经可以知道, Subset 就是我们增量扫描的突破点. 接下来我们看一下 runFileDetectors:
- if(scope.contains(Scope.JAVA_FILE)||scope.contains(Scope.ALL_JAVA_FILES)){
- valchecks = union(scopeDetectors[Scope.JAVA_FILE],scopeDetectors[Scope.ALL_JAVA_FILES])
- if(checks != null&&!checks.isEmpty()) {
- valfiles = project.subset
- if(files != null) {
- checkIndividualJavaFiles(project,main,checks,files)
- }else{
- valsourceFolders= project.javaSourceFolders
- valtestFolders=if(scope.contains(Scope.TEST_SOURCES))
- project.testSourceFolders
- else
- emptyList<File> ()
- valgeneratedFolders=if(isCheckGeneratedSources)
- project.generatedSourceFolders
- else
- emptyList<File> ()
- checkJava(project,main,sourceFolders,testFolders,generatedFolders,checks)
- }
- }
- }
- {
- 1
- }
这里更加明确, 如果 project.subset 不为空, 就对单独的 Java 文件扫描, 否则, 就对源码文件和测试目录以及自动生成的代码目录进行扫描. 整个 runFileDetectors 的扫描顺序入下:
- Scope.MANIFEST
- Scope.ALL_RESOURCE_FILES)|| scope.contains(Scope.RESOURCE_FILE) || scope.contains(Scope.RESOURCE_FOLDER) || scope.contains(Scope.BINARY_RESOURCE_FILE)
- scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)
- scope.contains(Scope.CLASS_FILE) || scope.contains(Scope.ALL_CLASS_FILES) || scope.contains(Scope.JAVA_LIBRARIES)
- scope.contains(Scope.GRADLE_FILE)
- scope.contains(Scope.OTHER)
- scope.contains(Scope.PROGUARD_FILE)
- scope.contains(Scope.PROPERTY_FILE)
与 [ 官方文档 ] 的描述顺序一致.
现在我们已经知道, 增量扫描的突破点其实是需要构造 project.subset 对象.
- /**
- *Adds thegiven file tothelist of files whichshouldbe checked in this
- *project. If no files areadded,thewhole projectwillbe checked.
- *
- * @param file thefile to be checked
- */
- publicvoidaddFile(@NonNullFile file) {
- if(files ==null) {
- files =newArrayList<>();
- }
- files.add(file);
- }
- /**
- *The list of files to be checked in this project. If null, thewhole
- *project shouldbe checked.
- *
- * @return thesubset of files to be checked, or null forthewhole project
- */
- @Nullable
- publicList<File> getSubset() {
- returnfiles;
- }
注释也很明确的说明了只要 Files 不为 Null, 就会扫描指定文件, 否则扫描整个工程.
Lint 增量扫描 Gradle 任务实现
前面分析了如何获取差异文件以及增量扫描的原理, 分析的重点还是侧重在 Lint 工具本身的实现机制上. 接下来分析, 在 Gradle 中如何实现一个增量扫描任务. 大家知道, 通过执行./gradlew lint 命令来执行 Lint 静态代码检测任务. 创建一个新的 Android 工程, 在 Gradle 任务列表中可以在 Verification 这个组下面找到几个 Lint 任务, 如下所示:
这几个任务就是 Android Gradle 插件在加载的时候默认创建的. 分别对应于以下几个 Task:
lint-> LintGlobalTask : 由 TaskManager 创建;
lintDebug,lintRelease,lintVitalRelease-> LintPerVariantTask : 由 ApplicationTaskManager 或者 LibraryTaskManager 创建, 其中 lintVitalRelease 只在 release 下生成.
所以, 在 Android Gradle 插件中, 应用于 Lint 的任务分别为 LintGlobalTask 和 LintPerVariantTask. 他们的区别是前者执行的是扫描所有 Variant, 后者执行只针对单独的 Variant. 而我们的增量扫描任务其实是跟 Variant 无关的, 因为我们会把所有差异文件都收集到. 无论是 LintGlobalTask 或者是 LintPerVariantTask, 都继承自 LintBaseTask. 最终的扫描任务在 LintGradleExecution 的 runLint 方法中执行, 这个类位于 lint-gradle-26.1.1 中, 前面提到这个库是基于 Lint 的 API 针对 Gradle 任务做的一些封装.
- /** Runs lint on the given variant and returns the set of warnings */
- privatePair<List<Warning>, LintBaseline> runLint(
- @Nullable Variant variant,
- @NonNull VariantInputs variantInputs,
- booleanreport,booleanisAndroid) {
- IssueRegistry registry = createIssueRegistry(isAndroid);
- LintCliFlags flags =newLintCliFlags();
- LintGradleClientclient=
- newLintGradleClient(
- descriptor.getGradlePluginVersion(),
- registry,
- flags,
- descriptor.getProject(),
- descriptor.getSdkHome(),
- variant,
- variantInputs,
- descriptor.getBuildTools(),
- isAndroid);
- booleanfatalOnly = descriptor.isFatalOnly();
- if(fatalOnly) {
- flags.setFatalOnly(true);
- }
- LintOptions lintOptions = descriptor.getLintOptions();
- if(lintOptions !=null) {
- syncOptions(
- lintOptions,
- client,
- flags,
- variant,
- descriptor.getProject(),
- descriptor.getReportsDir(),
- report,
- fatalOnly);
- }else{
- // Set up some default reporters
- flags.getReporters().add(Reporter.createTextReporter(client, flags,null,
- newPrintWriter(System.out,true),false));
- File HTML = validateOutputFile(createOutputPath(descriptor.getProject(),null,".html",
- null, flags.isFatalOnly()));
- File xml = validateOutputFile(createOutputPath(descriptor.getProject(),null, DOT_XML,
- null, flags.isFatalOnly()));
- try{
- flags.getReporters().add(Reporter.createHtmlReporter(client, HTML, flags));
- flags.getReporters().add(Reporter.createXmlReporter(client, xml,false));
- }catch(IOException e) {
- thrownewGradleException(e.getMessage(), e);
- }
- }
- if(!report || fatalOnly) {
- flags.setQuiet(true);
- }
- flags.setWriteBaselineIfMissing(report && !fatalOnly);
- Pair<List<Warning>, LintBaseline> warnings;
- try{
- warnings =client.run(registry);
- }catch(IOException e) {
- thrownewGradleException("Invalid arguments.", e);
- }
- if(report &&client.haveErrors() && flags.isSetExitCode()) {
- abort(client, warnings.getFirst(), isAndroid);
- }
- returnwarnings;
- }
我们在这个方法中看到了 warnings = client.run(registry) , 这就是 Lint 扫描得到的结果集. 总结一下这个方法中做了哪些准备工作用于 Lint 扫描:
创建 IssueRegistry, 包含了 Lint 内建的 BuiltinIssueRegistry;
创建 LintCliFlags;
创建 LintGradleClient, 这里面传入了一大堆参数, 都是从 Gradle Android 插件的运行环境中获得;
同步 LintOptions, 这一步是将我们在 build.gralde 中配置的一些 Lint 相关的 DSL 属性, 同步设置给 LintCliFlags, 给真正的 Lint 扫描核心库使用;
执行 Client 的 Run 方法, 开始扫描.
扫描的过程上面的原理部分已经分析了, 现在我们思考一下如何构造增量扫描的任务. 我们已经分析到扫描的关键点是 client.run(registry) , 所以我们需要构造一个 Client 来执行扫描. 一个想法是通过反射来获取 Client 的各个参数, 当然这个思路是可行的, 我们也验证过实现了一个用反射方式构造的 Client. 但是反射这种方式有个问题是丢失了从 Gradle 任务执行到调用 Lint API 开始扫描这一过程中做的其他事情, 侵入性比较高, 所以我们最终采用继承 LintBaseTask 自行实现增量扫描任务的方式.
FindBugs 扫描简介
FindBugs 是一个静态分析工具, 它检查类或者 JAR 文件, 通过 Apache 的 [ BCEL ] 库来分析 Class, 将字节码与一组缺陷模式进行对比以发现问题. FindBugs 自身定义了一套缺陷模式, 目前的版本 3.0.1 内置了总计 300 多种缺陷, 详细可参考 [ 官方文档 ].FindBugs 作为一个扫描的工具集, 可以非常灵活的集成在各种编译工具中. 接下来, 我们主要分析在 Gradle 中 FindBugs 的相关内容.
Gradle FindBugs 任务属性分析
在 Gradle 的内置任务中, 有一个 FindBugs 的 Task, 我们看一下 [ 官方文档 ] 对 Gradle 属性的描述. 选几个比较重要的属性介绍:
Classes
该属性表示我们要分析的 Class 文件集合, 通常我们会把编译结果的 Class 目录用于扫描.
Classpath
分析目标集合中的 Class 需要用到的所有相关的 Classes 路径, 但是并不会分析它们自身, 只用于扫描.
Effort
包含 MIN,Default,MAX, 级别越高, 分析得越严谨越耗时.
findbugsClasspath
Finbugs 库相关的依赖路径, 用于配置扫描的引擎库.
reportLevel
报告级别, 分为 Low,Medium,High. 如果为 Low, 所有 Bug 都报告, 如果为 High, 仅报告 High 优先级.
Reports
扫描结果存放路径.
通过以上属性解释, 不难发现要 FindBugs 增量扫描, 只需要指定 Classes 的文件集合就可以了.
FindBugs 任务增量扫描分析
在做增量扫描任务之前, 我们先来看一下 FindBugs IDEA 插件是如何进行单个文件扫描的.
我们选择 Analyze Current File 对当前文件进行扫描, 扫描结果如下所示:
可以看到确实只扫描了一个文件. 那么扫描到底使用了哪些输入数据呢, 我们可以通过扫描结果的提示清楚看到:
这里我们能看到很多有用的信息:
源码目录列表, 包含了工程中的 Java 目录, res 目录, 以及编译过程中生成的一些类目录;
需要分析的目标 Class 集合, 为编译后的 Build 目录下的当前 Java 文件对应的 Class 文件;
Aux Classpath Entries, 表示分析上面的目标文件需要用到的类路径.
所以, 根据 IDEA 的扫描结果来看, 我们在做增量扫描的时候需要解决上面这几个属性的获取. 在前面我们分析的属性是 Gradle 在 FindBugs lib 的基础上, 定义的一套对应的 Task 属性. 真正的 Finbugs 属性我们可以通过 [ 官方文档 ] 或者源码中查到.
配置 AuxClasspath
前文提到, ClassPath 是用来分析目标文件需要用到的相关依赖 Class, 但本身并不会被分析, 所以我们需要尽可能全的找到所有的依赖库, 否则在扫描的时候会报依赖的类库找不到.
- FileCollection buildClasses = project.fileTree(dir: "${project.buildDir}/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",includes: classIncludes)
- FileCollection targetClasspath = project.files()
- GradleUtils.collectDepProject(project, variant).each{
- targetProject ->
- GradleUtils.getAndroidVariants(targetProject).each{
- targetVariant ->
- if (targetVariant.name.capitalize().equalsIgnoreCase(variant.name.capitalize())) {
- targetClasspath += targetVariant.javaCompile.classpath
- }
- }
- }
- classpath = variant.javaCompile.classpath+ targetClasspath + buildClasses
FindBugs 增量扫描误报优化
对于增量文件扫描, 参与的少数文件扫描在某些模式规则上可能会出现误判, 但是全量扫描不会有问题, 因为参与分析的目标文件是全集. 举一个例子:
classA{ publicstaticStringbuildTime =""; .... }
静态变量 buildTime 会被认为应该加上 Final, 但是其实其他类会对这个变量赋值. 如果单独扫描类 A 文件, 就会报缺陷 BUG_TYPE_MS_SHOULD_BE_FINAL . 我们通过 FindBugs-IDEA 插件来扫描验证, 也同样会有一样的问题. 要解决此类问题, 需要找到谁依赖了类 A, 并且一同参与扫描, 同时也需要找出类 A 依赖了哪些文件, 简单来说: 需要找出与类 A 有直接关联的类 . 为了解决这个问题, 我们通过 ASM 来找出相关的依赖, 具体如下:
voidfindAllScanClasses(ConfigurableFileTree allClass) { allScanFiles = []asHashSet StringbuildClassDir ="${project.buildDir}/$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN" Set<File> moduleClassFiles = allClass.files for(File file : moduleClassFiles) { String[] splitPath = file.absolutePath.split("$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN/") if(splitPath.length>1) { StringclassName = getFileNameNoFlag(splitPath[1],'.') StringinnerClassPrefix =""if(className.contains('$')) { innerClassPrefix = className.split('\\$')[0] } if(diffClassNamePath.contains(className) || diffClassNamePath.contains(innerClassPrefix)) { allScanFiles.add(file) }else{ Iterable<String> classToResolve =newArrayList<String>() classToResolve.add(file.absolutePath) Set<File> dependencyClasses = Dependencies.findClassDependencies(project,newClassAcceptor(), buildClassDir, classToResolve) for(File dependencyClass : dependencyClasses) { if(diffClassNamePath.contains(getPackagePathName(dependencyClass))) { allScanFiles.add(file) break } } } } } }
通过以上方式, 我们可以解决一些增量扫描时出现的误报情况, 相比 IDEA 工具, 我们更进一步降低了扫描部分文件的误报率.
CheckStyle 增量扫描
相比而言, CheckStyle 的增量扫描就比较简单了. CheckStyle 对源码扫描, 根据 [ 官方文档 ] 各个属性的描述, 我们发现只要指定 Source 属性的值就可以指定扫描的目标文件.
void configureIncrementScanSource(){ boolean isCheckPR =false DiffFileFinder diffFileFinder if(project.hasProperty(CodeDetectorExtension.CHECK_PR)) { isCheckPR = project.getProperties().get(CodeDetectorExtension.CHECK_PR) } if(isCheckPR) { diffFileFinder =newDiffFileFinderHelper.PRDiffFileFinder() }else{ diffFileFinder =newDiffFileFinderHelper.LocalDiffFileFinder() } source diffFileFinder.findDiffFiles(project) if(getSource().isEmpty()) { println '没有找到差异 java 文件, 跳过 checkStyle 检测' } }
优化结果数据
经过全量扫描和增量扫描的优化, 我们整个扫描效率得到了很大提升, 一次 PR 构建扫描效率整体提升 50% . 优化数据如下:
落地与沉淀
扫描工具通用性
解决了扫描效率问题, 我们想怎么让更多的工程能 低成本 的使用这个扫描插件. 对于一个已经存在的工程, 如果没有使用过静态代码扫描, 我们希望在接入扫描插件后续新增的代码能够保证其经过增量扫描没有问题. 而老的存量代码, 由于代码量过大增量扫描并没有效率上的优势, 我们希望可以使用全量扫描逐步解决存量代码存在的问题. 同时, 为了配置工具的灵活, 也提供配置来让接入方自己决定选择接入哪些工具. 这样可以让扫描工具同时覆盖到新老项目, 保证其通用. 所以, 要同时支持配置使用增量或者全量扫描任务, 并且提供灵活的选择接入哪些扫描工具 .
扫描完整性保证
前面提到过, 在 FindBugs 增量扫描可能会出现因为参与分析的目标文件集不全导致的某类匹配规则误报, 所以在保证扫描效率的同时, 也要保证扫描的完整性和准确性. 我们的策略是 以增量扫描为主, 全量扫描为辅, PR 提交使用增量扫描提高效率, 在 CI 配置 Daily Build 使用全量扫描保证扫描完整和不遗漏 .
我们在自己的项目中实践配置如下:
apply plugin:'code-detector' codeDetector { // 配置静态代码检测报告的存放位置 reportRelativePath = rootProject.file('reports') /** * 远程仓库地址, 用于配置提交 pr 时增量检测 */ upstreamGitUrl ="ssh://git@xxxxxxxx.git" checkStyleConfig { /** * 开启或关闭 CheckStyle 检测 * 开启: true * 关闭: false */ enable =true /** * 出错后是否要终止检查 * 终止: false * 不终止: true. 配置成不终止的话 CheckStyleTask 不会失败, 也不会拷贝错误报告 */ ignoreFailures =false /** * 是否在日志中展示违规信息 * 显示: true * 不显示: false */ showViolations =true /** * 统一配置自定义的 checkstyle.xml 和 checkstyle.xsl 的 uri * 配置路径为: *"${checkStyleUri}/checkstyle.xml" *"${checkStyleUri}/checkstyle.xsl" * * 默认为 null, 使用 CodeDetector 中的默认配置 */ checkStyleUri = rootProject.file('codequality/checkstyle') } findBugsConfig { /** * 开启或关闭 Findbugs 检测 * 开启: true * 关闭: false */ enable =true /** * 可选项, 设置分析工作的等级, 默认值为 max *min, default, or max. max 分析更严谨, 报告的 bug 更多. min 略微少些 */ effort ="max" /** * 可选项, 默认值为 high *low, medium, high. 如果是 low 的话, 那么报告所有的 bug */ reportLevel ="high" /** * 统一配置自定义的 findbugs_include.xml 和 findbugs_exclude.xml 的 uri * 配置路径为: *"${findBugsUri}/findbugs_include.xml" *"${findBugsUri}/findbugs_exclude.xml" * 默认为 null, 使用 CodeDetector 中的默认配置 */ findBugsUri = rootProject.file('codequality/findbugs') } lintConfig { /** * 开启或关闭 lint 检测 * 开启: true * 关闭: false */ enable =true /** * 统一配置自定义的 lint.xml 和 retrolambda_lint.xml 的 uri * 配置路径为: *"${lintConfigUri}/lint.xml" *"${lintConfigUri}/retrolambda_lint.xml" * 默认为 null, 使用 CodeDetector 中的默认配置 */ lintConfigUri = rootProject.file('codequality/lint') } }
我们希望扫描插件可以灵活指定增量扫描还是全量扫描以应对不同的使用场景, 比如已存在项目的接入, 新项目的接入, 打包时的检测等.
执行脚本示例:
./gradlew":${appModuleName}:assemble${ultimateVariantName}"-PdetectorEnable=true-PcheckStyleIncrement=true-PlintIncrement=true-PfindBugsIncrement=true-PcheckPR=${checkPR}-PsourceCommitHash=${sourceCommitHash}-PtargetBranch=${targetBranch}--stacktrace
希望一次任务可以暴露所有扫描工具发现的问题, 当某一个工具扫描到问题后不终止任务, 如果是本地运行在发现问题后可以自动打开浏览器方便查看问题原因.
deffinalizedTaskArray= [lintTask,checkStyleTask,findbugsTask] checkCodeTask.finalizedBy finalizedTaskArray "open ${reportPath}".execute()
为了保证提交的 PR 不会引起打包问题影响包的交付, 在 PR 时触发的任务实际为打包任务, 我们将静态代码扫描任务挂接在打包任务中. 由于我们的项目是多 Flavor 构建, 在 CI 上我们将触发多个 Job 同时执行对应 Flavor 的增量扫描和打包任务. 同时为了保证代码扫描的完整性, 我们在真正的打包 Job 上执行全量扫描.
总结与展望
本文主要介绍了在静态代码扫描优化方面的一些思路与实践, 并重点探讨了对 Lint,FindBugs,CheckStyle 增量扫描的一些尝试. 通过对扫描插件的优化, 我们在代码扫描的效率上得到了提升, 同时在实践过程中我们也积累了自定义 Lint 检测规则的方案, 未来我们将配合基础设施标准化建设, 结合静态扫描插件制定一些标准化检测规则来更好的保证我们的代码规范以及质量.
参考资料
CheckStyle 插件文档
FindBugs 插件文档
Android Lint 开发介绍
Lint 增量扫描 https://juejin.im/post/5a1d4e9df265da430f31d7a2
FindBugs 配置属性介绍
美团外卖 Android Lint 代码检查实践 https://www.mtyun.com/library/waimai-android-lint
Gradle 源码 https://github.com/gradle/gradle.git
静态代码检测工具对比 http://lidu.me/2018/12/16/static-code-analyze/
Android Gradle 插件源码