Android 静态代码检查

Posted 麦田里的守望者-Jiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 静态代码检查相关的知识,希望对你有一定的参考价值。

文章目录

背景

随着项目的不断迭代,以及代码的增加和开发人员的增加,代码规范或代码质量的把控,是当前版本发布前必要的一环。在当前开发流程中:编码→构建→测试→发布,代码规范或代码质量相关问题,只能靠人工 Review,或灰度和线上 Bugly 反馈。人工 Review 代码,可能比较费时以及遗漏部分Case,而灰度和线上 Bugly 反馈,为时已晚。所以,要在版本发布前尽量去发现代码质量问题,避免带到线上(被动反馈),可以在构建过程之前中去添加 静态代码检查环节,让每一次的构建都能自动地去分析代码是否存在质量问题。

项目当前代码质量问题例子

下面简单例举项目中的几个代码质量问题。

重复类问题

项目中存在代码复制和粘贴的情况,例如:
common/player/src/main/java/com/xxx/xxx/player/ijk/util/ArrayUtils.java 和 类 template/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/ArrayUtils.java ,类 common/player/src/main/java/com/xxx/xxx/player/ijk/util/StringUtils.javatemplate/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/StringUtils.java,类common/player/src/main/java/com/xxx/xxx//ijk/util/CpuInfo.java和 类template/src/main/java/com/xxx/xxx/module/templatelist/ijk/util/CpuInfo.java中的代码几乎完全一样。

Java 代码问题

  1. 容易报控制针异常问题,例如:
    mainvideoeditor/src/xxx/java/com/xxx/xxx/module/editor/speed/adapter/SpeedRvAdapter.java 类中的87行:
    public void reset() 
        if (mDataList == null && mDataList.isEmpty()) 
            return;
        
        for (int i = 0; i < mDataList.size(); ++i) 
            mDataList.get(i).selected = false;
        
        notifyDataSetChanged();
    
  1. 线程不安全问题,例如:

mainvideoeditor/src/xxx/java/com/xxx/xxx/module/filter/datacenter/VisualEffectsItemHelper.java 中的29行:

public class VisualEffectsItemHelper 
    public static List<VisualEffectItem> VISUAL_EFFECTS_LIST;
    //...省略
    public static List<VisualEffectItem> getVisualEffectsList() 
        if (VISUAL_EFFECTS_LIST == null) 
            VISUAL_EFFECTS_LIST = new ArrayList<>(FX_COUNT);
            VISUAL_EFFECTS_LIST.add(new VisualEffectItem(
                    FX_RANK_BRIGHTNESS,
                    R.string.video_editor_visual_effect_fx_name_brightness,
                    R.drawable.ic_editor_visual_effect_fx_brightness,
                    EditFxVisualCustomFilterEffectHelper.get(EditVisualEffectsConst.FX_TYPE_BRIGHTNESS))
            );
         //...省略
     
  1. I/O流没有关闭问题,例如:
    mainvideoeditor/src/xxx/java/com/xxx/xxx/module/humanaction/mediacodec/utils/GlUtil.java中的234行:
  public static String readTextFromRawResource(final Context applicationContext,
                                                 @RawRes final int resourceId) 
        final InputStream inputStream =
                applicationContext.getResources().openRawResource(resourceId);
        final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String nextLine;
        final StringBuilder body = new StringBuilder();
        try 
            while ((nextLine = bufferedReader.readLine()) != null) 
                body.append(nextLine);
                body.append('\\n');
            
         catch (IOException e) 
            return null;
        
        return body.toString();
    

Kotlin 代码问题

  1. 声明异常却没有捕获,引发运行崩溃问题,例如:
    material/src/main/java/com/xxx/xxx/module/lol/LoLRemoteFragment.kt 中的97行:
    override fun onAttach(context: Context) 
        super.onAttach(context)
        mListener = if (context is OnFragmentInteractionListener) 
            context
         else 
            throw RuntimeException("$context must implement OnFragmentInteractionListener")
        
    
  1. 容易报空指针异常,例如:
    material/src/main/java/com/xxx/xxx/module/material/viewmodel/MaterialMusicViewModel.kt的36行:
    fun getMusicTab() 
        viewModelScope.launch 
            val response = try 
                repository.makeMusicTabRequest()
             catch (e: Exception) 
                null
            
            //livedata的value不能为空
            musicCategoryLiveData.value = response
        
    

