×

Android 自定义Lint实践总结

96
Zurich37度
2018.02.26 13:38* 字数 1055

自定义Lint

Android Lint 是由 Android SDK 提供的一种静态代码检测工具,用于检测 Android 项目的代码质量,帮你查出可能发生的bug以及可以优化的代码
此文档只针对
AS 3.0+
Gradle 3.3+
Android Plugin Version 3.0.1+

本文参考以下项目
googlesamples的android-custom-lint-rules库
GavinCT(美团)的MeituanLintDemo

LintOptions配置

如果将 abortOnError 设置为true,自定义Lint很容易出现编译失败,此时检查message窗口将不必要的检查进行忽略禁用十分必要,例如 disable 'MissingTranslation' 此配置将忽略没有对strings.xml进行翻译报的错误。其他配置项查看下面各注释👇

各个配置含义

android {
lintOptions {
    // 设置为 true时lint将不报告分析的进度
    quiet true
    // 如果为 true,则当lint发现错误时停止 gradle构建
    abortOnError false
    // 如果为 true,则只报告错误
    ignoreWarnings true
    // 如果为 true,则当有错误时会显示文件的全路径或绝对路径 (默认情况下为true)
    //absolutePaths true
    // 如果为 true,则检查所有的问题,包括默认不检查问题
    checkAllWarnings true
    // 如果为 true,则将所有警告视为错误
    warningsAsErrors true
    // 不检查给定的问题id
    disable 'TypographyFractions','TypographyQuotes'
    // 检查给定的问题 id
    enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
    // * 仅 * 检查给定的问题 id
    check 'NewApi', 'InlinedApi'
    // 如果为true,则在错误报告的输出中不包括源代码行
    noLines true
    // 如果为 true,则对一个错误的问题显示它所在的所有地方,而不会截短列表,等等。
    showAll true
    // 重置 lint 配置(使用默认的严重性等设置)。
    lintConfig file("default-lint.xml")
    // 如果为 true,生成一个问题的纯文本报告(默认为false)
    textReport true
    // 配置写入输出结果的位置;它可以是一个文件或 “stdout”(标准输出)
    textOutput 'stdout'
    // 如果为真,会生成一个XML报告,以给Jenkins之类的使用
    xmlReport false
    // 用于写入报告的文件(如果不指定,默认为lint-results.xml)
    xmlOutput file("lint-report.xml")
    // 如果为真,会生成一个HTML报告(包括问题的解释,存在此问题的源码,等等)
    htmlReport true
    // 写入报告的路径,它是可选的(默认为构建目录下的 lint-results.html )
    htmlOutput file("lint-report.html")
    // 设置为 true, 将使所有release 构建都以issus的严重性级别为fatal(severity=false)的设置来运行lint
    // 并且,如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制)
    checkReleaseBuilds true
    // 设置给定问题的严重级别(severity)为fatal (这意味着他们将会
    // 在release构建的期间检查 (即使 lint 要检查的问题没有包含在代码中)
    fatal 'NewApi', 'InlineApi'
    // 设置给定问题的严重级别为error
    error 'Wakelock', 'TextViewEdits'
    // 设置给定问题的严重级别为warning
    warning 'ResourceAsColor'
    // 设置给定问题的严重级别(severity)为ignore (和不检查这个问题一样)
    ignore 'TypographyQuotes'
    }
}

此部分内容粘贴自简书LintOptions
此外,还可以通过 lint.xml 文件进行lint规则的配置,如果项目工程中没有此文件自行创建在项目module根目录下即可,具体配置参考:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Ignore the ObsoleteLayoutParam issue in the given files -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- Ignore the UselessLeaf issue in the given file -->
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>

    <!-- Change the severity of hardcoded strings to "error" -->
    <issue id="HardcodedText" severity="error" />
</lint>  

自定义Lint方案

针对不同的自定义lint方案,其中LinkedIn方案可行性最高,将lint.jar放入AAR包中,项目依赖AAR从而进行自定义lint开发,其中lint.jar只对当前项目工程有效。
LinkedIn提供的解决方案原文

项目详细配置

创建java工程,配置Gradle

提供lint.jar,自定义lint规则在此工程中进行

apply plugin: 'java-library'

def lintVersion = "26.0.0-beta5"
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
    testCompile "junit:junit:4.12"
    testCompile "com.android.tools.lint:lint:$lintVersion"
}

jar {
   manifest {
        attributes("Lint-Registry-v2":"com.appchina.android.lint.core.AppChinaIssueRegistry")
    }
 }  

创建AAR,配置Gradle

apply plugin: 'com.android.library'
...
dependencies {
    lintChecks project('lintCoreLibrary') //lintCoreLibrary为上面的java工程名
}
...  

此处 lint 还不会生效,需要将 aar 包引用到主项目,两种方式:
1.手动编译出 .aar 集成至主项目,即放至 libs 目录并进行 aar 依赖
2.还上传至 JCenter,进行远程依赖,更新更为方便

创建Detector

下面是系统ToastUtilsDetector的源码

