自定义Lint规则

Google对这块儿还是比较重视的,从频繁更新可见一斑。

  • AndroidStudio2.0+需要手动把lint.jar拷贝到.android/lint/下;
  • AndroidStudio3.0+支持 lintChecks project(’:checks’)的形式添加自定义规则。
    新建一个Java module(这里名字记做checks),在build.gradle中配置依赖:
dependencies {
    // 注意这里必须是compileOnly
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
}


jar {
    manifest {
        // Only use the "-v2" key here if your checks have been updated to the
        // new 3.0 APIs (including UAST)
        attributes("Lint-Registry-v2": "com.example.lint.checks.SampleIssueRegistry")
    }
}

在需要被检测的工程中添加依赖

dependencies {
    lintChecks project(':checks')
}

目前大多设置Javaversion为1.8

sourceCompatibility = "1.8"
targetCompatibility = "1.8"
  • AndroidStudio4.0+同3.0+的差不多,不过指定自定义Lint的方式改为通过services的形式,配置如下:
checks
    src
        main
            java
            resources
                META-INF
                    services
                        com.android.tools.lint.client.api.issueRegistry

com.android.tools.lint.client.api.issueRegistry文件中的内容就是registry的完整类名:

com.example.lint.checks.SampleIssueRegistry

java Module中的build.gradle中可以通过以下配置lint:

lintOptions {
    htmlReport true
    htmlOutput file("lint-report.html")
    textReport true
    absolutePaths false
    ignoreTestSources true
}

其他配置和3.0+的一样

下面只是记录一下api相关的东西,方便查找

首先来了解一下Registry,从上面的配置可以看出这个自定义lint配置的入口类,这个类的主要作用就是提供一个检察的列表。

/*
 * The list of issues that will be checked when running <code>lint</code>.
 */
public class SampleIssueRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
                SampleCodeDetector.ISSUE,
                FullImplDetector.ISSUE,
                PrintDetector.ISSUE,
                XLoggerDetector.ISSUE
        );
    }

    @Override
    public int getApi() {
        return ApiKt.CURRENT_API;
    }
}

接下来一起认识一下Issue。下面是源码中的一段注释

An issue is a potential bug in an Android application. An issue is discovered by a [Detector], and has an associated [Severity].

说白了Issue就是问题的简称,其内部囊括了上报问题的一些信息,下面简单的解释下:

public static final Issue ISSUE = Issue.create(
            // the unique id of this issue(唯一ID)
            "ShortUniqueId",

            // 问题的简单描述
            "Lint Mentions",

            // 分析问题的原因,以及提出修改建议.
            "This check highlights string literals in code which mentions " +
                    "the word `lint`. Blah blah blah.\n" +
                    "\n" +
                    "Another paragraph here.\n",
            // 问题所属的主要类别        
            Category.CORRECTNESS,
            // 严重级别 a priority from 1 to 10
            6,
            // 问题的严重程度 the severity of the issues found by this detector
            Severity.WARNING,
            // 问题的检查者(The implementation for the given issue,包括问题检察的detector和扫描的范围)
            new Implementation(
                    SampleCodeDetector.class,
                    Scope.JAVA_FILE_SCOPE));

上面注释中提到Issue是通过Detector来发现的,所以重头戏在Detector,接下来我们来一起简单认识下这位重量级“人物”。

目前比较稳定的版本是26.5,当然其中的也是最新的,网上很多都是之前版本的,这里只介绍26.6版本的api。

Detector是一个abstract类,其中定义了几种扫描接口:

abstract class Detector {
 
 /**
     * See [com.android.tools.lint.detector.api.SourceCodeScanner]; this class is
     * (temporarily) here for backwards compatibility
     可以看到这里已经不推荐使用该接口,留在这里只是为了做兼容,分析文件专用(eg: Java source files (or other similar source files, such as Kotlin files.))
     */
    interface UastScanner : com.android.tools.lint.detector.api.SourceCodeScanner

    /**
     * See [com.android.tools.lint.detector.api.ClassScanner]; this class is
     * (temporarily) here for backwards compatibility
     分析扫描字节码专用(scan bytecode / compiled class files)
     */
    interface ClassScanner : com.android.tools.lint.detector.api.ClassScanner

    /**
     * See [com.android.tools.lint.detector.api.BinaryResourceScanner]; this class is
     * (temporarily) here for backwards compatibility
     分析扫描资源二进制文件专用(scan binary resource files
 * (typically bitmaps but also files in res/raw)
     */
    interface BinaryResourceScanner : com.android.tools.lint.detector.api.BinaryResourceScanner

