本文地址:http://www.reibang.com/p/5991bca931e2
要做自定義的lint檢查抢呆,首先要理解一個Abstract Syntax Tree(簡稱AST)抽象語法樹的概念梯找。它是一個java程序完成編譯所必要的前期工作噩凹,將java代碼的語法按照樹結構解析存儲起來广辰。相關講解可見(https://juejin.im/entry/576b61162e958a0078c637fb)炭懊。
而我們要做的lint檢查就是基于這個抽象語法樹的檢查票渠。
下面翩活,我從實現(xiàn)一個自定義的lint(檢查變量名是否按要求命名)來講解一下怎么實現(xiàn)阱洪。
首先,我們需要做的是一個準備的工作菠镇,就是引入lint包:(總不能安裝studio這樣的工作也要我放在一個步驟里吧冗荸,所以這些感覺不那么核心的步驟我們就不當做主要步驟來歸檔了,包括后面的使jar生效等步驟利耍。但這些都是必須的蚌本,只不過于其實現(xiàn)原理沒那么核心)
我們創(chuàng)建一個java library的module,在其build.gradle文件中添加:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.tools.lint:lint-api:25.3.3'
}
我用的是當前最新的版本25.3.3隘梨,我注意到以前一些版本跟這個版本的實現(xiàn)方法和小要求不大一樣程癌,如果不是這個版本的,我建議換成最新的這個來做吧轴猎。而且我感覺這個版本的api比以前那套確實好用一點點嵌莉,雖然還不是很清晰。捻脖。锐峭。
接著,我們就可以開始主要的步驟了可婶。
總的來說沿癞,實現(xiàn)過程主要有兩步:
一. 使用gradle注冊一個lint檢查器
二. 實現(xiàn)這個lint檢查器
注冊檢查器
我們可能以為直接就可以把檢查器注冊進構建文檔里,其實不是扰肌,我們需要先添加一個“注冊器”抛寝,然后才在注冊器里面申明要執(zhí)行的檢查器。這可能是為了解耦吧曙旭,然而我覺得這個過程有點多余盗舰,完全可以直接設計成注冊檢查器的方式,因為build文件里面本來就是一個注冊的角色了桂躏,而且最終也只是為了注冊檢查器钻趋,又何必再加多一個注冊器呢。至少從目前的功能要求上講剂习,這個注冊器有點多余蛮位。我們接著按照要求來做:
首先,在gradle文件中加入這樣一個配置:
jar {
manifest {
attributes("Lint-Registry": "com.ives.lint.WordSpellRegistry")
}
}
顯然鳞绕,這似乎是把一個注冊器注冊給了gradle失仁,但這個注冊器我們似乎還沒有,那么下一步我們就是要添加一個這樣的注冊器们何。
這一步只需要在上面所寫的包下添加一個相應類名的java類WordSpellRegistry萄焦,這個類繼承于IssueRegistry,并且實現(xiàn)里面的getIssues方法:
public class WordSpellRegistryextends IssueRegistry {
public WordSpellRegistry() {//!!必須要有無參構造器
}
@Override
public List<Issue> getIssues() {
return Arrays.asList(//這幾個元素后面接著講
WordSpellDetector.ISSUE,
MessageObtainDetector.ISSUE,
ParameterLengthConstraintDetector.ISSUE
);
}
}
getIssues方法里面返回的就是好幾個檢查器冤竹,下面拂封,我們就是進入第二步了,就是實現(xiàn)檢查器鹦蠕。
實現(xiàn)檢查器
要實現(xiàn)的檢查器就是我們在getIssues方法里返回的這幾個東西.其實只需要一個就行了冒签,我上面添加了三個是為了表明這可以有多個檢查器而已。
我們創(chuàng)建一個WordSpellDetector類钟病。這個類的繼承關系及基本實現(xiàn)如下:
public class WordSpellDetector extends Detector implements Detector.JavaPsiScanner {
public static final Issue ISSUE = Issue.create("WordSpellStaticFinalUpper",
"Static final field should be all uppercase",
"Static final field should be all uppercase as our specification",
Category.CORRECTNESS,
9,
Severity.WARNING,
new Implementation(WordSpellDetector.class,
Scope.JAVA_FILE_SCOPE));
@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return super.getApplicablePsiTypes();
}
@Override
public JavaElementVisitor createPsiVisitor(JavaContext context) {
return super.createPsiVisitor(context);
}
}
ISSUE顯然是創(chuàng)建了一個提示相關的信息的封裝對象萧恕。其中里面的new Implementation()返回了所要映射的檢查器。
檢查器里面最重要的兩個方法就是getApplicablePsiTypes和createPsiVisitor肠阱。前者表示所要檢查的節(jié)點類型票唆,以前的版本里這個方法叫做getApplicableNodeTypes,現(xiàn)已廢棄(可能是舊的那個Note接口信息太少了辖所,現(xiàn)已替換成了PsiElement接口惰说,但以前的好名字被站了咯,但起的這個新名字有點讓人傻眼)缘回。
一個完整的檢查static final型的名字必須大寫的檢查器如下:
public class WordSpellDetector extends Detector implements Detector.JavaPsiScanner {
/**
* 這是一個Issue聲明
*/
public final static Issue ISSUE = Issue.create("WordSpellStaticFinalUpper",
"Static final field should be all uppercase",
"Static final field should be all uppercase as our specification",
Category.CORRECTNESS,
9,
Severity.WARNING,
new Implementation(WordSpellDetector.class,
Scope.JAVA_FILE_SCOPE));
@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Arrays.asList(PsiField.class);//聲明為檢查成員變量類型節(jié)點
}
@Override
public JavaElementVisitor createPsiVisitor(JavaContext context) {
return new WordSpellChecker(context);//返回本檢查器真正的檢查執(zhí)行者
}
private class WordSpellChecker extends JavaElementVisitor {
private JavaContext mContext;
WordSpellChecker(JavaContext context){
this.mContext = context;
}
@Override
public void visitField(PsiField field) {//實現(xiàn)哪個方法吆视,需要跟getApplicablePsiTypes()返回的類型對應
if(field==null)return;
PsiElement[] children = field.getChildren();
if(children!=null){
for (int i = 0; i < children.length; i++) {
if(!isAllUpper(field.getName())
&& children[i].getText().contains("final") &&children[i].getText().contains("static")){
mContext.report(ISSUE, mContext.getLocation(field), "field name with static final should be all uppercase");
}
}
}
super.visitField(field);
}
}
private boolean isAllUpper(String str){
if(str==null)return true;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
if(chars[i]>='a'&& chars[i]<='z')return false;//只判斷有沒有小寫字母,不限定其它特殊字符
}
return true;
}
}
至此酥宴,所有的編碼已經(jīng)完成啦吧。相關的api和要注意的地方我在代碼注釋里都提示了,具體還需要大家去探索它的api了拙寡。
最后要做的就是把java library構建出jar文件授滓,放在系統(tǒng)用戶路徑下的.android/lint/子目錄中,名稱任意般堆,只要是jar文件ok的。但這樣會對整個studio的project都生效淮摔。如果想更好地管理這個lint文件的話,可以另外創(chuàng)建一個android library和橙,把jar文件一起編譯進去打出aar包或者直接依賴library到主module里即可仔燕,這些可以百度一下挺多講到這里。
最后的效果是這樣:
溫馨提醒:
不管lint文件是直接放在.android目錄下魔招,還是用aar等形式晰搀,最后都需要重啟android
studio才能生效办斑。我現(xiàn)在用的studio 是2.3.1版本外恕。
相關代碼俄周,可訪問我的github吁讨,歡迎加星:
https://github.com/IvesWang/CustomLint
https://github.com/IvesWang/lint-rules
相關參考文章:
http://www.tuicool.com/articles/qaEZfiI
https://engineering.linkedin.com/android/writing-custom-lint-checks-gradle
http://blog.csdn.net/u010360371/article/details/50189171
http://tools.android.com/tips/lint-custom-rules
https://github.com/yangcheng/CustomLint
https://github.com/yangcheng/LintDemoApp
http://www.tuicool.com/articles/nY3Ary
https://juejin.im/entry/576b61162e958a0078c637fb