Lint增量扫描

前言

先来讲我为何要作增量扫描这个事情,毕竟代码扫描已经老生常谈了,业界方案一搜一大堆,有什么好讲的,大部人看到这篇文章的时候确定这么想吧,可是注意今天我要分享的不是全量扫描,我分享的是从无到有实现增量扫描的过程,有的时候实现一个方案历来不是重点,咱们对于方案的认知程度才是咱们本身最重要的收获 ̄▽ ̄ 。html

再来讲说怎么样的代码扫描才算是高效的,我是这么理解的:java

不能增量检查的代码扫描都是耍流氓,之前的代码一大堆问题,谁有耐心所有去解决
不能自动化的代码扫描都是欺骗咱们感情,不是每一个人都有良好的意识每次都去检查的
不能撤销提交的代码扫描都是本身骗本身,检查出来问题不改,这样的代码扫描要来何用
不能持续集成的代码扫描都是不专业的,问题要快上线了才发现,这样的代码扫描风险多高
开发缺的历来就不是工具,咱们缺的是无缝嵌入的自动化流程、自我Code Review的意识,意识比工具重要。android

这里扯了一些大道理,你们谅解,口号喊得响,你们才有兴趣看嘛。后面全是干货,你们放心,嘿嘿。git

方案介绍

OkLint做为一个Gradle插件,使用起来超简单,他能在你提交时发现增量问题,撤销提交并给你发邮件。github

在根目录下的build.gradle写web

allprojects {
    apply plugin: 'oklint'
}复制代码

方案思考

在讲具体实现以前,先来说讲我对于高效的代码扫描是怎么想的。shell

高效的代码扫描我以为有五个方案:apache

  1. 方案一是Android Studio自带的错误提示功能,他有个好处就是实时发现问题,缺点就是有些问题隐藏在花花绿绿的代码里,你要指定你想检查的问题为error才能暴露出来,这样就须要在每台电脑上都改动一下,太麻烦了。api

  2. 方案二是Android Studio的增量代码扫描功能,缺点就是不能自动化,不能在团队内很好落实,不利于统计问题和持续集成。
    android-studio

    image.png
    image.png

  3. 方案三是用Sonar持续集成,可是他有个问题是不能增量,咱们团队用过,最后由于之前问题太多根本推行不起来,相信好多团队都是这样吧。

  4. 方案四是用Android Gradle插件2.3.0之后提供的新功能 baseline,他也是全量扫描,可是他能增量显示问题,这个方案后期和Sonar持续集成,能够做为Plan B。

  5. 方案五是我如今用的方案,增量代码扫描和git hooks搭配使用,味道更好。刚开始的思路是在git commit以前扫描增量代码,结果发现lint扫描比较慢(我尝试改了,改了之后确实快了可是有些问题就扫描不到了,毕竟扫描代码仍是须要整个项目的代码才能更好的找到问题)。后面我听取了同事峰哥的意见,采用另一个思路,偷偷在git commit以后去扫描。有些人要问了为何不在gitlab上的webhook里面执行,嗯你很机智,这样实现也有很大的优势,可是我更想及时检查每一次改动,越早发现越好解决问题。

我的以为上面五个方案,方案四和方案五左右开弓效果更好。方案五负责及时检查每一次改动,方案四负责发现全量代码潜在的问题。

方案对比

方案作出来了,要是不对比一下,就没办法愉快地吹NB了。

功能 OkLint Lint命令行 Android Gradle 插件 Android Studio
增量 能够 不行 不行,2.3.0后支持全量扫描增量显示 能够
自动化 能够 不行 不行 不行
持续集成 能够 不行 不行 不行
代码回滚 能够 不行 不行 能够
只扫描优先级高的问题 能够 配置麻烦 配置麻烦 配置麻烦