    /**
     * See [com.android.tools.lint.detector.api.ResourceFolderScanner]; this class is
     * (temporarily) here for backwards compatibility
     分析扫描资源文件夹专用(scan resource folders)
     */
    interface ResourceFolderScanner : com.android.tools.lint.detector.api.ResourceFolderScanner

    /**
     * See [com.android.tools.lint.detector.api.XmlScanner]; this class is (temporarily) here
     * for backwards compatibility
     分析扫描xml文件专用(scan XML files)
     */
    interface XmlScanner : com.android.tools.lint.detector.api.XmlScanner

    /**
     * See [com.android.tools.lint.detector.api.GradleScanner]; this class is (temporarily)
     * here for backwards compatibility
     分析扫描gradle文件专用(scan Gradle files)
     */
    interface GradleScanner : com.android.tools.lint.detector.api.GradleScanner

    /**
     * See [com.android.tools.lint.detector.api.OtherFileScanner]; this class is
     * (temporarily) here for backwards compatibility
     分析扫描其他文件使用(scan other files )
     */
    interface OtherFileScanner : com.android.tools.lint.detector.api.OtherFileScanner


}

扫描顺序

1.  Manifest file
2.  Resource files, in alphabetical order by resource type
    (therefore, "layout" is checked before "values", "values-de" is checked before
    "values-en" but after "values", and so on.
3.  Java sources
4.  Java classes
5.  Gradle files
6.  Generic files
7.  Proguard files
8.  Property files

如果关心的是方法重写以下方法

fun getApplicableMethodNames(): List<String>?

fun visitMethodCall(
        context: JavaContext,
        node: UCallExpression,
        method: PsiMethod
    )

关心构造函数

fun getApplicableConstructorTypes(): List<String>?

fun visitConstructor(
        context: JavaContext,
        node: UCallExpression,
        constructor: PsiMethod
    )

关心引用

fun getApplicableReferenceNames(): List<String>?

fun visitReference(
        context: JavaContext,
        reference: UReferenceExpression,
        referenced: PsiElement
    )

关心资源引用

fun appliesToResourceRefs(): Boolean

fun visitResourceReference(
        context: JavaContext,
        node: UElement,
        type: ResourceType,
        name: String,
        isFramework: Boolean
    )

关心父类

fun applicableSuperClasses(): List<String>?

fun visitClass(context: JavaContext, declaration: UClass)

// 该方法主要针对匿名内部类,及lambda
fun visitClass(context: JavaContext, lambda: ULambdaExpression)

关心注解

/**
     * Returns a list of fully qualified names for super classes that this
     * detector cares about. If not null, this detector will **only** be called
     * if the current class is a subclass of one of the specified superclasses.
     * Lint will invoke [.visitClass] (and sometimes
     * [.visitClass] when it encounters
     * subclasses and lambdas for these types.
     *
     * @return a list of fully qualified names
     */
    fun applicableAnnotations(): List<String>?

    /**
     * Returns whether this detector cares about an annotation usage of the given type.
     * Most detectors are interested in all types except for
     * [AnnotationUsageType.BINARY] and [AnnotationUsageType.EQUALITY], which only
     * apply for annotations that are expressing a "type", e.g. it would be suspicious
     * to combine (compare, add, etc) resources of different type.
     */
    fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean

    /**
     * Whether this lint detector wants to consider annotations on an
     * element that were inherited (e.g. defined on a super class or super method)
     */
    fun inheritAnnotation(annotation: String): Boolean
    
    fun visitAnnotationUsage(
        context: JavaContext,
        usage: UElement,
        type: AnnotationUsageType,
        annotation: UAnnotation,
        qualifiedName: String,
        method: PsiMethod?,
        referenced: PsiElement?,
        annotations: List<UAnnotation>,
        allMemberAnnotations: List<UAnnotation>,
        allClassAnnotations: List<UAnnotation>,
        allPackageAnnotations: List<UAnnotation>
    )

participate in a shared traversal of the tree, registering element types they’re
interested with using {@link #getApplicableUastTypes()} and then providing
a visitor where they implement the corresponding visit methods
For the shared traversal, just provide this handler instead and implement the
appropriate visit methods.

fun getApplicableUastTypes(): List<Class<out UElement>>?

fun createUastHandler(context: JavaContext): UElementHandler?

下面是一个简单的检察Serializable中属性没有实现Serializable例子:

①s继承Detector,这里由于要分析Java文件,实现SourceCodeScanner
public class FullImplDetector extends Detector implements SourceCodeScanner {

    private final String SERIALIZABLE = "java.io.Serializable";
    private final String PARCELABLE = "android.os.Parcelable";

    // ②定义问题Issue
 /**
     * The main issue discovered by this detector
     */
    public static final Issue ISSUE =
            Issue.create(
                    "Parcelable&Serializable",
                    "missing implement Parcelable or Serializable",
                    "make fields that in a class that implement Parcelable or Serializable implements Parcelable or Serializable too ",
                    Category.CORRECTNESS,
                    3,
                    Severity.ERROR,
                    new Implementation(FullImplDetector.class, Scope.JAVA_FILE_SCOPE))
                    .addMoreInfo("http://developer.android.com/reference/android/os/Parcel.html")
                    .setAndroidSpecific(true);


    // 由于这里关心的是实现Serializable或Parcelable的类,所以重写applicableSuperClasses方法
    @Nullable
    @Override
    public List<String> applicableSuperClasses() {
        return Arrays.asList(
                SERIALIZABLE,
                PARCELABLE
        );
    }
    
    // applicableSuperClasses方法对应的方法是visitClass
    @Override
    public void visitClass(@NotNull JavaContext context, @NotNull UClass declaration) {
        JavaEvaluator javaEvaluator = context.getEvaluator();
        UField[] fields = declaration.getFields();
        // 遍历所有field,过滤掉基本数据类型和Bitmap,drawable
        for (UField field : fields) {
            String clazzName = field.getType().getCanonicalText(); // 获取field的类型对应的完整字符串
            if (!isBaseType(clazzName)) {
                // check wrapper eg:array list
//                if (javaEvaluator.extendsClass(javaEvaluator.findClass(clazzName), "java.util.Collection", false)) {
                if (clazzName.contains("<")) { // 这里泛型拿不出来,暂时通过是否包含<来判断是不是容器类型
                    // 取泛型
//                    Type type = ((ParameterizedType)(field.getClass().getGenericSuperclass())).getActualTypeArguments()[0];
                    // 这里获取泛型暂时不知道怎么获取,先使用截取字符串的方法获取完整类名
                    String clazzFullName = getTargetStr(clazzName);

                    if (!isBaseType(clazzFullName)){
                        if (javaEvaluator.extendsClass(declaration,SERIALIZABLE, true)) {
//                            context.getUastContext()--转化
                            if (!javaEvaluator.extendsClass(javaEvaluator.findClass(clazzFullName), SERIALIZABLE, false)) {
                                context.report(ISSUE,context.getLocation(javaEvaluator.findClass(clazzFullName)),clazzFullName+" should implement "+SERIALIZABLE);
                            }
                        }
                        if (javaEvaluator.extendsClass(declaration,PARCELABLE, true)) {
                            if (!javaEvaluator.extendsClass(javaEvaluator.findClass(clazzFullName), PARCELABLE, false)) {
                                context.report(ISSUE,context.getLocation(javaEvaluator.findClass(clazzFullName)),clazzFullName+" should implement "+PARCELABLE);
                            }
                        }
                    }
                } else {
                    // (循环--这里不需要,只要检察一次即可【下次添加的成为新的检察项】)检察是否实现 Parcelable or Serializable
                    if (javaEvaluator.extendsClass(declaration,SERIALIZABLE, true)) {
                        if (!javaEvaluator.extendsClass(javaEvaluator.findClass(clazzName), SERIALIZABLE, false)) {
                            context.report(ISSUE,context.getLocation(javaEvaluator.findClass(clazzName)),clazzName+" should implement "+SERIALIZABLE);
                        }
                    }
                    if (javaEvaluator.extendsClass(declaration,PARCELABLE, true)) {
                        if (!javaEvaluator.extendsClass(javaEvaluator.findClass(clazzName), PARCELABLE, false)) {
                            context.report(ISSUE,context.getLocation(javaEvaluator.findClass(clazzName)),clazzName+" should implement "+PARCELABLE);
                        }
                    }

                }
            }

        }
    }
    
    // 下面是一些工具方法
    
    private String[] types = {
            byte.class.getCanonicalName(),
            int.class.getCanonicalName(),
            short.class.getCanonicalName(),
            double.class.getCanonicalName(),
            float.class.getCanonicalName(),
            char.class.getCanonicalName(),
            String.class.getCanonicalName(),
            boolean.class.getCanonicalName(),
            "android.graphics.Bitmap",// bitmap
            "android.graphics.drawable.Drawable"// drawable
    };

    private boolean isBaseType(String typeClassName) {
        for (String s : types) {
            if (s.equals(typeClassName)) {
                return true;
            }
        }
        return false;
    }
    
    // 提取<>之间的泛型类型的完整字符串
    private String getTargetStr(String s){
        int start = 0;
        int end = s.length();
        if (s.contains("<")){
            start = s.indexOf("<")+1;
        }
        if (s.contains(">")){
            end = s.indexOf(">");
        }
        return s.substring(start,end);
    }

}

执行命令:
gradlew app名字:lintDebug

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值