public class ToastUtilsDetector extends Detector implements Detector.UastScanner {

private static final Class<? extends Detector> DETECTOR_CLASS = ToastUtilsDetector.class;
private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;
private static final Implementation IMPLEMENTATION = new Implementation(
        DETECTOR_CLASS,
        DETECTOR_SCOPE
);

private static final String ISSUE_ID = "ToastUseError";
private static final String ISSUE_DESCRIPTION = "You should use our{ToastUtils}";
private static final String ISSUE_EXPLANATION = "You should NOT use android.widget.Toast directly. Instead you should use ToastUtils we offered.";
private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;
private static final int ISSUE_PRIORITY = 9;
private static final Severity ISSUE_SEVERITY = Severity.ERROR;
private static final String CHECK_PACKAGE = "android.widget.Toast";

public static final Issue ISSUE = Issue.create(
        ISSUE_ID,
        ISSUE_DESCRIPTION,
        ISSUE_EXPLANATION,
        ISSUE_CATEGORY,
        ISSUE_PRIORITY,
        ISSUE_SEVERITY,
        IMPLEMENTATION
);

@Override
public List<String> getApplicableMethodNames() {
    return Arrays.asList("makeText", "show");
}

@Override
public void visitMethod(@NonNull JavaContext context, @NonNull UCallExpression node, @NonNull PsiMethod method) {
    if (!context.getEvaluator().isMemberInClass(method, CHECK_PACKAGE)) {
        return;
    }

    List<UExpression> args = node.getValueArguments();
    UExpression duration = null;
    if (args.size() == 3) {
        duration = args.get(2);
    }
    LintFix fix = null;
    if (duration != null) {
        String replace;
        if ("Toast.LENGTH_LONG".equals(duration.toString())) {
            replace = "ToastUtils.showLong(" + args.get(0).toString() + ", " + args.get(1).toString() + ");";
        } else {
            replace = "ToastUtils.showShort(" + args.get(0).toString() + ", " + args.get(1).toString() + ");";
        }
        fix = fix().name("Replace with ToastUtils")
                .replace()
                .with(replace)
                .build();
    }
    if (fix != null) {
        context.report(ISSUE, node, context.getLocation(node), ISSUE_DESCRIPTION, fix);
    }
}   
}  

LintFix 提供快捷修复错误,如上面代码中快捷替换错误代码片段;支持使用正则表达式,具体api可查看LintFix源码,使用比较简单;
如果不需要快捷修复,可以使用JavaContext.report()的其他方法,此时只会进行代码标注(红(Error) / 黄(Waring))提示,Alt+Enter并不会出现快捷修复的提示

自定义Detector需要继承自Detector并实现 Detector.UastScanner 接口,25.2.0及之前版本的Detector.JavaPsiScanner已被弃用,UastScanner相比于JavaPsiScanner以及更老的JavaScanner,主要提供了对Kotlin支持,API更加简单,特点是成对存在(满足条件 -> visitor)此外可以lint-checks-version.jar中的各类型Detector源码可以学习其用法。

UastScanner包含13个回调方法,下面介绍常用的几个:
1.getApplicableUastTypes

此方法返回需要检查的AST节点的类型,类型匹配的UElement将会被createUastHandler(createJavaVisitor)创建的UElementHandler(Visitor)检查。

2.createUastHandler

创建一个UastHandler来检查需要检查的UElement,对应于getApplicableUastTypes

3.getApplicableMethodNames

返回你所需要检查的方法名称列表,或者返回null,相匹配的方法将通过visitMethod方法被检查

4.visitMethod

检查与getApplicableMethodNames相匹配的方法

5.getApplicableConstructorTypes

返回需要检查的构造函数类型列表,类型匹配的方法将通过visitConstructor被检查

6.visitConstructor

检查与getApplicableConstructorTypes相匹配的构造方法

7.getApplicableReferenceNames

返回需要检查的引用路径名,匹配的引用将通过visitReference被检查

8.visitReference

检查与getApplicableReferenceNames匹配的引用

9.appliesToResourceRefs

返回需要检查的资源引用,匹配的引用将通过visitResourceReference被检查

10.visitResourceReference

检查与appliesToResourceRefs匹配的资源引用

11.applicableSuperClasses

返回需要检查的父类名列表,此处需要类的全路径名

11.visitClass

检查applicableSuperClasses返回的类

ISSUE

ISSUE在每个Detector中定义,lint检查到相关项将ISSUE报告出来,示例:

public static final Issue ISSUE = Issue.create(
        "ListView",
        "AppChinaLint:Replace 'ListView' with 'RecyclerView'",
        "RecyclerView is better than ListView",
        Category.CORRECTNESS, 6, Severity.WARNING,
        new Implementation(ListViewDetector.class, Scope.JAVA_FILE_SCOPE));

在此处可自定义错误描述、安全级别(WARNING、ERROR等)

Category

Category表示lint结果在IDE中的分类,系统已有类别:
Lint
Correctness (incl. Messages)
Security
Performance
Usability (incl. Icons, Typography)
Accessibility
Internationalization
Bi-directional text
此外还可以自定义Category,示例:

public class MTCategory {
    public static final Category NAMING_CONVENTION = Category.create("命名规范", 101);
}
IssueRegistry

提供需要被检测的Issue列表,示例:

@Override
public synchronized List<Issue> getIssues() {
    return Arrays.asList(
            LogDetector.ISSUE,
            ListViewDetector.ISSUE,
            HashMapForJDK7Detector.ISSUE,
            ToastUtilsDetector.ISSUE,
            SampleCodeDetector.ISSUE,
            FindViewDetector.ISSUE,
            BaseActivityDetector.ISSUE,
            BaseFragmentDetector.ISSUE
    );
}
日记本
Web note ad 1