预期收益

根据上面项目中的代码质量问题例子,做构建阶段前的静态代码检查,预期可以得到收益大概分为以下几点:

  1. 防止版本线上出现 Java 和 Kotlin 代码低端错误引起崩溃,如空指针,异常未捕获,部分线程,性能等问题;
  2. 防止项目中出现复制粘贴代码情况;
  3. 制定代码规范约束,统一开发人员的开发风格,方便代码的阅读和维护;
  4. 可以根据项目当前状况,扩展规则,解决日志,线程,文件,权限等使用相关问题,特别是权限的使用导致发版后在应用市场不能上架问题;
  5. 遏制重复资源,大资源,不合理资源的引入问题。

技术方案

根据项目的目前状况,静态代码检查工具需要满足的需求有至少以下几点:

  1. 增量检查,由于项目从未做过静态代码检查,全量检查会影响开发者的正常迭代时间,所以只需对修改或增加文件检查即可;
  2. 重复文件检查,由于项目部分历史原因,项目中存在复制和粘贴的代码,所以需要检查出这些代码,并把这些代码进行抽象或下沉到通用组件,不再有冗余代码;
  3. Java 和 Kotlin 代码检查,目前急需的是要去发现一些易出错(线上异常引起崩溃),性能,安全性的代码,其它的代码风格,代码设计,易用性等可以后续再检查
  4. 资源文件检查,drawable, layout, assets等资源文件,当前可以不用去检查,但是需要支持制定相关规则(检查文件大小,无用资源等),方便后续进行检查;
  5. 自定义规则检查,除了通用的规则检查外,可以自定义一些适合当前项目状况的规则去做检查,如日志,线程,文件,权限使用规范等。

技术调研

目前对于android项目来说,官方有提供 lint 静态代码检查工具,它同时支持Java 和 Kotlin 代码检查。其它比较成熟的针对 Java 代码的检查工具有:FindBugs, PMD, CheckStyleerrorprone;针对 Kotlin 代码的检查工具有:ktlint , detekt。由于项目当前是同时使用 Java 和 Kotlin 开发,所以需要综合来使用这些工具。

检查工具对比:

名称简要原理Gradle增量自定义
lintlint 工具可以检查您的 Android 项目源文件是否有潜在的 bug,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。最新版本通过UAST来分析代码,以前的版本使用过AST和PSI来分析代码支持不支持(可Hook)支持
FindBugsJava 代码检查工具,运行的是 Java 字节码,而不是源码。将字节码与一组缺陷模式进行对比来发现可能存在的问题。这些问题包括空指针引用、无限递归循环、死锁等支持支持支持
PMDJava 代码检查工具,PMD可以发现常见的编程缺陷,如未使用的变量、空捕获块、不必要的对象创建等。它主要关注Java和Apex,但支持其他六种语言。使用AST来分析代码支持支持支持
CheckStyleJava 代码检查工具,通过检查对代码编码格式,命名约定,Javadoc,类设计等方面进行代码规范和风格的检查,从而有效约束开发人员更好地遵循代码编写规范。使用AST来分析代码支持支持支持
errorproneJava 代码检查工具,在编译期间查找代码缺陷的代码检查工具 ,hook正常的build过程,错误产生时及时告知,提供修复建议,并允许基于这些修复建议制定相应模型。使用AST来分析代码支持不支持支持
ktlintKotlin代码检查工具,Kotlin 代码风格检查,遵循的 Android Kotlin 官方代码风格,可以自动格式化代码。使用Kotlin AST来分析代码支持支持支持
detektKotlin代码检查工具,高度可配置的规则集,支持不同的报告格式,检查风格和潜在性风险。使用Kotlin AST来分析代码支持支持支持

这里,对于重复类检查,主要会使用和 PMD 同一个开发者的 CPD

