Как я могу получить доступ к телу методов во время обработки аннотаций Kotlin? - PullRequest
0 голосов
/ 25 октября 2018

Обзор

Мне интересно, есть ли способ, которым я могу применять аннотации к функциям и получать доступ к телу этих функций во время обработки аннотаций.Если невозможно напрямую получить тело метода посредством проверки 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
}

То, что я пробовал

На основании этого ответа , Я попытался использовать 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: {
  }

Обновление

Доступ к Element.enclosingElement в ExecutableElement, представляющем каждую функцию, дает мне квалифицированное имя пакета / модуля, гдефункция определена.Например, addTwoNumbers объявлен как функция верхнего уровня в Main.kt, и во время обработки аннотации я получаю следующий вывод: Processing addTwoNumbers: com.mycompany.testmaster.playground.MainKt.

Есть ли способ получить доступ к исходному исходному файлу (Main.kt) дали эту информацию?

1 Ответ

0 голосов
/ 29 октября 2018

Это было непросто, но мне в итоге удалось найти одно (довольно хакерское) решение для этого.

Я обнаружил, что во время обработки аннотаций 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?)
...