Это было непросто, но мне в итоге удалось найти одно (довольно хакерское) решение для этого.
Я обнаружил, что во время обработки аннотаций Kotlin генерировал файлы метаданных во временном каталоге вывода сборки.Эти файлы метаданных содержали сериализованную информацию, которая включала пути к исходным файлам, содержащим аннотации, которые я обрабатывал:
Просматривая исходный код для плагина Kapt, я обнаружил этот файл , который позволил мне выяснить, как десериализовать информацию в этих файлах, позволив мне извлечь местоположения исходного исходного кода.
Я создал объект 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 в своем проекте с помощью плагина Kapt Gradle (в настоящее время версия 1.3.0-rc-198), который определяет каталоги, в которых хранятся все созданные файлы (включая файлы метаданных).,Затем я делаю предположение, что файлы метаданных хранятся в /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?)