以上工具,根据我们项目当前现状,目前不适合接入工具有:

  • FindBugs ,由于是字节码检查,自定义规则成本高,且年久失修
  • CheckStyle,只是Java代码的规范和风格检查
  • errorprone,编译期间检查,且自定义规则成本高
  • ktlint,只是 Kotlin 代码的规范和风格检查

剩余工具相关内置规范:

  • lint:正确性、安全性、性能、易用性、无障碍性和国际化;
  • PMD:最佳做法、代码样式、设计、文档、容易出错、多线程、性能、安全、其他规则集;
  • CPD :Java 和 Kotlin 重复代码;
  • detekt:代码注释和文档、复杂代码、协程、空代码块、异常、格式化代码、命名规范、性能,潜在性bug、风格。

根据项目当前的情况,第一期只检查正确性,安全性,性能,多线程,容易出错,协程,异常,潜在性bug问题。等问题收敛后,再进一步检查易用性,设计,复杂代码等,以及后续自定义规则检查

技术实施

总体流程

在静态代码检查初期,最主要的是自动化检查,且不影响常规开发。所以把检查放在CI 构建阶段之前的分析阶段中,在分析阶段去创建一个静态代码检查任务,每当代码 push 到 Gitlab,就去触发这个任务。下面为总体流程:

技术细节

首先,根据项目的上下文环境,去集成每一个工具。关于重复代码检查,检查的是项目中的所有Java 和 Kotlin 文件(全量检查),所以要把 CPD 这个工具独立集成和触发。关于 Java 和 Kotlin 代码增量(新增或修改)检查, 需要把得到的增量文件,分发给 lint、PMD、detekt 工具去检查。 另外,lint 工具 不支持增量检查,还需要开发适配 lint 增量检查的插件。

下面为各个工具接入的简单概况,不介绍细节。

CPD 重复代码检查

根据 CPD 接入文档,以及它的 CLI 。新建 cpd.gradle 文件(放在.codequality目录下,后面的都一样),编写相关脚本(这里只做核心介绍):

    //将收集到项目 java 和 kotlin 文件列表分别交给 cpd

     runcpd(javalist, 340, 'java')  String output ->
            reportLines.addAll(output.readLines())
        
     runcpd(kotlinlist, 210, 'kotlin')  String output ->
            reportLines.addAll(output.readLines())
        

    void runcpd(File filelist, int tokens, String language, Closure closure) 
        def out = new ByteArrayOutputStream(1024)
        project.javaexec  spec ->
            spec.main = "net.sourceforge.pmd.cpd.CPD"
            spec.maxHeapSize = '2g'
            spec.classpath = this.classpath.get()
            spec.args '--minimum-tokens', tokens == 0 ? '200' : tokens
            spec.args '--language', language ?: 'java'
            spec.args '--format csv --failOnViolation false'.tokenize()
            spec.args '--filelist', filelist.path
            spec.args '--skip-lexical-errors', '--ignore-annotations'
            spec.standardOutput = out
            spec.ignoreExitValue = true
        
        closure.call(out.toString("UTF-8"))
        logger.lifecycle("CPD run for $filelist")
    

使用 cpd 需要的 依赖有:

 'net.sourceforge.pmd:pmd-core:6.36.0'
 'net.sourceforge.pmd:pmd-java:6.36.0'
 'net.sourceforge.pmd:pmd-kotlin:6.10.0'

可以在 它的 github 上去下载最新版本:https://github.com/pmd/pmd/releases/tag/pmd_releases/6.49.0。需要把这些添加到 gradledependencies 中,然后才能使用 project.javaexec 去执行。

PMD Java 代码检查

根据 PMD 的 gradle 接入文档,再结合项目环境,新建 pmd.gradle 文件(放在.codequality目录下):

/*
 * des : inspect java code
 * doc : https://pmd.github.io/latest/pmd_userdocs_tools_gradle.html
 * use : should apply this script in root build.gradle
 * how to analyze result : https://pmd.github.io/latest/pmd_userdocs_report_formats.html , see html example.
 */
apply plugin: 'pmd'

