创建扫描器

create a detector

声明好了 Issue,我们需要一个具体的执行者去完成扫描的逻辑。下面我们通过 MyDetector 这个类来扫描所有源代码文件当中,是否存在名为 "helloWorld" 的方法。

class MyDetector : Detector(), SourceCodeScanner {

    companion object {
        val ISSUE_HELLO_WORLD: Issue = IssueFactory.create(
            "HelloWorld",
            "Don't use method helloWorld",
            "Don't use method helloWorld, because it's too simple",
            Category.CORRECTNESS,
            10,
            Severity.WARNING,
            Implementation(
                MyDetector::class.java,
                EnumSet.of(Scope.JAVA_FILE)
            )
        )
    }

    override fun getApplicableMethodNames() = listOf("helloWorld")

    override fun visitMethod(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        System.out.println("yymobile: method: $node return: ${node.returnType}")
    }
}

上面实现非常简单,MyDetector 必须继承 Detector 类并实现 SourceCodeScanner 接口,通过 getApplicableMethodNames 方法返回我们感兴趣的一个或多个方法名,然后就可以在 visitMethod 方法中处理扫描回调,这里只是简单的把回调参数打印为日志。

:app module 中,也就是被静态扫描的项目模块,有分别用 JavaKotlin 实现的代码,如下:

上面的是 Java 版实现,Kotlin版实现也是相同的行为。然后我们执行 :app->verification->lintDebug 的 Gradle Task,对 Demo 类做一次扫描,然后可以看到有下面的日志打印结果:

yymobile: method: org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression@72c40423 return: PsiType:int

yymobile: method: helloWorld("whatever") return: PsiType:int

第一行是扫描 Kotlin 类打印的日志,第二行是扫描 Java类打印的日志。通过日志我们可以发现 MyDetector 已经成功地扫描到了 helloWorld 这个方法,并且知道返回类型是 int 类型。通过第二行Java类的日志,我们可以更清晰地看到 helloWorld("whatever") 这个方法调用和参数都打印了出来,而 Kotlin 的Expression类根本没有实现 toString 方法。这也可以看出 LintKotlin的支持还有非常大的进步空间,对Java的语法树处理会更成熟一点,相信以后的版本会有改进。

我们可以发现 visitMethod 回调中有三个参数,JavaContext 类型是静态代码扫描的上下文,这个类非常重要,地位相当于 Android 中的 android.content.Context 类,后面可以看到更多关于这个类的作用和用法。UCallExpressionPsiMethod 都是语法树的节点类型,它们分别属于两个系列的节点:U开头的 UElement 系列节点和 Psi 开头的 PsiElement 系列节点。更详细的介绍会在语法树这章说明。

除了 visitMethod 以外,还有很多其他节点我们可以去关心。比如有以下这些:

列出关心的元素

语法树回调

fun getApplicableMethodNames(): List<String>?

fun visitMethod(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)

...

...

很多诸如此类的 visit... 方法,可以通过这些方法来访问自己关心的节点。如何知道想扫描的代码句属于哪种语法树节点?这个会在语法树章节详细说明。

然而事实上,这些 visit... 的方法并不是最常用的方法。他们相当于是一种快捷方式,当你很明确要扫描的类名或者方法名具体是什么,才能使用这样的方式。但如果是更复杂的规则,比如要扫描所有形如A.B()这样格式的方法调用,且A的类型是C类的子类,visit... 的方法就会捉襟见肘。

更多的情况下会重写 fun createUastHandler(context: JavaContext): UElementHandler? 方法来返回一个自定义的 'visitor',这个访问器会遍历语法树并回调:

重写 getApplicableUastTypes 方法并返回关心的类型,比如这里对所有的类声明(UClass)和所有的方法声明(UMethod) 进行访问。然后重写 createUastHandler 方法返回一个自定义的实现类 MyVisitor,在这个类中重写 visitClassvisitMethod 方法,也是简单的日志打印:

visitClass com.yy.mobile.lintdemo.Demo

visitMethod method1

visitMethod helloWorld

我们的访问器成功地遍历到了整个 Demo 类。

Last updated