Android自定义lint开发

2,647 阅读6分钟

我自己写的静态扫描的demo

简介

Android Lint 是 SDK Tools 16(ADT 16)开始引入的一个代码扫描工具,通过对代码进行静态分析,可以帮助开发者发现代码质量问题和提出一些改进建议。除了检查 Android 项目源码中潜在的错误,对于代码的正确性、安全性、性能、易用性、便利性和国际化方面也会作出检查。

最近在项目开发过程中发现如果要推进一些中间件的对接工作,不给业务方提供一些对应的工具的情况下,很难推动代码更新迭代的过程。所以我打算通过lint静态代码检查工具,将业务端内出现了我们想改进的地方的代码标错,然后根据错误提示,帮助他们修改以及升级业务代码。

lint配置

对于执行 Lint 操作的相关配置,是定义在 gradle 文件的 lintOptions 中,可定义的选项及其默认值

android {
    lintOptions {
        // 设置为 true,则当 Lint 发现错误时停止 Gradle 构建
        abortOnError false
        // 设置为 true,则当有错误时会显示文件的全路径或绝对路径 (默认情况下为true)
        absolutePaths true
        // 仅检查指定的问题(根据 id 指定)
        check 'NewApi', 'InlinedApi'
        // 设置为 true 则检查所有的问题,包括默认不检查问题
        checkAllWarnings true
        // 设置为 true 后,release 构建都会以 Fatal 的设置来运行 Lint。
        // 如果构建时发现了致命(Fatal)的问题,会中止构建(具体由 abortOnError 控制)
        checkReleaseBuilds true
        // 不检查指定的问题(根据问题 id 指定)
        disable 'TypographyFractions','TypographyQuotes'
        // 检查指定的问题(根据 id 指定)
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // 在报告中是否返回对应的 Lint 说明
        explainIssues true
        // 写入报告的路径,默认为构建目录下的 lint-results.html
        htmlOutput file("lint-report.html")
        // 设置为 true 则会生成一个 HTML 格式的报告
        htmlReport true
        // 设置为 true 则只报告错误
        ignoreWarnings true
        // 重新指定 Lint 规则配置文件
        lintConfig file("default-lint.xml")
        // 设置为 true 则错误报告中不包括源代码的行号
        noLines true
        // 设置为 true 时 Lint 将不报告分析的进度
        quiet true
        // 覆盖 Lint 规则的严重程度,例如:
        severityOverrides ["MissingTranslation": LintOptions.SEVERITY_WARNING]
        // 设置为 true 则显示一个问题所在的所有地方,而不会截短列表
        showAll true
        // 配置写入输出结果的位置,格式可以是文件或 stdout
        textOutput 'stdout'
        // 设置为 true,则生成纯文本报告(默认为 false)
        textReport false
        // 设置为 true,则会把所有警告视为错误处理
        warningsAsErrors true
        // 写入检查报告的文件(不指定默认为 lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 设置为 true 则会生成一个 XML 报告
        xmlReport false
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Fatal
        fatal 'NewApi', 'InlineApi'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Error
        error 'Wakelock', 'TextViewEdits'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Warning
        warning 'ResourceAsColor'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 ignore
        ignore 'TypographyQuotes'
    }
}

**
lint checks已经更新到v2版本了,所以配置项必须按照如下规则填写。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly 'com.android.tools.lint:lint-api:26.4.2'
    compileOnly 'com.android.tools.lint:lint-checks:26.4.2'
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.xxx.lint.router.=IssueRegistry")
    }
}

这里面有个很关键的点 attributes  "Lint-Registry-v2" 这个key必须要写死。value就是项目内的IssueRegistry。

如何实现自定义lint规则

我这边简单的介绍下Detector类。首先这种检查类需要明确的是第一次我们要检查些什么,这个很简单也很明确,我要检查的是哪些类,是代码还是资源文件或者xml,是构造函数还是方法调用。其次第二点才是哪些边界条件是我们认为有问题的地方,这个地方要抛出一个异常出来,给予开发人员提示。

public class RouteDetector extends Detector implements Detector.UastScanner {
    private final String WM_ROUTER_PACKAGE = "com.sankuai.waimai.router";
    private final String WM_ROUTER_ANNOTATION = WM_ROUTER_PACKAGE + ".annotation.RouterPage";
    private final String WM_ROUTER_CALL = WM_ROUTER_PACKAGE + ".Router";
    static final Issue ISSUE = Issue.create(
            "router_annotation_issue",    //唯一 ID
            "不允许使用该注解",    //简单描述
            "全局项目不允许使用该注解 请更换RouterUri",  //详细描述
            Category.CORRECTNESS,   //问题种类(正确性、安全性等)
            6,  //权重
            Severity.ERROR,   //问题严重程度(忽略、警告、错误)
            new Implementation(     //实现,包括处理实例和作用域
                    RouteDetector.class,
                    Scope.JAVA_FILE_SCOPE));
    static final Issue CALL_ISSUE = ISSUE.create("router_call_issue",    //唯一 ID
            "不要直接引用WM router",    //简单描述
            "使用项目封装的路由中间件完成跳转",  //详细描述
            Category.CORRECTNESS,   //问题种类(正确性、安全性等)
            6,  //权重
            Severity.ERROR,   //问题严重程度(忽略、警告、错误)
            new Implementation(     //实现,包括处理实例和作用域
                    RouteDetector.class,
                    Scope.JAVA_FILE_SCOPE));
	