Android有本身的Code Lint,可是他只能全量扫描,并且无法只扫描优先级高的。当然Android Studio能够在提交前面执行code analysis,可是做为一个团队你很难落实让每一个人每次提交代码都去执行,就算执行了你也不能保证他必定去改正这个问题,就算他改了这个问题,你也不能保证多个分支合并的代码没有问题,因此一个能自动在git commit时扫描增量代码的工具仍是颇有必要的。

方案实现


思路其实很简单的,流程很简单

gradle插件copy git hooks------> git hooks自动执行增量扫描的任务------> git diff找到增量代码------> lint-api.jar调用project.addfile() 扫描增量代码------>javamail发送问题邮件------>git reset回滚代码

好了如今你已经获得个人大乘佛法了,你能够屁颠屁颠地回大唐娶妻生子走向人生巅峰了,我保证我不阻止你。

找到增量代码

这个命令感谢个人另外一个同事马老板,他坐为旁边,我每次急躁的时候他都耐心帮我找答案。

private List<String> getPostCommitChange() {
        ArrayList<String> filterList = new ArrayList<String>()
        try {
            String projectDir = getProject().getProjectDir()
            String commond = "git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0 $projectDir"
            String changeInfo = commond.execute(null, project.getRootDir()).text.trim()
            if (changeInfo == null || changeInfo.empty) {
                return filterList
            }
            String[] lines = changeInfo.split("\\n")
            return lines.toList()
        } catch (Exception e) {
            return filterList
        }
    }复制代码

用git diff命令找到刚提交的commit都改动了哪些文件,我讲一下他的每一个参数的意思

  • git diff 比较两个commit
  • HEAD~1是前一个commit,HEAD~0是当前的commit,有个注意点HEAD~1 HEAD~0 的前后顺序,刚开始写反了,增长的文件变成了删除的文件
  • diff-filter是筛选文件类型,没写D用来去除删除的文件
  • name-only用来只列出文件名
  • projectDir必定要写,否则git不知道要找哪一个项目,并且注意我这里写的是当前module dir,确保每一个module只检查本身的改动,用来加快扫描速度和防止扫描出来重复的问题。

这里着重说一下在gralde里写命令的一个注意点,要执行带有单引号的命令会执行为空的问题
譬如

git status  -s  | grep -v '^D'//列出当前要提交的commit变更了哪些文件并排除删除的文件复制代码

你觉得"git status -s | grep -v '^D'".execute就好了吗,太天真了,执行结果为空,刚开始我觉得只要加上转义符就行,结果仍是不行。后面反复实验发现要这么写

["/bin/bash", "-c", "git status -s | grep -v '^D'"].execute()复制代码

增量代码扫描具体实现

原理比较长,怕你们看的似懂非懂,我先给结果,这样比较好。看到一些不明白的名词能够先忽略掉,后面原理里面会提,我尽可能讲的浅显易懂。
我写了一个增量扫描的task,而后写了一个LintClinet,这个LintClient会扫描代码,它继承android gradle的LintGradleClient,task会调用这个client的run方法,run方法就是扫描方法。
而增量扫描的关键性代码是修改LintGradleClientcreateLintRequest方法,往project加入要扫描的文件

@Override
    protected LintRequest createLintRequest(@NonNull List<File> files) {
//注意这个project是com.android.tools.lint.detector.api.project
  LintRequest lintRequest = super.createLintRequest(files);
        for (Project project : lintRequest.getProjects()) {
                 project.addFile(changefile);//加入要扫描的文件
                addChangeFiles(project);
        }
     return lintRequest;
    }复制代码

有个注意点我要提一下
LintGradleClient构造函数须要参数,除了variant能够为空,其余都不能为空。由于不在android gradle插件内部,因此有些参数获取须要动一些脑筋。

