Android自定義Lint實踐(二)

爲何須要自定義

  • 原生Lint沒法知足咱們團隊特有的需求,例如:編碼規範。html

  • 原生Lint存在一些檢測缺陷或者缺乏一些咱們認爲有必要的檢測。node

自定義方案

LinkedIn提供了一種思路 : 將jar放到一個aar中。這樣咱們就能夠針對工程進行自定義Lint,lint.jar只對當前工程有效。android

clipboard.png

Google指出,aar文件能夠包含一個自定義的lint.jar文件express

aar雖然方便,但依然有不少問題,緣由在於,要想統一開發者的lint檢查,每一個開發者都須要配置lint.xml、lintOptions。api

開發插件,統一管理lint.xml和lintOptions,自動添加aar。app

開發插件後,繼承了原生lint和自定義lint的全部檢查規則,內置lintOptions。maven

方案實施

建立Android項目

該項目主要用於測試規則是否正確ide

建立lint依賴項目

該項目主要用於將lint.jar轉換爲lint.aar文件,並提供maven庫發佈功能測試

建立lint的Java項目

該項目主要用於編寫lint的自定義規則類,以及打包成lint.jar文件gradle

建立plugin的groovy項目

該項目主要用於編寫lint的引用,lint的自定義規則,並提供maven庫發佈功能

自定義lint類

  • 引入lint-api和lint-checks的依賴包

  • 建立IssueRegistry類,用於註冊全部的ISSUE

  • 建立相關的Detector類,繼承自Detector,實現相關的接口

以CustomEquaslDetector類爲例,其繼承Detector類,實現JavaScanner接口。注意Detector類是一個抽象類,其包含了不少內部接口類,並實現了它們的全部方法。接口類以下所示:

  • XmlScanner

  • ResourceFolderScanner

  • OtherFileScanner

  • JavaScanner

  • GradleScanner

  • ClassScanner

  • BinaryResoucrceScanner

看類的名稱就知道其相對應的做用,因此咱們實現JavaScanner類。

建立ISSUE對象,並註冊到IssueRegistry類中,建立對象的方式是靜態工程方法建立:

public static final Issue ISSUE = Issue.create(
        "LogUse",
        "避免使用Log/System.out.println",
        "使用Ln,防止在正式包打印log",
        Category.SECURITY, 5, Severity.ERROR,
        new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
  • id : 惟一值,應該能簡短描述當前問題。利用Java註解或者XML屬性進行屏蔽時,使用的就是這個id。

  • summary : 簡短的總結,一般5-6個字符,描述問題而不是修復措施。

  • explanation : 完整的問題解釋和修復建議。

  • category : 問題類別。詳見下文詳述部分。

  • priority : 優先級。1-10的數字,10爲最重要/最嚴重。

  • severity : 嚴重級別:Fatal, Error, Warning, Informational, Ignore。

  • Implementation : 爲Issue和Detector提供映射關係,Detector就是當前Detector。聲明掃描檢測的範圍Scope,Scope用來描述Detector須要分析時須要考慮的文件集,包括:Resource文件或目錄、Java文件、Class文件。

相對應的,其在lint的html報告中對應的關係,以下:

clipboard.png

clipboard.png

總結下,每一個Lint檢查都須要四部分:

  • Issues 一個issue對應於Android項目中的一個可能的問題或bug。

  • Detectors 一個detector用於搜尋代碼潛在的Issues,一個單獨的detector能夠搜尋多個獨立但相關的Issues。

  • implementations 一個implementation將一個Issue鏈接到對應的Detector類,並指定在哪兒搜尋Issue。

  • Registries 一個註冊類包含一系列的Issues,默認的Registry類是BuiltinIssueRegistry類,由於咱們編寫了本身的自定義Issues,因此咱們須要提供自定義Registry類。

clipboard.png

舉個例子:

public class EnumDetector extends Detector implements Detector.JavaScanner {

    ... // Implementation and Issue code from above

    /**
     * Constructs a new {@link EnumDetector} check
     */
    public EnumDetector() {
    }

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }

    @Override
    public EnumSet<Scope> getApplicableFiles() {
        return Scope.JAVA_FILE_SCOPE;
    }

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Arrays.<Class<? extends Node>>asList(
                EnumDeclaration.class
        );
    }

    @Override
    public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
        return new EnumChecker(context);
    }

    private static class EnumChecker extends ForwardingAstVisitor {

        private final JavaContext mContext;

        public EnumChecker(JavaContext context) {
            mContext = context;
        }

        @Override
        public boolean visitEnumDeclaration(EnumDeclaration node) {
            mContext.report(ISSUE, Location.create(mContext.file),                ISSUE.getBriefDescription(TextFormat.TEXT));
            return super.visitEnumDeclaration(node);
        }
    }
}
  • appliesTo方法 決定是否給定的文件可用並可被掃描,咱們return true來檢查給定的範圍

  • getApplicableFiles方法定義了Detector的範圍,該例是全部的Java文件。

  • getApplicableNodeTypes方法,注意其中的node,指的是一段代碼。一個node能夠是一個類的申明,一個方法的調用,或者一個註釋,由於咱們只關心Enum的申明,全部返回 EnumDeclaration.class。

  • createJavaVisitor方法是Lombok遍歷Java樹的方法。咱們建立一個EnumChecker內部類來表示檢查node樹的過程。

  • 由於只有一個node類型須要被檢查,全部覆寫visitEnumDeclaration方法。每當有一個Enum的申明,該方法就會被執行一次。

  • mContext.report方法用於問題的報告。ISSUE爲哪種Issue,location爲問題的發現地,以及Issue的簡要描述。

