什么是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很相似?
类名 | 要扫描的方法 | 监测是否有调用的方法 |
---|---|---|
Toast | makeText | show |
BABuilder | addName | submit |
如何写呢?
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中的代码即可
- checks是一个java-library
- checks中有如下依赖
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
- checks中有两个类,SampleCodeDetector和SampleIssueRegistry
- SampleIssueRegistry只是引用了SampleCodeDetector.ISSUE并没有实质性代码,但在gradle中需要注册SampleIssueRegistry
jar {
manifest {
attributes("Lint-Registry-v2": "com.example.lint.checks.SampleIssueRegistry")
}
}
- SampleCodeDetector是lint的具体实现继承了Detector实现了UastScanner
分析至此,我并没有看SampleCodeDetector里的具代码,因为我要实现的效果和Toast相似,因此如果能找到Toast的Detector那么就可以在其基础上进行修改。
而在我们添加com.android.tools.lint:lint-checks依赖的时候就已经把编译器自带的lint引入进来了,可以在External Libraries->lint-checks中找到ToastDetector(或双击shift,搜索ToastDetector)。
3 撸码
- 按照上面分析的,在你的工程下面创建一个java-library,名叫lint_ea,添加相应依赖。
- 创建两个类EADetector和EAIssueRegistry,并在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;
}
}
-
将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代码,改一改)