def pmdConfigPath = new File(codequality, 'scripts/pmd').path

task pmd(type: Pmd) 
    FileCollection fileCollection
    //获取到 git diff 后的 java 文件
    if (project.hasProperty("commitJavaFiles")) 
        fileCollection = project.files(project.property("commitJavaFiles"))
     else 
       //当前项目下的 src 目录下的所有文件,注意这里可能有 flavor
        fileCollection = project.files("src")
    
    if (fileCollection == null) 
        return
    
    //区分是 CI 环境,还是本地环境
    def isCiServer = false
    if (project.hasProperty("isCiServer")) 
        isCiServer = project.property("isCiServer")
    
    if (isCiServer) 
        ignoreFailures = false
     else 
        ignoreFailures = true
    
    consoleOutput = true
    //规则集
    ruleSetFiles = files("$pmdConfigPath/pmd-ruleset.xml")
    ruleSets = []
    source fileCollection
    include '**/*.java'
    exclude '**/gen/**'

    reports 
        xml.getRequired().set(false)
        html.getRequired().set(true)
        //结果输出到项目根目录下的 build/reports/pmd 文件夹中,只需要html格式的结果文档
        def destination = new File(new File("$project.rootProject.buildDir.path/reports/pmd"), "$project.name-pmd.html")
        html.destination(destination)
    

规则集的配置,在同目录下创建 scripts/pmd/pmd-ruleset.xml

<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Custom Rules"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

    <description>
        Custom ruleset for Android application
    </description>

    <!-- rule des :https://pmd.github.io/pmd-6.41.0/pmd_rules_java.html -->
    <exclude-pattern>.*/R.java</exclude-pattern>
    <exclude-pattern>.*/gen/.*</exclude-pattern>
    <!-- Your rules will come here -->
    <!--     容易出现运行时错误 -->
    <rule ref="category/java/errorprone.xml">
        <exclude name="BeanMembersShouldSerialize" />
    </rule>
    <!--    多线程时问题-->
    <rule ref="category/java/multithreading.xml" />
    <!--    需要优化代码的地方-->
    <rule ref="category/java/performance.xml" />
    <!--    安全问题-->
    <rule ref="category/java/security.xml" />
</ruleset>

这里前期只配置:容易出现运行时错误,多线程时问题,需要优化代码的地方,安全问题规则。

detekt Kotlin 代码检查

根据 detekt 的 gradle 接入文档,首先在项目根目录下配置 classpath

buildscript 
    repositories 
        gradlePluginPortal()
    
    dependencies 
        classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0"
    

然后新建 detekt.grale 文件:

/*
 * des : inspect kotlin code
 * doc : https://github.com/detekt/detekt
 * use : should apply this script in root build.gradle
 * how to analyze result : https://detekt.dev/docs/introduction/reporting , see html example.
 */
apply plugin: 'io.gitlab.arturbosch.detekt'

def detektConfigPath = new File(codequality, 'scripts/detekt').path

detekt 
    //获取到 git diff 后的 kotlin 文件
    if (project.hasProperty("commitKotlinFiles")) 
        source = project.files(project.property("commitKotlinFiles"))
    
    buildUponDefaultConfig = true // preconfigure defaults
    //规则集
    config = files("$detektConfigPath/detekt.yml")
    // point to your custom config defining rules to run, overwriting default behavior
    baseline = file("$detektConfigPath/baseline.xml")
    basePath = projectDir.path
    def isCiServer = false
    //区分是 CI 环境,还是本地环境
    if (project.hasProperty("isCiServer")) 
        isCiServer = project.property("isCiServer")
    
    if (isCiServer) 
        ignoreFailures = false
     else 
        ignoreFailures = true
    


tasks.detekt.jvmTarget = "1.8"

tasks.named("detekt").configure 
    reports 
        // Enable/Disable HTML report (default: true)
        def destination = new File(new File("$project.rootProject.buildDir.path/reports/detekt"), "$project.name-detekt.html")
        html.required.set(true)
        html.outputLocation.set(destination)
    

规则集的配置,在同目录下创建 scripts/detekt/detekt.xml

