什麼是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代碼,改一改)