赞
踩
lint是Android自带的静态代码检测框架,用来在不运行代码的情况下,对项目代码进行检测,检测范围包括方法调用、硬编码等。本文从项目结构、依赖、检查器编写、打包、运行测试的环节,描述一个lint检查器的编写和使用全过程。
lint检查器首先是一个插件,需要打成aar或jar来被待检测项目使用。其次,lint检查器本身,需要包含两层:外层是壳工程,生成检查器aar供待检测项目使用;aar内容来自内层实现层;内层是实现层,负责检查器逻辑实现、注册等。
为了方便,本文将待检测项目、壳工程和实现层都放在一个项目中,作为不同的模块存在。不过壳工程还是会生成独立的aar,来供待检测模块使用。
libs
中的lintlibout-debug.aar
就是壳工程生成的aar,src
中有业务代码。
lintlibout
是生成aar的壳工程,里面除了在build.gradle
中指定aar的生成方式,没有其他代码;lintlib
是实现层,里面存在着检查器相关代码
只需要在build.gradle
中依赖到lint检查器aar即可:
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.testlint' compileSdk 32 defaultConfig { applicationId "com.example.testlint" minSdk 26 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation files("libs/lintlibout-debug.aar") }
添加的唯一一行,是dependencies
块中最后的implementation files("libs/lintlibout-debug.aar")
壳工程的依赖方式如下,在dependencies
模块中指定lint aar
的源工程:
plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' } android { namespace 'comszc.lintlibout' compileSdk 32 defaultConfig { minSdk 26 targetSdk 32 consumerProguardFiles "consumer-rules.pro" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } } dependencies { lintPublish project(':lintlib') }
实现层需要:
issue
plugins { id 'org.jetbrains.kotlin.jvm' id 'java-library' } dependencies { // 1. 依赖Android的lint组件 compileOnly "com.android.tools.lint:lint-api:30.2.1" compileOnly "com.android.tools.lint:lint-checks:30.2.1" compileOnly "com.android.tools.lint:lint:30.2.1" compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20" } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } jar { manifest { // 2. 指定lint注册器 attributes('Lint-Registry-V2': 'com.szc.lintlib.MyIssueRegistry') } } // 3. 将源文件生成jar,同时调用第2步注册我们的注册器 configurations { lintJarOutput } dependencies { lintJarOutput files(jar) }
检测器代码编写可分为两大步:Detector
注册和Detector
编写,均在liblint
实现层中。本文通过方法调用检测和Toast
硬编码检测两个案例,来进行说明。
编写IssueRegistry
的子类,覆写getIssues()
方法,返回要检测的检测issue
:
public class MyIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(
MyDetector.ISSUE
);
}
}
MyDetector
是检测器,ISSUE
在其中定义,所以直接看下一小节的Detector
编写即可。
public class MyDetector extends Detector implements Detector.UastScanner { private static final String ISSUE_ID = "LogUseError"; private static final String ISSUE_DESCRIPTION = "Please use LogUtil instead of Log."; private static final String ISSUE_EXPLANATION = "Please use LogUtil instead of Log, because I don`t like it!"; private static final Category ISSUE_CATEGORY = Category.CORRECTNESS; private static final int ISSUE_PRIORITY = 5; private static final Severity ISSUE_SEVERITY = Severity.WARNING; // 1、定义 ISSUE public static final Issue ISSUE = Issue.create( ISSUE_ID, // 唯一ID 这个id必须是独一无二的 ISSUE_DESCRIPTION, // 简单描述 ISSUE_EXPLANATION, // 详细描述 ISSUE_CATEGORY, // 验证正确性 ISSUE_PRIORITY, // 权重,优先级,必须在1到10之间。 ISSUE_SEVERITY, // 这是一个警告 new Implementation( // 这是连接Detector与Scope的桥梁,其中Detector的功能是寻找issue,而scope定义了在什么范围内查找issue MyDetector.class, Scope.JAVA_FILE_SCOPE)); private static final List<String> CANDIDATE_METHOD_NAMES = Arrays.asList( "i", "d", "e", "v", "w", "wtf" ); private static final String LOG_CLASS = "android.util.Log"; // 2. 定义要检查的方法 @Override public List<String> getApplicableMethodNames() { return CANDIDATE_METHOD_NAMES; } @Override public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) { if (!context.getEvaluator().isMemberInClass(method, LOG_CLASS)) { // 3. 判断是不是指定的目标类 return; } // 4. 上报issue信息 context.report(ISSUE, node, context.getLocation(node), ISSUE_DESCRIPTION); } }
在IssueRegistry
的子类的getIssues()
方法添加新的检测issue
:
public class MyIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<Issue> getIssues() {
return Arrays.asList(
MyDetector.ISSUE,
ToastHardCodeDetector.ISSUE_HARD_CODE
);
}
}
public class ToastHardCodeDetector extends Detector implements Detector.UastScanner { private static final String ISSUE_ID_HARD_CODE = "HardCodeToast"; private static final String ISSUE_DESCRIPTION_HARD_CODE = "Toast should not be hard-coded"; private static final String ISSUE_EXPLANATION_HARD_CODE = "Using hard-coded Toast message is not recommended, " + "as it makes it harder to maintain, translate and update the app."; private static final Category ISSUE_CATEGORY_HARD_CODE = Category.CORRECTNESS; private static final int ISSUE_PRIORITY_HARD_CODE = 5; private static final Severity ISSUE_SEVERITY_HARD_CODE = Severity.WARNING; private static final String TOAST_CLASS = "android.widget.Toast"; private static final String MAKE_TEXT_METHOD = "makeText"; // 1. 定义Issue public static final Issue ISSUE_HARD_CODE = Issue.create( ISSUE_ID_HARD_CODE, // Issue的Id ISSUE_DESCRIPTION_HARD_CODE, // 简单描述 ISSUE_EXPLANATION_HARD_CODE, // 详细描述 ISSUE_CATEGORY_HARD_CODE, // 检查类型:检查正确性 ISSUE_PRIORITY_HARD_CODE, // 检查优先级 ISSUE_SEVERITY_HARD_CODE, // 检查不通过时的严重性 new Implementation(ToastHardCodeDetector.class, Scope.JAVA_FILE_SCOPE)); // 连接Detector与Scope的桥梁,其中Detector的功能是寻找issue,而scope定义了在什么范围内查找issue,这里是在java或kotlin源文件中寻找并检测issue。 // 2. 定义要检测的方法 @Override public List<String> getApplicableMethodNames() { return Arrays.asList(MAKE_TEXT_METHOD); } // 3. 方法检测的具体逻辑 @Override public void visitMethodCall(@NotNull JavaContext context, @Nullable UCallExpression call, @NotNull PsiMethod method) { if (!context.getEvaluator().isMemberInClass(method, TOAST_CLASS)) { // 该方法是否在目标类中实现 return; } if (call == null) { return; } // 获取目标方法的入参列表 List<UExpression> arguments = call.getValueArguments(); if (arguments == null || arguments.size() < 3) { return; } // 获取某个入参,Toast.makeText()方法中第2个参数容易被硬编码,是检测目标 UExpression messageArgument = arguments.get(1); if (messageArgument == null) { return; } // 获取该入参的原始字符串,是字符串的话会加上"" String sourceMessage = messageArgument.asSourceString(); if (sourceMessage == null) { return; } if (sourceMessage.matches("\".*\"")) { // 检测到message参数硬编码,则上报issue context.report(ISSUE_HARD_CODE, messageArgument, context.getLocation(messageArgument), ISSUE_DESCRIPTION_HARD_CODE); } } }
点击后,会在壳工程lintlibout/build/outputs/aar
目录下生成检测的aar包:
根据2.1节指定的依赖方式,将生成的aar复制到libs目录下:
编写待检测的代码:
class MainActivity: Activity() {
fun test() {
// 检测Log.*方法
Log.i("", "")
Log.d("", "")
// 检测Toast硬编码
Toast.makeText(this, "", Toast.LENGTH_LONG).show()
val text = "text"
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
// 检测套娃是否能逃过Toast硬编码检测
ToastUtils.show(this, "")
}
}
public class TestLint {
void func() {
// 检测Log.*方法
Log.i("", "");
}
}
//
public class ToastUtils {
public static void show(Context context, String text) {
// 检测Toast硬编码
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
}
}
可能需要重启下AS才能生效:
Log.*
方法检测:
Toast硬编码:
Log.*
方法检测:
Toast硬编码检测(测试是否会误报):
在项目根目录下,运行gradlew :app:lint
,其中app
是待检测的模块:
D:\develop\Android\AndroidStudioProjects\TestLint> gradlew lint
> Task :app:compileDebugKotlin
'compileDebugJavaWithJavac' task (current target is 11) and 'compileDebugKotlin' task (current target is 1.8) jvm target compatibility should be set to the same Java version.
> Task :app:lintAnalyzeDebug
com.szc.lintlib.MyIssueRegistry in D:\develop\Android\gradle\caches\transforms-3\8140217cb6f1adb1d3c77da9def64777\transformed\lintlibout-debug\jars\lint.jar does not specify a vendor; see IssueRegistry#vendor
> Task :app:lintReportDebug
Wrote HTML report to file:///D:/develop/Android/AndroidStudioProjects/TestLint/app/build/reports/lint-results-debug.htm
BUILD SUCCESSFUL in 31s
58 actionable tasks: 34 executed, 24 up-to-date
D:\develop\Android\AndroidStudioProjects\TestLint>
检测输出在待检测模块的build/reports
目录下:
最清楚的应该是html格式的报告了,我们可以用浏览器打开查看:
官网介绍:https://developer.android.google.cn/studio/write/lint?hl=zh-cn#commandline
其他论坛:https://zhuanlan.zhihu.com/p/307382854
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。