Android 自定义lint浅尝

什么是lint

就是我们在写toast时候忘记写show()编译器给我们那个提示。
在这里插入图片描述
专业的解释是静态代码分析,能够在代码运行前检测出可能出现的问题。lint的本质是定义了某些代码的使用规则。如Toast的使用规则就是在使用makeText后要调用show()方法。

为什么要自定义lint

希望我们自己写的代码在使用是遵守某一使用规则时,而在未遵守该规则调用的情况下能够给出提示。

实践

本文以我项目中EABuilder(Event Analyse Builder)为例来讲述如何自定义lint
我的EABuilder使用链式编程,使用方法如下

//addName 为事件起个名字
//add 添加事件的额外参数,可链式添加多个
//submit 将事件上报到服务器
 EABuilder(mContext).addName("select_day").add("position", position).add("id",id).submit()

如果没有调用submit()代不会报错,但是事件并没有上报到服务器。因此我希望在调用addName(事件都是有名字的)后必须调用submit。是不是和Toast很相似?

类名要扫描的方法监测是否有调用的方法
ToastmakeTextshow
BABuilderaddNamesubmit

如何写呢?

1 导入一个lint项目

google已经为我们提供的相关的示例android-custom-lint-rules。下载该仓库,仓库下有android-studio-2和android-studio-3两个项目,这是由于AndroidStudio2和AndroidStudio3中lint的写法有很大的不用,需要根据自己编译器的版本导入相应的示例代码,我导入的是android-studio-3

2 分析lint项目

项目中有两个module(checks和library),library中没有代码,只是在gradle中有一下代码

dependencies {
    lintChecks project(':checks')
}

因此只需要看checks中的代码即可

  1. checks是一个java-library
  2. checks中有如下依赖
 compileOnly "com.android.tools.lint:lint-api:$lintVersion"
 compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
  1. checks中有两个类,SampleCodeDetectorSampleIssueRegistry
  2. SampleIssueRegistry只是引用了SampleCodeDetector.ISSUE并没有实质性代码,但在gradle中需要注册SampleIssueRegistry
jar {
    manifest {
        attributes("Lint-Registry-v2": "com.example.lint.checks.SampleIssueRegistry")
    }
}
  1. SampleCodeDetector是lint的具体实现继承了Detector实现了UastScanner

分析至此,我并没有看SampleCodeDetector里的具代码,因为我要实现的效果和Toast相似,因此如果能找到Toast的Detector那么就可以在其基础上进行修改。
而在我们添加com.android.tools.lint:lint-checks依赖的时候就已经把编译器自带的lint引入进来了,可以在External Libraries->lint-checks中找到ToastDetector(或双击shift,搜索ToastDetector)。

3 撸码
  1. 按照上面分析的,在你的工程下面创建一个java-library,名叫lint_ea,添加相应依赖。
  2. 创建两个类EADetectorEAIssueRegistry,并在EADetector创建ISSUE的静态常量,具体写法可以把ToastDetector.ISSUE直接copy过来,把字符串中Toast字样改一改。EAIssueRegistry完全copy自EAIssueRegistry只是把SampleCodeDetector.ISSUE改成EADetector.ISSUE,代码如下:
public class EAIssueRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        return Collections.singletonList(EADetector.ISSUE);
    }

    @Override
    public int getApi() {
        return ApiKt.CURRENT_API;
    }
}

  1. 将ToastDetector中的代码copy到EADetector,并做以下修改

    将getApplicableMethodNames中的makeText改为addName
    visitMethod中的"android.widget.Toast"改为"xxx.xxx.EABuilder"
    将visitCallExpression中的"show"改成"submit"
    对删除Toast.LENGTH_SHORT or Toast.LENGTH_LONG的检测

最后代码如下:

public class EADetector extends Detector implements SourceCodeScanner {

    public static final Issue ISSUE;

    static {
        ISSUE = Issue.create("EABuilder", "EABuilder used but not submit", "You must call `submit()` on the resulting object to actually make the `EABuilder` submit.", Category.CORRECTNESS, 6, Severity.WARNING, new Implementation(TTADetector.class, Scope.JAVA_FILE_SCOPE));
    }

    public TTADetector() {
    }

    @Nullable
    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("addName");
    }

    public void visitMethod(JavaContext context, UCallExpression call, PsiMethod method) {
        if (!context.getEvaluator().isMemberInClass(method, "xxx.xxx.EABuilder")) {
            return;
        }

        @SuppressWarnings("unchecked")
        UElement surroundingDeclaration =
                UastUtils.getParentOfType(
                        call, true, UMethod.class, UBlockExpression.class, ULambdaExpression.class);

        if (surroundingDeclaration == null) {
            return;
        }

        UElement parent = call.getUastParent();
        if (parent instanceof UMethod
                || parent instanceof UReferenceExpression
                && parent.getUastParent() instanceof UMethod) {
            return;
        }

        SubmitFinder finder = new SubmitFinder(call);
        surroundingDeclaration.accept(finder);
        if (!finder.isShowCalled()) {
            context.report(
                    ISSUE,
                    call,
                    context.getCallLocation(call, true, false),
                    "EABuilder used but not submit: did you forget to call `submit()` ?");
        }
    }

    private static class SubmitFinder extends AbstractUastVisitor {
        private final UCallExpression target;
        private boolean found;
        private boolean seenTarget;

        private SubmitFinder(UCallExpression target) {
            this.target = target;
        }

        @Override
        public boolean visitCallExpression(UCallExpression node) {
            if (node == target || node.getPsi() != null && node.getPsi() == target.getPsi()) {
                seenTarget = true;
            } else {
                if ((seenTarget || target.equals(node.getReceiver()))
                        && "submit".equals(getMethodName(node))) {
                    found = true;
                }
            }
            return super.visitCallExpression(node);
        }

        @Override
        public boolean visitReturnExpression(UReturnExpression node) {
            if (UastUtils.isChildOf(target, node.getReturnExpression(), true)) {
                found = true;
            }
            return super.visitReturnExpression(node);
        }

        boolean isShowCalled() {
            return found;
        }
    }
}

代码编写完毕

使用

方法一: build该项目,将build文件夹下的lint_ea.jar复制到~/.android/lint/下,这样跟系统的lint一样会对每个项目都进行检测。
方法二:直接在要进行检测的项目中的gradle中添加如下代码:

dependencies {
    lintChecks project(':lint_ea')
}

重新rebuild项目(如果无效,可以试试重启AndroidStudio),再次编写时就会有提示了。效果如下:
在这里插入图片描述

这里似乎有个误区,谷歌提供的android-custom-lint-rules项目中是将lint项目放在了一个library里面。因此网上很多人都说要将自定义lint打入到一个aar包才能被引用,而我在要检测的项目中直接lintChecks project(’:lint_ea’)也是起作用的,没必要打入aar包中。

到此自定义lint完成。(似乎就说了copy代码,改一改)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

得食猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值