枚举包中定义的扩展属性

Enumerate the extension properties defined in a package

我正在用 Kotlin 编写 HTML 模板语言。

我的模板引擎需要通过查找 "myProperty" 来解析 属性 表达式,例如 obj.myProperty,而不仅仅是在 obj 的 class 和 superclasses,但也在用户指定的 Kotlin 包列表中定义的 扩展属性 中。

例如,如果我的解释器正在评估 x.absoluteValue 并且 x 结果是一个 Int,我有以下信息:

我可以使用什么 API 来获取给定包中定义的所有顶级扩展属性的列表,比如 kotlin.math,作为反映项目的列表,例如 List<KProperty<*>>?在模板编译时(即 Kotlin 运行时),我将浏览该扩展列表并寻找一个名为 "absoluteValue" 与 Int 接收器兼容的扩展。

我知道我可以在导入后手动定义扩展属性列表,例如 listOf(Int::absoluteValue, ...),但我希望我的用户指定一个包列表,而不是单个属性。


更新:我决定将我的模板引擎基于 Kotlin 的 JSR-223 支持,javax.script.ScriptEngineManager,因此使用稳定的 API 并让 Kotlin 编译器解析它认为合适的扩展属性。

关于 kotlin 扩展函数的一些知识:

  • 从 java 的角度来看,kotlin 扩展函数是静态方法,它们将 class 作为参数进行扩展

这使得它们很难与常规的旧静态函数区分开来。起初我什至不确定有什么不同。

所以让我们看看是否存在差异。

ExtensionFunctions.kt 中的声明:

class Test

fun bar(test : Test){}

fun Test.bar2(){}

fun Test.foo45(bar : Test, i :Int): Int = i

一些命令行:


francis@debian:~/test76/target/classes/io/github/pirocks$  javap -p -c -s -l ExtensionFunctionsKt.class 
Compiled from "ExtensionFunctions.kt"
public final class io.github.pirocks.ExtensionFunctionsKt {
  public static final void bar(io.github.pirocks.Test);
    descriptor: (Lio/github/pirocks/Test;)V
    Code:
       0: aload_0
       1: ldc           #9                  // String test
       3: invokestatic  #15                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: return
    LineNumberTable:
      line 6: 6
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  test   Lio/github/pirocks/Test;

  public static final void bar2(io.github.pirocks.Test);
    descriptor: (Lio/github/pirocks/Test;)V
    Code:
       0: aload_0
       1: ldc           #19                 // String $this$bar2
       3: invokestatic  #15                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: return
    LineNumberTable:
      line 8: 6
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0 $this$bar2   Lio/github/pirocks/Test;

  public static final int foo45(io.github.pirocks.Test, io.github.pirocks.Test, int);
    descriptor: (Lio/github/pirocks/Test;Lio/github/pirocks/Test;I)I
    Code:
       0: aload_0
       1: ldc           #23                 // String $this$foo45
       3: invokestatic  #15                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_1
       7: ldc           #24                 // String bar
       9: invokestatic  #15                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
      12: iload_2
      13: ireturn
    LineNumberTable:
      line 10: 12
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      14     0 $this$foo45   Lio/github/pirocks/Test;
          0      14     1   bar   Lio/github/pirocks/Test;
          0      14     2     i   I

  <further output omitted>
francis@debian:~/test76/target/classes/io/github/pirocks$ 


如您所见,除了第一个参数名称外,常规静态函数和扩展函数之间没有太大区别。扩展函数参数命名为 $this$functionName。我们可以使用它来通过解析字节码和检查参数名称来确定函数是否具有扩展变量。值得一提的是,这有点 hacky,如果所讨论的 classes 已通过字节码混淆器 运行,则可能无法正常工作。因为自己编写字节码解析器需要大量工作,所以我使用 commons-bcel 为我完成所有工作。

ExtensionFunctions.kt:

package io.github.pirocks
import org.apache.bcel.classfile.ClassParser

class Test

fun bar(test : Test){}

fun Test.bar2(){}

fun Test.foo45(bar : Test, i :Int): Int = i


fun main(args: Array<String>) {
    val classFileInQuestionStream = "Just wanted an object instance".javaClass.getResourceAsStream("/io/github/pirocks/ExtensionFunctionsKt.class")!!
    val parsedClass = ClassParser(classFileInQuestionStream, "ExtensionFunctionsKt.class").parse()
    parsedClass.methods.forEach { method ->
        if(method.localVariableTable.localVariableTable.any {
            it.name == ("$this$${method.name}")
        }){
            println("Is an extension function:")
            println(method)
        }
    }

}

上面应该输出:

Is an extension function:
public static final void bar2(io.github.pirocks.Test $this$bar2) [RuntimeInvisibleParameterAnnotations]
Is an extension function:
public static final int foo45(io.github.pirocks.Test $this$foo45, io.github.pirocks.Test bar, int i) [RuntimeInvisibleParameterAnnotations]

Commons-bcel 还可以为您提供每个扩展功能的 type/name/attribute 信息。

您在问题中提到使用 Int 上的扩展函数来执行此操作。这比较棘手,因为声明了 absoluteValue,谁知道在哪里(Intellij Ctrl+B 告诉我它位于这个名为 MathH.kt 的巨大文件中,实际上是 MathKt.class,在包中kotlin.math,在 Maven 包含的一些随机 jar 中)。由于不是每个人都会从 maven 获得相同的随机 jar,最好的做法是在 System.getProperty("java.class.path") 中寻找 kotlin 标准库。恼人的是 absoluteValue 被声明为内联函数,因此在 stdlib jar 中没有它的踪迹。并非所有 kotlin stdlib 扩展函数都是如此。所以你可以使用下面的方法获取 stdlib 中的所有扩展函数(更正:有两个 stdlib jar,所以这只获取在 kotlin-stdlib-version-number 中声明的扩展函数)。

package io.github.pirocks

import org.apache.bcel.classfile.ClassParser
import java.nio.file.Paths
import java.util.jar.JarFile


class Test

fun bar(test: Test) {}

fun Test.bar2() {}

fun Test.foo45(bar: Test, i: Int): Int = i


fun main(args: Array<String>) {
    val jarPath = System.getProperty("java.class.path").split(":").filter {
        it.contains(Regex("kotlin-stdlib-[0-9]\.[0-9]+\.[0-9]+\.jar"))
    }.map {
        Paths.get(it)
    }.single()//if theres more than one kotlin-stdlib we're in trouble

    val theJar = JarFile(jarPath.toFile())
    val jarEntries = theJar.entries()

    while (jarEntries.hasMoreElements()) {
        val entry = jarEntries.nextElement()
        if (entry.name.endsWith(".class")) {
            val cp = ClassParser(theJar.getInputStream(entry), entry.getName())
            val javaClass = cp.parse()
            javaClass.methods.forEach { method ->
                if (method.localVariableTable?.localVariableTable?.any {
                        it.name == ("$this$${method.name}")
                    } == true) {
                    println("Is an extension function:")
                    println(method)
                }

            }
        }


    }
}


编辑:

关于实际回答有关如何在包中获取扩展函数的问题:

您需要遍历 class 路径中的每个条目,包括 classes 和 jar,并检查是否有任何 classes 匹配所需的包。至于判断一个class的包可以使用commons-bcel函数JavaClass::getPackageName.