    @Override
    public List<Class<? extends UElement>> getApplicableUastTypes() {
        List<Class<? extends UElement>> types = new ArrayList<>();
        types.add(UAnnotation.class);
        types.add(UCallExpression.class);
        return types;
    }

    @Override
    public UElementHandler createUastHandler(@NotNull JavaContext context) {
        return new UElementHandler() {

            @Override
            public void visitAnnotation(@NotNull UAnnotation node) {
                isAnnotation(node);
            }

            private void isAnnotation(UAnnotation node) {
                String type = node.getQualifiedName();
                if (WM_ROUTER_ANNOTATION.equals(type)) {
                    context.report(ISSUE, node, context.getLocation(node),
                            "该注解不允许使用");
                }
            }

            @Override
            public void visitCallExpression(@NotNull UCallExpression node) {
                checkIsMethod(node);
                checkIsConstructorCall(node);
            }

            private void checkIsConstructorCall(UCallExpression node) {
                if (!UastExpressionUtils.isConstructorCall(node)) {
                    return;
                }
                UReferenceExpression classRef = node.getClassReference();
                if (classRef != null) {
                    String className = UastUtils.getQualifiedName(classRef);
                    String value = "com.xxx.routerprotocol.request.xxxUriRequest";
                    String uriValue = WM_ROUTER_PACKAGE + ".common.DefaultUriRequest";
                    String pageValue = WM_ROUTER_PACKAGE + ".common.DefaultPageUriRequest";
                    if (className.equals(value)) {
                        context.report(CALL_ISSUE, node, context.getLocation(node),
                                "请使用项目提供的路由中间件 ");
                    }
                    if (className.equals(uriValue) || className.equals(pageValue)) {
                        context.report(CALL_ISSUE, node, context.getLocation(node),
                                "请使用项目提供的路由中间件 ");
                    }
                }
            }

            private void checkIsMethod(UCallExpression node) {
                if (UastExpressionUtils.isMethodCall(node)) {
                    if (node.getReceiver() != null && node.getMethodName() != null) {
                        PsiMethod method = node.resolve();
                        if (context.getEvaluator().isMemberInClass(method, WM_ROUTER_CALL)) {
                            context.report(CALL_ISSUE, node, context.getLocation(node),
                                    "请使用项目提供的路由中间件");
                        }
                    }
                }
            }

        };
    }

}

上面是项目lint检查的代码块,主要的核心点在getApplicableUastTypes,你允许type之后才能在下面的UElementHandler被传入截获,这样才能进行代码检查的开发。

UCallExpression 这个类型可以接受代码中的构造器以及方法调用,如果有一些特殊的类或者对象你不允许业务人员使用的情况下你对它进行一个报错处理。

UElementHandler 有个地方一定要注意,你想要做检查的地方一定不要写super,会导致无法继续完成代码。

上述代码完成的功能就是扫描代码是否实现了RouterPager这个注解。然后检测是否项目内引用到了Router这个类。项目内是不是用了过期的UriRequest或者美团的DefaultUriRequest,如果引用到抛出一个issue。

出了上述写的简单的代码扫描功能意外,lint 还能扫描 xml 文件 ,资源文件,gradle文件,通过静态扫描的方式可以更好的帮助我们构建我们的安卓项目。

下面分析下另外一个 关于资源文件检查的lint


import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.*;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.EnumSet;

public class PngResourceDetector extends Detector implements Detector.ResourceFolderScanner {

    public static final Issue ISSUE = Issue.create(
            "image too large",
            "Log Usage",
            "Please use the unified LogUtil class!",
            Category.CORRECTNESS,
            6,
            Severity.ERROR,
            new Implementation(PngResourceDetector.class, Scope.RESOURCE_FOLDER_SCOPE)
    );


    @Override
    public boolean appliesToResourceRefs() {
        return super.appliesToResourceRefs();
    }


    @Override
    public void checkFolder(@NotNull ResourceContext context, @NotNull String folderName) {
        super.checkFolder(context, folderName);
        File parent = context.file;
        for (File file : parent.listFiles()) {
            if (file.isFile()) {
                long length = file.length();
                System.out.print(file.toString());
                if (length > 100) {
                    context.report(ISSUE, Location.create(file),
                            "This code mentions `lint`: **Congratulations**");
                }
            }
        }

    }

    @Override
    public boolean appliesTo(@NotNull ResourceFolderType folderType) {
        System.out.print(folderType.name());
        // return true;
        return folderType.compareTo(ResourceFolderType.DRAWABLE) == 0 || folderType.compareTo(ResourceFolderType.MIPMAP) == 0;
    }
}

上面的代码做了什么
你要检查哪些资源你就要实现相关的接口,比如说我要检查java kotlin 代码,那么你要去实现UastScanner,如果你要扫描资源文件,那么你要去实现ResourceFolderScanner。
之后我们需要复写appliesTo 方法,我们需要扫描哪些资源文件,这个lint只扫描mipmap还有drawable,所以我们只要扫描这两个类型就可以了。
之后会进入checkFolder 方法内,我们当前做的很简单,检查下文件的length,当文件大小大于多少的时候会提示一个错误。