LintGradleClient(
             IssueRegistry registry,//扫描规则
            LintCliFlags flags,
            org.gradle.api.Project gradleProject,//gradle 项目
            AndroidProject modelProject,// android项目
            File sdkHome,// android sdk目录
            Variant variant,//编译的Variant
            BuildToolInfo buildToolInfo) {//编译工具包复制代码

篇幅有限,参数讲太多反而把你们搞糊涂,我就讲一个参数,如何获取AndroidProject

private AndroidProject getAndroidProject() {
        GradleConnector gradleConn = GradleConnector.newConnector()
        gradleConn.forProjectDirectory(getProject().getProjectDir())
        AndroidProject modelProject = gradleConn.connect().getModel(AndroidProject.class)
        return modelProject
    }复制代码

增量代码扫描原理分析

刚开始想的很简单呀,命令行 Lint不是也能扫描代码吗,那里面确定有指定扫描文件和目录的参数吧,别说还真有, --sources <dir> ,结果一试,发现是有结果,可是扫描出来的问题根本不是那个文件的问题呀,而后我同事说在他电脑却提示不能扫描gradle项目,一会儿就蒙蔽了,无从下手的感受,刚开始我觉得命令没用对,可是改来改去都不对,后面我尝试去除里面的gradle project判断限制,而后指定扫描文件,仍是扫描不出该有的问题,我就先暂停这个方案的研究。

既然上面这条路走不通,我就去找android studio的源码看他是怎么实现增量扫描的,结果在Android Studio源码里面,搜索lint根本没有找到任何相关的代码,后面发现实际上是在另外的Plugin源码里。不过他依赖于Intellij Module,Module会找到每一个类,那我又没有Module这个上下文,这么说这个方案仍是走不通。

那就再换一个思路,Android Gradle插件不是也能够实现Lint扫描,那我改一改不就能够增量扫描,结果一拿到他的代码就感受无从下手,改来改去都不对呀,不知道哪一行代码能够实现增量扫描,就算后面完成了增量扫码,扫描也很慢。

带着上面的几个坑,我研究了Lint内部的实现原理找到了增量代码扫描的实现方法

  1. 为何命令行Lint 扫描不出增量代码的问题
  2. android studio是怎么实现lint增量扫描的

我先讲一下关于Lint的预备知识,而后再来说上面几个问题,方便你们更好理解

###Lint扫描内部原理

其实不管是Lint命令行、android gradle插件、android studio都依赖了两个jar

  • lint-api.jar:lint-api是代码扫描的具体实现
  • lint-check.jar:lint-check是默认的扫描规则

    lint-api.jar内部实现原理:
    LintDriver调用analyze()分析LintRequest中的文件------>checkProject----->runFileDetectors----->check对应文件的Visitor,譬如JavaPsiVisitor分析java文件,AsmVisitor分析class文件等

下面讲讲三种方式分别怎么实现的

Lint命令行:
lint.sh------>lint.jar------>LintCliClient 的run(IssueRegistry registry, List<File> files)------>LintDriver analyze分析 project

Lint Gradle Task:
Lint.groovy------>LintGradleClient的run(IssueRegistry registry)------>LintDriver analyze分析 LintGradleProject

Android Studio:
AndroidLintGlobalInspectionContext------> performPreRunActivities-----> LintDriver analyze分析IntellijLintProject

明白了原理,咱们回到上面两个问题

  1. 为何命令行Lint 扫描不出增量代码的问题
    我举个例子:
    譬若有个TestActivity里面写了静态的activity变量,LeakDetector会去检查这个状况,可是直接lint --sources app/src/com/demo/TestActivity.java .你会发现扫描不出这个错误或者提示'app' is a Gradle project. To correctly analyze Gradle projects, you should run "gradlew :lint" instead. [LintError],其实这两个问题都是同一个缘由。
    LeakDetector会去判断静态变量是否是Activity类,可是变量的PsiField倒是com.demo.TestActivity不是'android'开头,这样就扫描不出问题了。

    @Override
         public void visitField(PsiField field) {
          String fqn= field.getType().getCanonicalText();
            if (fqn.startsWith("android.")) {//fqn变量是com.demo.TestActivity
                 if (isLeakCandidate(cls, mContext.getEvaluator())
                         && !isAppContextName(cls, field)) {
                     String message = "Do not place Android context classes in static fields; "
                             + "this is a memory leak (and also breaks Instant Run)";
                     report(field, modifierList, message);
                 }
             }
    }复制代码

    那为何fqn不是android.app.activity呢,由于lint命令行会把lib目录下面jar的class加入扫描造成抽象语法树,可是gradle项目是compile jar的,不在lib目录下面,这就是为何高版本的lint里面提示不能扫描gradle项目。这也侧面说明了命令行lint走不通

  2. android studio是怎么实现lint增量扫描的
    android studio内部会扫描IntellijLintProject中的文件,IntellijLintProject是由
    create(IntellijLintClient client, List<VirtualFile> files,Module... modules)生成的,那就只要找到文件加入project的代码就能找到增量代码扫描的方案了。

    if (project != null) {
       project.setDirectLibraries(Collections.<Project>emptyList());
       if (file != null) {
         project.addFile(VfsUtilCore.virtualToIoFile(file));
       }
    }复制代码

    那为何addfile之后LintDriver会增量扫描呢,拿java文件扫描举个例子,LintDriver会判断subset是否是为空,不为空就不扫描JavaSourceFolders,只扫描增量文件。

    List<File> files = project.getSubset();
                 if (files != null) {//判断是否是要增量扫描
                     checkIndividualJavaFiles(project, main, checks, files);
                 } else {
                     List<File> sourceFolders = project.getJavaSourceFolders();
                     List<File> testFolders = scope.contains(Scope.TEST_SOURCES)
                             ? project.getTestSourceFolders() : Collections.emptyList();
                     checkJava(project, main, sourceFolders, testFolders, checks);
                 }复制代码

只扫描优先级高的问题

虽然Lint支持配置lint.xml去忽略Issue,可是只能一个个忽略,个人方案是设置优先级低的规则为Severity.IGNORE,LintDirver会忽略Severity.IGNORE的规则

@Override
            public Severity getSeverity(Issue issue) {
                Severity severity = super.getSeverity(issue);
                if (onlyHighPriority) {
                    if (issue.getCategory().compareTo(Category.USABILITY) < 0 && issue.getPriority() > 4) {//只扫描优先级比较高的规则
                        return severity;
                    }
                    return Severity.IGNORE;
                }
                return severity;
            }复制代码

自动执行代码扫描

Git Hooks提供了post-commit实现commit以后自动执行任务,可是你会发如今post-commit里写 ./gradlew Lint,仍是要等lint任务执行完了才commit成功。我发现只要在shell脚本里加入&>/dev/null就能够后台执行了。

nohup ./gradlew  LintIncrement  &>/dev/null &复制代码

自动同步Git Hooks

若是Git Hooks脚本须要每台电脑本身去复制,这明显不利于团队合做,并且不方便后面更新脚本,我选择用Gradle命令复制到指定目录,可是这里有个问题,gradle插件能带资源文件吗,若是没有专门学过gradle说不定一时无从下手,还好我恰好之前看过fastdex里面是怎么解决的,经过getResourceAsStream能够复制Gradle插件resources下面的文件

public static void copyResourceFile(String name, File dest) throws IOException {
        FileOutputStream os = null;
        File parent = dest.getParentFile();
        if (parent != null && (!parent.exists())) {
            parent.mkdirs();
        }
        InputStream is = null;

        try {
            is = FileUtils.class.getResourceAsStream("/" + name);
            os = new FileOutputStream(dest, false);

            byte[] buffer = new byte[BUFFER_SIZE];
            int length;
            while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
            }
        } finally {
            if (is != null) {
                is.close();
            }
            if (os != null) {
                os.close();
            }
        }
    }复制代码