#whiteProjectListbuild:
#  maxIssues: 0
#  excludeCorrectable: false
#  weights:
  # complexity: 2
  # LongParameterList: 1
  # style: 1
  # comments: 1

config:
  validation: true
  warningsAsErrors: false
#  checkExhaustiveness: false
  # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
  excludes: ''

processors:
  active: true
  exclude:
    - 'DetektProgressListener'
  # - 'KtFileCountProcessor'
  # - 'PackageCountProcessor'
  # - 'ClassCountProcessor'
  # - 'FunctionCountProcessor'
  # - 'PropertyCountProcessor'
  # - 'ProjectComplexityProcessor'
  # - 'ProjectCognitiveComplexityProcessor'
  # - 'ProjectLLOCProcessor'
  # - 'ProjectCLOCProcessor'
  # - 'ProjectLOCProcessor'
  # - 'ProjectSLOCProcessor'
  # - 'LicenseHeaderLoaderExtension'

console-reports:
  active: true
  exclude:
    - 'ProjectStatisticsReport'
    - 'ComplexityReport'
    - 'NotificationReport'
    - 'FindingsReport'
    - 'FileBasedFindingsReport'
  #  - 'LiteFindingsReport'

output-reports:
  active: true
  exclude:
  # - 'TxtOutputReport'
  # - 'XmlOutputReport'
  # - 'HtmlOutputReport'
  # - 'MdOutputReport'
#协程问题
coroutines:
  active: true
  GlobalCoroutineUsage:
    active: false
  InjectDispatcher:
    active: true
    dispatcherNames:
      - 'IO'
      - 'Default'
      - 'Unconfined'
  RedundantSuspendModifier:
    active: true
  SleepInsteadOfDelay:
    active: true
  SuspendFunWithCoroutineScopeReceiver:
    active: false
  SuspendFunWithFlowReturnType:
    active: true
#空代码块问题
empty-blocks:
  active: true
  EmptyCatchBlock:
    active: true
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  EmptyClassBlock:
    active: true
  EmptyDefaultConstructor:
    active: true
  EmptyDoWhileBlock:
    active: true
  EmptyElseBlock:
    active: true
  EmptyFinallyBlock:
    active: true
  EmptyForBlock:
    active: true
  EmptyFunctionBlock:
    active: true
    ignoreOverridden: true
  EmptyIfBlock:
    active: true
  EmptyInitBlock:
    active: true
  EmptyKtFile:
    active: true
  EmptySecondaryConstructor:
    active: true
  EmptyTryBlock:
    active: true
  EmptyWhenBlock:
    active: true
  EmptyWhileBlock:
    active: true
#异常问题
exceptions:
  active: true
  ExceptionRaisedInUnexpectedLocation:
    active: true
    methodNames:
      - 'equals'
      - 'finalize'
      - 'hashCode'
      - 'toString'
  InstanceOfCheckForException:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
  NotImplementedDeclaration:
    active: false
  ObjectExtendsThrowable:
    active: false
  PrintStackTrace:
    active: true
  RethrowCaughtException:
    active: true
  ReturnFromFinally:
    active: true
    ignoreLabeled: false
  SwallowedException:
    active: true
    ignoredExceptionTypes:
      - 'InterruptedException'
      - 'MalformedURLException'
      - 'NumberFormatException'
      - 'ParseException'
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  ThrowingExceptionFromFinally:
    active: true
  ThrowingExceptionInMain:
    active: false
  ThrowingExceptionsWithoutMessageOrCause:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
    exceptions:
      - 'ArrayIndexOutOfBoundsException'
      - 'Exception'
      - 'IllegalArgumentException'
      - 'IllegalMonitorStateException'
      - 'IllegalStateException'
      - 'IndexOutOfBoundsException'
      - 'NullPointerException'
      - 'RuntimeException'
      - 'Throwable'
  ThrowingNewInstanceOfSameException:
    active: true
  TooGenericExceptionCaught:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
    exceptionNames:
      - 'ArrayIndexOutOfBoundsException'
      - 'Error'
      - 'Exception'
      - 'IllegalMonitorStateException'
      - 'IndexOutOfBoundsException'
      - 'NullPointerException'
      - 'RuntimeException'
      - 'Throwable'
    allowedExceptionNameRegex: '_|(ignore|expected).*'
  TooGenericExceptionThrown:
    active: true
    exceptionNames:
      - 'Error'
      - 'Exception'
      - 'RuntimeException'
      - 'Throwable'
