【问题标题】:How can I access the body of methods during Kotlin annotation processing?在 Kotlin 注释处理期间如何访问方法体?
【发布时间】:2018-10-25 09:15:35
【问题描述】:

概述

我想知道是否有一种方法可以将注释应用于函数并在注释处理期间访问这些函数的主体。如果无法通过检查注解处理器中的Element 对象直接获取方法体,是否有其他替代方法可以访问应用这些注解的函数的源代码?

详情

作为我正在进行的项目的一部分,我正在尝试使用 kapt 来检查使用特定类型注释注释的 Kotlin 函数并基于它们生成类。例如,给定一个这样的注释函数:

@ElementaryNode
fun addTwoNumbers(x: Int, y: Int) = x + y

我的注释处理器当前生成这个:

class AddTwoNumbers : Node {
    val x: InputPort<Int> = TODO("implement node port property")

    val y: InputPort<Int> = TODO("implement node port property")

    val output: OutputPort<Int> = TODO("implement node port property")
}

但是,我需要在这个类中包含原始函数本身,基本上就像它作为私有函数被复制/粘贴一样:

class AddTwoNumbers : Node {
    val x: InputPort<Int> = TODO("implement node port property")

    val y: InputPort<Int> = TODO("implement node port property")

    val output: OutputPort<Int> = TODO("implement node port property")

    private fun body(x: Int, y: Int) = x + y
}

我的尝试

基于this answer,我尝试使用com.sun.source.util.Trees访问注解函数对应的ExecutableElement的方法体:

override fun inspectElement(element: Element) {
    if (element !is ExecutableElement) {
        processingEnv.messager.printMessage(
            Diagnostic.Kind.ERROR,
            "Cannot generate elementary node from non-executable element"
        )

        return
    }

    val docComment = processingEnv.elementUtils.getDocComment(element)
    val trees = Trees.instance(processingEnv)
    val body = trees.getTree(element).body
    processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, "Processing ${element.simpleName}: $body")
}

但是,kapt 只生成方法体的存根,所以我得到的每个方法体都是这样的:

$ gradle clean build
...
> Task :kaptGenerateStubsKotlin
w: warning: Processing addTwoNumbers: {
      return 0;
  }
w: warning: Processing subtractTwoNumbers: {
      return 0.0;
  }
w: warning: Processing transform: {
      return null;
  }
w: warning: Processing minAndMax: {
      return null;
  }
w: warning: Processing dummy: {
  }

更新

访问代表每个函数的ExecutableElement 上的Element.enclosingElement 为我提供了定义函数的包/模块的限定名称。例如,addTwoNumbersMain.kt 中被声明为顶级函数,并且在注释处理期间我得到这个输出:Processing addTwoNumbers: com.mycompany.testmaster.playground.MainKt

有没有办法在给定这些信息的情况下访问原始源文件 (Main.kt)?