复制脚本installGitHooks是这样实现的,finalizedBy保证它在build任务后面自动执行,它会把/resource/post-commit文件复制到工程.git/hooks/post-commit。chmod -R +x .git/hooks/必定要写,否则没有权限

private void createGitHooksTask(Project project) {
        def preBuild = project.tasks.findByName("preBuild")

        if (preBuild == null) {
            throw new GradleException("lint need depend on preBuild and clean task")
            return
        }

        def installGitHooks = project.getTasks().create("installGitHooks")
                .doLast {

                    File postCommitFile = new File(project.rootProject.rootDir, PATH_POST_COMMIT)
                    if (lintIncrementExtension.isCheckPostCommit()) {
                      FileUtils.copyResourceFile("post-commit", postCommitFile)
                    } else {
                         if (preCommitDestFile.exists()) {
                             preCommitDestFile.delete()
                            }
                    }
                    Runtime.getRuntime().exec("chmod -R +x .git/hooks/")
                }

        preBuild.finalizedBy installGitHooks
    }复制代码

Gradle插件实现发送邮件

image.png
image.png

原来打算直接用shell脚本里面的sendmail去发送邮件的,可是听同事说若是mac上没有登陆邮箱是无法发送成功的,我就用了javamail,网上的方案大多数是在java里面实现javamail,在gradle里面发送邮件的方案比较少,我尝试了屡次才解决。