#性能问题
performance:
  active: true
  ArrayPrimitive:
    active: true
  CouldBeSequence:
    active: false
    threshold: 3
  ForEachOnRange:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
  SpreadOperator:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
  UnnecessaryTemporaryInstantiation:
    active: true
#潜在bug问题
potential-bugs:
  active: true
  AvoidReferentialEquality:
    active: true
    forbiddenTypePatterns:
      - 'kotlin.String'
  CastToNullableType:
    active: false
  Deprecation:
    active: false
  DontDowncastCollectionTypes:
    active: false
  DoubleMutabilityForCollection:
    active: true
    mutableTypes:
      - 'kotlin.collections.MutableList'
      - 'kotlin.collections.MutableMap'
      - 'kotlin.collections.MutableSet'
      - 'java.util.ArrayList'
      - 'java.util.LinkedHashSet'
      - 'java.util.HashSet'
      - 'java.util.LinkedHashMap'
      - 'java.util.HashMap'
  DuplicateCaseInWhenExpression:
    active: true
  ElseCaseInsteadOfExhaustiveWhen:
    active: false
  EqualsAlwaysReturnsTrueOrFalse:
    active: true
  EqualsWithHashCodeExist:
    active: true
  ExitOutsideMain:
    active: false
  ExplicitGarbageCollectionCall:
    active: true
  HasPlatformType:
    active: true
  IgnoredReturnValue:
    active: true
    restrictToAnnotatedMethods: true
    returnValueAnnotations:
      - '*.CheckResult'
      - '*.CheckReturnValue'
    ignoreReturnValueAnnotations:
      - '*.CanIgnoreReturnValue'
    ignoreFunctionCall: []
  ImplicitDefaultLocale:
    active: true
  ImplicitUnitReturnType:
    active: false
    allowExplicitReturnType: true
  InvalidRange:
    active: true
  IteratorHasNextCallsNextMethod:
    active: true
  IteratorNotThrowingNoSuchElementException:
    active: true
  LateinitUsage:
    active: false
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
    ignoreOnClassesPattern: ''
  MapGetWithNotNullAssertionOperator:
    active: true
  MissingPackageDeclaration:
    active: false
    excludes: ['**/*.kts']
  MissingWhenCase:
    active: true
    allowElseExpression: true
  NullCheckOnMutableProperty:
    active: false
  NullableToStringCall:
    active: false
  RedundantElseInWhen:
    active: true
  UnconditionalJumpStatementInLoop:
    active: false
  UnnecessaryNotNullOperator:
    active: true
  UnnecessarySafeCall:
    active: true
  UnreachableCatchBlock:
    active: true
  UnreachableCode:
    active: true
  UnsafeCallOnNullableType:
    active: true
    excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
  UnsafeCast:
    active: true
  UnusedUnaryOperator:
    active: true
  UselessPostfixExpression:
    active: true
  WrongEqualsTypeParameter:
    active: true

这里前期只配置:协程问题,空代码问题,异常问题,性能问题,潜在bug问题规则。

lint Java 和 Kotlin 代码检查

根据 lint 的 gradle 接入文档,新建 lint.grale 文件:

/*
 * des : inspect java and kotlin code by increment
 * doc :  https://juejin.cn/post/7093133800812576781#heading-8
 * use : should apply this script in root build.gradle
 * custom lint plugin : https://github.com/googlesamples/android-custom-lint-rules
 * */
