自定義lint與抽象語法樹(AST)檢查器

本文地址: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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市峦朗,隨后出現(xiàn)的幾起案子建丧,更是在濱河造成了極大的恐慌波势,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尺铣,死亡現(xiàn)場離奇詭異凛忿,居然都是意外死亡,警方通過查閱死者的電腦和手機店溢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來床牧,“玉大人,你說我怎么就攤上這事戈咳『敬担” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵耳贬,是天一觀的道長泳姐。 經(jīng)常有香客問我暂吉,道長胖秒,這世上最難降的妖魔是什么慕的? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮风题,結果婚禮上,老公的妹妹穿的比我還像新娘嫉父。我一直安慰自己,他們只是感情好绕辖,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仪际,像睡著了一般。 火紅的嫁衣襯著肌膚如雪树碱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天成榜,我揣著相機與錄音,去河邊找鬼赎婚。 笑死刘绣,一個胖子當著我的面吹牛惑淳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播歧焦,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼肚医,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肠套?” 一聲冷哼從身側響起猖任,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朱躺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體长搀,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年枪芒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舅踪。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡良蛮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出背镇,到底是詐尸還是另有隱情,我是刑警寧澤瞒斩,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布破婆,位于F島的核電站胸囱,受9級特大地震影響祷舀,放射性物質發(fā)生泄漏烹笔。R本人自食惡果不足惜裳扯,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一饰豺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冤吨,春花似錦蒿柳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽圾叼。三九已至捺癞,卻和暖如春夷蚊,著一層夾襖步出監(jiān)牢的瞬間翘簇,已是汗流浹背撬码。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工版保, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留夫否,地道東北人彻犁。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓凰慈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親微谓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容