首先在gradle插件的build.gradle里面加入javamail的依赖,刚开始我是直接compile了,可是运行之后提示我没找到javamail的类,原来是要ant能找到javamail的类才行

configurations {
   antClasspath
}
dependencies {
   antClasspath 'ant:ant-javamail:1.+'
   antClasspath 'javax.activation:activation:1.1.1'
   antClasspath 'javax.mail:mail:1.+'
}
ClassLoader antClassLoader = org.apache.tools.ant.Project.class.classLoader
configurations.antClasspath.each { File jar ->
   antClassLoader.addURL( jar.toURI().toURL() )
}复制代码

而后在gralde里面执行发送任务

void send(File file) {
       getProject().ant.mail(
 from: fromMail,// 发件方
 tolist: toList,//收件方
 ccList: ccList,//抄送方
 message: message,//消息内容
 subject: subject,//标题
 mailhost: mailhost,//SMTP转发服务器
 messagemimetype: "text/html",//消息格式
 files: file.getAbsolutePath()//发送文件目录
        )
    }复制代码

这里有几个注意点

  1. mailhost填入不须要SSL 认证的smtp服务器,否则你就须要输入帐号和密码才能发送邮件
  2. message里面换行,不能用\n,由于messagemimetype是html格式,要使用<br>

发现问题回滚代码

if (lintClient.haveErrors() ) {
          "git reset HEAD~1".execute(null, project.getRootDir())
        }复制代码

如何调试gradle 插件

我原来看了几篇Lint原理分析就打算去实现增量扫描,而后发现看和作仍是不同的,中间遇到好多问题,还好gradle插件能够调试。

第一步 点击edit configurations

image.png
image.png

第二步 建立remote,默认选项就能够
image.png
image.png

第三步 在你要运行的gradle任务里面加入
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
image.png
image.png

第四步,先点击运行你要运行的gradle任务,gradle会等待你点击remote,而后就能够调试了

Lint版本变更

发现android gradle最新的几个版本对于lint作了一些优化,我顺便提一下。

  1. 2.3.0之后运行./gradlew lint会更快,Google实现了LintCharSequence来完成数据的存储和传参,实现了内存中只有一份拷贝
  2. 2.3.0之后lint-report.html是material design,更好看、更方便查问题
  3. 2.3.0之后支持baseline增量显示bug
  4. 3.0.0之后自定义lint规则就不用像原来美团的方法)同样麻烦了,官方支持
  5. 扫描会更快,uast语法树替换了如今的psi和lombok语法树

尾声

回过头来看,其实增量扫描也很简单,就一行关键性代码project.addfile(file)

最后讲一下你们关心的开源问题吧,那要等在公司内部稳定运行之后在公司Github地址开源,毕竟咱们是一款严肃的产品嘛。

相关文章
相关标签/搜索