project.afterEvaluate 

    if (!(project.getPlugins().hasPlugin('com.android.application') ||
            project.getPlugins().hasPlugin('com.android.library'))) 
        return
    
    //自定义的 lint 增量插件
    project.apply plugin: 'com.xxx.xxx.lint'

    def lintConfigPath = new File(codequality, 'scripts/lint').path
    project.android 
        //doc : https://developer.android.google.cn/reference/tools/gradle-api/7.1/com/android/build/api/dsl/LintOptions?hl=en
        lint 
            // true:关闭lint报告的分析进度
            quiet true
            // true:错误发生后停止gradle构建
            abortOnError false
            // true:只报告error
            ignoreWarnings true
            // true:忽略有错误的文件的全/绝对路径(默认是true)
            absolutePaths true
            // true:检查所有问题点,包含其他默认关闭项
            checkAllWarnings false
            // true:所有warning当做error
            warningsAsErrors false
            // 关闭指定问题检查
            disable 'Usability', 'Accessibility', 'Internationalization','ResourceName'
            // true:error输出文件不包含源码行号
            noLines false
            // true:显示错误的所有发生位置,不截取
            showAll true
            // 回退lint设置(默认规则)
            lintConfig file("$lintConfigPath/default-lint.xml")
            // true:生成txt格式报告(默认false)
            textReport true
            // true:生成XML格式报告
            xmlReport false
            // true:生成HTML报告(带问题解释,源码位置,等)
            htmlReport true
            // html报告可选路径(构建器默认是lint-results.html )
            def destination = new File(new File("$project.rootProject.buildDir.path/reports/lint"), "$project.name-lint.html")
            htmlOutput file(destination)
            //  true:所有正式版构建执行规则生成崩溃的lint检查,如果有崩溃问题将停止构建
            checkReleaseBuilds true
            checkDependencies false
        
    

由于 lint 不支持增量文件检查,这里需要 hook 原 lint plugin 中添加检查文件的地方,hook 原理可以查看AGP7.0增量Lint适配,这里不具体介绍。当拦截到 lint 插件中 Main 类的 run 方法时,通过 LintRequest 获取 project,然后将添加增量文件,此时 lint 就只会检查增量文件,不会检查项目所有文件。那么,为了适配 lint 增量检查需要做的就是,在运行 lint task 之前,将 git diff 出的文件列表,存储到一个本地文件中,然后运行 lint task 的时候,去读取这个文件中的文件列表,然后添加到 project 检查文件列表中,这样就实现了 lint 增量检查。这里不再展开细节。

统一触发检查工具

接入完 CPD,PMD,detekt,lint 工具后,就是这么去做增量检查(版本迭代,新增或修改文件检查),这里主要结合 git log 命令。再新建code_inspect_increment.gradle文件(还是放在.codequality目录下)

ext 
    //存储增量的Java文件,给pmd使用
    commitJavaFiles = new ArrayList<String>()
    //存储增量的kotlin文件,给detekt使用
    commitKotlinFiles = new ArrayList<String>()
    commitAuthor = ""
    appVersionName = ""

/**
 * 增量开关
 */
def isIncrement = false
/**
 * 是否是CI环境
 * @return true是,否则不是
 */
def isCiServer() 
    def isCiServer = false
    if (project.hasProperty("isCiServer")) 
        isCiServer = project.property("isCiServer")
    
    return isCiServer

/**
 * 通过cmd命令获取结果值
 * @return string 结果值
 */
def getResultFromCmd(String cmd) 
    logger.lifecycle("$project.name cmd:$cmd")
    def out = new ByteArrayOutputStream()
    if (System.properties['os.name'].toLowerCase().contains('windows')) 
        project.exec 
            ExecSpec execSpec ->
                executable 'cmd'
                args '/c', "$cmd"
                standardOutput = out
        .assertNormalExitValue()
     else 
        project.exec 
            ExecSpec execSpec ->
                executable 'bash'
                args '-c', "$cmd"
                standardOutput = out
        .assertNormalExitValue()
    
    return out.toString().trim()

/**
 * 获取提交作者
 * @return 提交作者
 */
def getCommitAuthor() 
    def commitAuthor Android 静态代码检查

Android代码静态检查(lintCheckstylektlintDetekt)

Android代码静态检查(lintCheckstylektlintDetekt)

静态分析工具汇总

动态静态检查

Java静态检测工具/Java代码规范和质量检查简单介绍(转)