原生Lint沒法知足咱們團隊特有的需求,例如:編碼規範。html
原生Lint存在一些檢測缺陷或者缺乏一些咱們認爲有必要的檢測。node
LinkedIn提供了一種思路 : 將jar放到一個aar中。這樣咱們就能夠針對工程進行自定義Lint,lint.jar只對當前工程有效。android
Google指出,aar文件能夠包含一個自定義的lint.jar文件express
aar雖然方便,但依然有不少問題,緣由在於,要想統一開發者的lint檢查,每一個開發者都須要配置lint.xml、lintOptions。api
開發插件,統一管理lint.xml和lintOptions,自動添加aar。app
開發插件後,繼承了原生lint和自定義lint的全部檢查規則,內置lintOptions。maven
該項目主要用於測試規則是否正確ide
該項目主要用於將lint.jar轉換爲lint.aar文件,並提供maven庫發佈功能測試
該項目主要用於編寫lint的自定義規則類,以及打包成lint.jar文件gradle
該項目主要用於編寫lint的引用,lint的自定義規則,並提供maven庫發佈功能
引入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報告中對應的關係,以下:
總結下,每一個Lint檢查都須要四部分:
Issues 一個issue對應於Android項目中的一個可能的問題或bug。
Detectors 一個detector用於搜尋代碼潛在的Issues,一個單獨的detector能夠搜尋多個獨立但相關的Issues。
implementations 一個implementation將一個Issue鏈接到對應的Detector類,並指定在哪兒搜尋Issue。
Registries 一個註冊類包含一系列的Issues,默認的Registry類是BuiltinIssueRegistry類,由於咱們編寫了本身的自定義Issues,因此咱們須要提供自定義Registry類。
舉個例子:
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'
注意key不該該這樣定義
注意key不能直接硬編碼,且key定義的String引用必須爲EXTRA_開頭
注意由於本身編寫的BaseActivity,可使用@Suppressint("activityUse")去除錯誤
在equals(value)中,value不能爲硬編碼或定義在該類中的static final字符串,由於當爲指定字符串的時候,須要value.equals(),防止空指針
由於有MuLog,因此不該該再次使用Log.d,防止敏感信息泄露。