【问题讨论】:

    标签: kotlin annotation-processing


    【解决方案1】:

    这并不容易,但我最终设法为此找到了一个(相当老套的)解决方案。

    我发现在注释处理过程中,Kotlin 在临时构建输出目录下生成元数据文件。这些元数据文件包含序列化信息,其中包括包含我正在处理的注释的原始源文件的路径:

    查看 Kapt 插件的源代码,我找到了this file,这让我能够弄清楚如何反序列化这些文件中的信息,从而使我能够提取原始源代码的位置。

    我创建了一个 Kotlin 对象 SourceCodeLocator 将所有内容放在一起,这样我就可以传递一个 Element 代表一个函数,它会返回一个包含它的源代码的字符串表示形式:

    package com.mycompany.testmaster.nodegen.parsers
    
    import com.mycompany.testmaster.nodegen.KAPT_KOTLIN_GENERATED_OPTION_NAME
    import com.mycompany.testmaster.nodegen.KAPT_METADATA_EXTENSION
    import java.io.ByteArrayInputStream
    import java.io.File
    import java.io.ObjectInputStream
    import javax.annotation.processing.ProcessingEnvironment
    import javax.lang.model.element.Element
    import javax.lang.model.element.ElementKind
    import javax.lang.model.element.ExecutableElement
    
    internal object SourceCodeLocator {
        fun sourceOf(function: Element, environment: ProcessingEnvironment): String {
            if (function !is ExecutableElement)
                error("Cannot extract source code from non-executable element")
    
            return getSourceCodeContainingFunction(function, environment)
        }
    
        private fun getSourceCodeContainingFunction(function: Element, environment: ProcessingEnvironment): String {
            val metadataFile = getMetadataForFunction(function, environment)
            val path = deserializeMetadata(metadataFile.readBytes()).entries
                .single { it.key.contains(function.simpleName) }
                .value
    
            val sourceFile = File(path)
            assert(sourceFile.isFile) { "Source file does not exist at stated position within metadata" }
    
            return sourceFile.readText()
        }
    
        private fun getMetadataForFunction(element: Element, environment: ProcessingEnvironment): File {
            val enclosingClass = element.enclosingElement
            assert(enclosingClass.kind == ElementKind.CLASS)
    
            val stubDirectory = locateStubDirectory(environment)
            val metadataPath = enclosingClass.toString().replace(".", "/")
            val metadataFile = File("$stubDirectory/$metadataPath.$KAPT_METADATA_EXTENSION")
    
            if (!metadataFile.isFile) error("Cannot locate kapt metadata for function")
            return metadataFile
        }
    
        private fun deserializeMetadata(data: ByteArray): Map<String, String> {
            val metadata = mutableMapOf<String, String>()
    
            val ois = ObjectInputStream(ByteArrayInputStream(data))
            ois.readInt() // Discard version information
    
            val lineInfoCount = ois.readInt()
            repeat(lineInfoCount) {
                val fqName = ois.readUTF()
                val path = ois.readUTF()
                val isRelative = ois.readBoolean()
                ois.readInt() // Discard position information
    
                assert(!isRelative)
    
                metadata[fqName] = path
            }
    
            return metadata
        }
    
        private fun locateStubDirectory(environment: ProcessingEnvironment): File {
            val kaptKotlinGeneratedDir = environment.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
            val buildDirectory = File(kaptKotlinGeneratedDir).ancestors.firstOrNull { it.name == "build" }
            val stubDirectory = buildDirectory?.let { File("${buildDirectory.path}/tmp/kapt3/stubs/main") }
    
            if (stubDirectory == null || !stubDirectory.isDirectory)
                error("Could not locate kapt stub directory")
    
            return stubDirectory
        }
    
        // TODO: convert into generator for Kotlin 1.3
        private val File.ancestors: Iterable<File>
            get() {
                val ancestors = mutableListOf<File>()
                var currentAncestor: File? = this
    
                while (currentAncestor != null) {
                    ancestors.add(currentAncestor)
                    currentAncestor = currentAncestor.parentFile
                }
    
                return ancestors
            }
    }
    

    注意事项

    这个解决方案似乎对我有用,但我不能保证它适用于一般情况。特别是,我通过Kapt Gradle plugin(当前版本为1.3.0-rc-198)在我的项目中配置了Kapt,它确定了所有生成的文件(包括元数据文件)的存储目录。然后我假设元数据文件存储在项目构建输出文件夹下的/tmp/kapt3/stubs/main

    我在 JetBrain 的问题跟踪器中创建了一个功能请求,以使这个过程更容易、更可靠,因此不需要这些黑客攻击。

    示例

    就我而言,我已经能够使用它来转换源代码,如下所示:

    minAndMax.kt

    package com.mycompany.testmaster.playground.nodes
    
    import com.mycompany.testmaster.nodegen.annotations.ElementaryNode
    
    @ElementaryNode
    private fun <T: Comparable<T>> minAndMax(values: Iterable<T>) =
        Output(values.min(), values.max())
    private data class Output<T : Comparable<T>>(val min: T?, val max: T?)
    

    并生成这样的源代码,包含原始源代码的修改版本:

    MinAndMax.gen.kt

    // This code was generated by the <Company> Test Master node generation tool at 2018-10-29T08:31:35.847.
    //
    // Do not modify this file. Any changes may be overwritten at a later time.
    package com.mycompany.testmaster.playground.nodes.gen
    
    import com.mycompany.testmaster.domain.ElementaryNode
    import com.mycompany.testmaster.domain.InputPort
    import com.mycompany.testmaster.domain.OutputPort
    import com.mycompany.testmaster.domain.Port
    import kotlin.collections.Set
    import kotlinx.coroutines.async
    import kotlinx.coroutines.coroutineScope
    
    class MinAndMax<T : Comparable<in T>> : ElementaryNode() {
        private val _values: Port<Iterable<out T>> = Port<Iterable<out T>>()
    
        val values: InputPort<Iterable<out T>> = _values
    
        private val _min: Port<T?> = Port<T?>()
    
        val min: OutputPort<T?> = _min
    
        private val _max: Port<T?> = Port<T?>()
    
        val max: OutputPort<T?> = _max
    
        override val ports: Set<Port<*>> = setOf(_values, _min, _max)
    
        override suspend fun executeOnce() {
            coroutineScope {
                val values = async { _values.receive() }
                val output = _nodeBody(values.await())
                _min.forward(output.min)
                _max.forward(output.max)
            }
        }
    }
    
    
    
    private  fun <T: Comparable<T>> _nodeBody(values: Iterable<T>) =
        Output(values.min(), values.max())
    private data class Output<T : Comparable<T>>(val min: T?, val max: T?)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-01-31
      • 2023-03-04
      • 2020-06-17
      • 1970-01-01
      • 2018-09-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多