在看下IntentExtraKeyDetector類:

public class IntentExtraKeyDetector extends Detector implements JavaScanner {
    public static final Issue ISSUE = Issue.create(
            "extraKey",
            "please avoid use hardcode defined intent extra key",
            "defined in another activity",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(IntentExtraKeyDetector.class,             Scope.JAVA_FILE_SCOPE));

    public IntentExtraKeyDetector() {}

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.FAST;
    }

    // ---- Implements JavaScanner ----

    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("putExtra");
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
                            @NonNull MethodInvocation node) {
        ResolvedNode resolved = context.resolve(node);
        if (resolved instanceof ResolvedMethod) {
            ResolvedMethod method = (ResolvedMethod) resolved;

            if (method.getContainingClass().isSubclassOf("android.content.Intent", false)
                    && method.getArgumentCount() == 2) {
                ensureExtraKey(context, node);
            }
        }
    }

    private static void ensureExtraKey(JavaContext context, @NonNull MethodInvocation node) {
        //獲取method的參數值
        StrictListAccessor<Expression, MethodInvocation> accessor = node.astArguments();

        if (accessor.size() != 2) {
            return;
        }
        Expression expression = accessor.first();
        //當第一個參數值類型爲String,這樣是硬編碼
        if (expression instanceof StringLiteral){
            context.report(ISSUE, node, context.getLocation(node), "please avoid use hardcode defined Intent.putExtra key");
            return;
        }
        //當第一個參數值類型爲變量
        //ConstantEvaluator.evaluate(context, expression);
        if (expression instanceof VariableReference){
            //獲取該變量的定義name
            String targetName = ((VariableReference)expression).astIdentifier().astValue();
            if (!targetName.startsWith("EXTRA_")){
                context.report(ISSUE, node, context.getLocation(node), "please defined intent extra key start with EXTRA_");
            }
        }
        //當第一個參數值是其餘類的變量時
        if (expression instanceof Select){
            String targetName = ((Select)expression).astIdentifier().astValue();
            if (!targetName.startsWith("EXTRA_")){
                context.report(ISSUE, node, context.getLocation(node), "please defined intent extra key start with EXTRA_");
            }
        }
    }

使用指南

  • 在project中的build.gradle文件中的dependencies中添加

classpath 'com.mucfc.muna.lint:plugin:latest.integration'
  • 在module app中的build.gradle文件中,添加:

apply plugin: 'MuLintPlugin'

融合項目後

使用系統Toast,而沒有使用muna中的自定義toast

clipboard.png

使用Bundle.putXXX("key","value")

注意key不該該這樣定義

clipboard.png

使用Intent.putExtra(key,value);

注意key不能直接硬編碼,且key定義的String引用必須爲EXTRA_開頭

clipboard.png

定義Activity類必須繼承BaseActivity(Fragment類同)

clipboard.png

注意由於本身編寫的BaseActivity,可使用@Suppressint("activityUse")去除錯誤

clipboard.png

使用equals方法

在equals(value)中,value不能爲硬編碼或定義在該類中的static final字符串,由於當爲指定字符串的時候,須要value.equals(),防止空指針

clipboard.png

使用原始的Log.d()方法

由於有MuLog,因此不該該再次使用Log.d,防止敏感信息泄露。

clipboard.png

相關文章
相關標籤/搜索