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很相似?

類名 要掃描的方法 監測是否有調用的方法
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中的代碼即可

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章