Android 自定义Lint实践总结

自定义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
    );
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容