Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using TreeTranslator to rename functions not working for Kotlin

I am trying to rename a method in a Java interface and a function in a Kotlin interface during building according to AST (Abstract Syntax Tree) rewriting. For this question we ignore the implications that renaming a method/function brings for invocations. To find the method/function to rename I am using a custom annotation and annotation processor. I have it working for the Java interface by following these instructions.

I created a new project with three modules. The app module, annotation module and annotation processor module.

The app module is an Android App and contains two separate Java and Kotlin interface files with one annotated method/function each.

RenameJava.java

package nl.peperzaken.renametest;

import nl.peperzaken.renameannotation.Rename;

public interface RenameJava {
    @Rename
    void methodToRename();
}

RenameKotlin.kt

package nl.peperzaken.renametest

import nl.peperzaken.renameannotation.Rename

interface RenameKotlin {
    @Rename
    fun functionToRename()
}

The annotation module is a Java Library that only contains the @Rename annotation and we specify to only allow it on functions and we say it may only be visible in the source code.

Rename.kt

package nl.peperzaken.renameannotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class Rename

The annotation processor module is a Java Library that only contains the processor that iterates the elements that have the annotation and do transformations on them.

RenameProcessor.kt

package nl.peperzaken.renameprocessor

import com.google.auto.service.AutoService
import com.sun.source.util.Trees
import com.sun.tools.javac.processing.JavacProcessingEnvironment
import com.sun.tools.javac.tree.JCTree
import com.sun.tools.javac.tree.TreeTranslator
import com.sun.tools.javac.util.Names
import nl.peperzaken.renameannotation.Rename
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
import javax.tools.Diagnostic

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("nl.peperzaken.renameannotation.Rename")
class RenameProcessor : AbstractProcessor() {

    private lateinit var trees: Trees
    private lateinit var names: Names

    private val visitor = object : TreeTranslator() {
        override fun visitMethodDef(jcMethodDecl: JCTree.JCMethodDecl) {
            super.visitMethodDef(jcMethodDecl)

            // print original declaration
            processingEnv.messager.printMessage(
                Diagnostic.Kind.NOTE,
                jcMethodDecl.toString()
            )

            // Rename declaration
            jcMethodDecl.name = names.fromString("renamed")

            // print renamed declaration
            processingEnv.messager.printMessage(
                Diagnostic.Kind.NOTE,
                jcMethodDecl.toString()
            )

            // commit changes
            result = jcMethodDecl
        }
    }

    @Synchronized
    override fun init(processingEnvironment: ProcessingEnvironment) {
        super.init(processingEnvironment)
        trees = Trees.instance(processingEnvironment)
        val context = (processingEnvironment as JavacProcessingEnvironment).context
        names = Names.instance(context)
    }

    override fun process(set: Set<TypeElement>, roundEnvironment: RoundEnvironment): Boolean {
        // Find elements that are annotated with @Rename
        for (element in roundEnvironment.getElementsAnnotatedWith(Rename::class.java)) {
            val tree = trees.getTree(element) as JCTree
            tree.accept(visitor)
        }
        return true
    }
}

Gradle files

I added the following to the annotation processor build.gradle:

// Add annotation dependency
implementation project(':rename-annotation')
// Used to generate META-INF so the processor can run
compile 'com.google.auto.service:auto-service:1.0-rc4'
kapt "com.google.auto.service:auto-service:1.0-rc4"
// To prevent unresolved references during building. You might not need this dependency.
implementation files("${System.getProperty('java.home')}/../lib/tools.jar")

I added the following to the app build.gradle:

compileOnly project(':rename-annotation')
annotationProcessor project(':rename-processor')

The annotation build.gradle doesn't have dependencies besides the default generated ones.

The reason we have different modules is so we can prevent the annotation and processor being build into the final APK since we only need those during building.

Output

The log shows the method in the Java interface was renamed:

Note: 
  @Rename()
  void methodToRename();
Note: 
  @Rename()
  void renamed();

There was no log generated for the Kotlin interface. Indicating the annotation processor did not run.

When you take a look at classes.dex of the generated APK then you will see the following:

Output annotationProcessor

You can see that the method of the Java interface has been renamed properly. While the function of the Kotlin interface has not. Even though it shows up in the log.

You will also notice this warning in the log:

app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'RenameTest:rename-processor:unspecified' and apply the kapt plugin: "apply plugin: 'kotlin-kapt'".

So lets do what the warning suggests. Add apply plugin: 'kotlin-kapt' to app build.gradle and change annotationProcessor to kapt. After sync and rebuild the output is:

Note: 
  @Rename()
  void methodToRename();
Note: 
  @Rename()
  void renamed();
Note: 
  @nl.peperzaken.renameannotation.Rename()
  public abstract void functionToRename();
Note: 
  @nl.peperzaken.renameannotation.Rename()
  public abstract void renamed();

Logs for the Java and Kotlin file are both appearing. Success you think? Looking at classes.dex of the newly generated APK will make you think otherwise since both are in their original form:

Output kapt

Question

Is there a way to get the desired output in the final APK? With the output being that both the method in the Java interface and the function in the Kotlin interface are renamed.

Link to sample project: https://github.com/peperzaken/kotlin-annotation-rename-test

like image 387
stefana Avatar asked Jun 29 '18 11:06

stefana


1 Answers

Kapt does not process Kotlin files directly – it runs annotation processing over Java file stubs instead. So the changes in AST tree for Kotlin files are only visible to other annotation processors and do not affect compilation.

Note that Java AST API is not a part of the annotation processing API (JSR 269) - it's actually an internal Javac API, and, obviously, Kotlinc is not Javac.

The more reliable approach to solve your problem would be a class file post-processing (or a Kotlin compiler plugin, but then it won't work for Java).

Also, in Kotlin you have the @JvmName() annotation that changes the JVM declaration name.

like image 199
yanex Avatar answered Oct 20 '22 08:10

yanex