kapt: Annotation Processing for Kotlin

As there have been many requests to support Java Annotation Processing, we are working on it, and first results are ready for preview. This is the call for early feedback.

We are planning to release the initial support for JSR 269 Annotation Processing in M12, which is planned for the end of May. Meanwhile, the bulk of the work is already done, and you can test it by using the SNAPSHOT version of the Kotlin plugin for Gradle. The support is rather limited, but Dagger 2 works

Annotation Processing Basics

JSR 269 defines an API for a special kind of plugins for the Java compiler, annotation processors. Such a plugin can ask the compiler, roughly, “what code elements (classes, methods, fields) are annotated with @Foo?” The compiler returns a collection of objects that represent annotated elements. Then, the processor can inspect them and generate some new code that will be compiled during the same pass as the annotated code. The trick is that the generated code can be used by the hand-written code although it did not exist when the compiler started working.

To support Annotation Processing in a language other than Java, one has a few options:

One. Re-implement the JSR 269 APIs. This requires some work, but is not terribly hard. The problem is that it does not helped in a mixed project: in the end we need to process annotated element from both Java and Kotlin code, and only supporting JSR 269 in Kotlin is not that much of a gain.

Two. Generate Java sources from Kotlin sources and then feed them into the Java compiler that in turn runs the processors. Of course, it’s too hard to translate Kotlin to completely working Java code (translation of method bodies would be extremely painful), but all that’s really needed is just declarations. This is the way Groovy does it: by generating Java “stubs” from Groovy code and feeding them to the Java compiler. This would work for Kotlin too, but requires two runs of the compiler: first to generate stubs, and second to compile fully against generated code. Referring to classes generated by the processors is possible, but there will be issues in some cases (inferring property/function types from the right-hand-side expressions that use generated code, while generating stubs).

Three. Pretend that Kotlin binaries are Java sources. Normally, the Kotlin compiler runs first and the Java compiler sees the Kotlin code as binary .class files. Of course, all code elements, both source and binary, are represented uniformly inside the Java compiler. So instead of getting only annotated Java sources, the processor may also get annotated Kotlin binaries, it won’t notice the difference through the available API. Unfortunately, Javac won’t do that automatically, but we can plug in between the Java compiler and the annotation processor, find the binary elements ourselves and add them to the source elements normally returned by javac. A huge advantage is that this solution is rather easy to implement (involves a little bytecode generation, but we are kind of used to it:) ). There are a few important limitations, though: Kotlin code can not refer to the declarations generated by the processor and source-retained annotations are not visible through binaries. (Update: both limitations have been lifted later on.)

For now, we went for the option three under then name of kapt (Kotlin Annotation Processing). It looks like it enables the most important real-life use cases. We may support option two later, though.

Example: Using Dagger 2

kapt is supported by the Gradle plugin, to enable it, add

1

2

3

4

5

6

dependencies{

...

compile'com.google.dagger:dagger:2.0'

kapt'com.google.dagger:dagger-compiler:2.0'

provided'org.glassfish:javax.annotation:10.0-b28'

to your build script. Annotations will be picked up from both your Java and Kotlin code.

Since kapt is not released yet, it is only available from the SNAPSHOT version of Kotlin:

1

2

3

4

5

6

7

8

9

10

11

12

13

buildscript{

repositories{

...

maven{

url'http://oss.sonatype.org/content/repositories/snapshots'

}

}

dependencies{

...

classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:0.1-SNAPSHOT"

}

}

Due to limitations mentioned above, the class(es) using code generated by Dagger have to be written in Java (normally, it is very little code):

1

2

3

4

5

6

7

8

9

10

import android.app.Application;

publicabstractclassBaseApplicationextendsApplication{

protectedApplicationComponent createApplicationComponent(){

returnDaggerApplicationComponent.builder()

.androidModule(newAndroidModule(this)).build();

}

}

Everything else can be written in Kotlin. Here’s an example project demonstrating usage of Dagger: kotlin-dagger (you may need to enforce re-downloading of snapshot artifacts).

Feedback

We’d really appreciate if you tried kapt in your project and gave us feedback:

@Andrey, I am a little confused by your explanation of limitations for the third option. I guess I don’t know the compilation process well enough. What documentation did you use? Don’t know what to google for. Could you please give me to a starting point?

Try:
Run with --debug option to get more log output.
Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:compileDebugJava'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:64)
at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:42)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure(AbstractTask.java:305)
at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.executeTask(AbstractTaskPlanExecutor.java:79)
at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:63)
at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:51)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:23)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:88)
at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:29)
at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
at org.gradle.execution.DefaultBuildExecuter.access$200(DefaultBuildExecuter.java:23)
at org.gradle.execution.DefaultBuildExecuter$2.proceed(DefaultBuildExecuter.java:68)
at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:55)
at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:149)
at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:106)
at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:86)
at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:80)
at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:33)
at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:24)
at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:36)
at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:51)
at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:171)
at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:237)
at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:210)
at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:35)
at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:24)
at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:206)
at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:169)
at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
at org.gradle.launcher.Main.doAction(Main.java:33)
at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:54)
at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:35)
at org.gradle.launcher.GradleMain.main(GradleMain.java:23)
at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:33)
at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:130)
at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:48)
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodError: kotlin.KotlinPackage.split$default(Ljava/lang/String;[CZII)Ljava/util/List;
at com.sun.tools.javac.main.Main.compile(Main.java:553)
at com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:129)
at com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:138)
at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:42)
at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:35)
at org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler.delegateAndHandleErrors(NormalizingJavaCompiler.java:97)
at org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler.execute(NormalizingJavaCompiler.java:50)
at org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler.execute(NormalizingJavaCompiler.java:36)
at org.gradle.api.internal.tasks.compile.CleaningJavaCompilerSupport.execute(CleaningJavaCompilerSupport.java:34)
at org.gradle.api.internal.tasks.compile.CleaningJavaCompilerSupport.execute(CleaningJavaCompilerSupport.java:25)
at org.gradle.api.tasks.compile.JavaCompile.performCompilation(JavaCompile.java:158)
at org.gradle.api.tasks.compile.JavaCompile.compile(JavaCompile.java:138)
at org.gradle.api.tasks.compile.JavaCompile.compile(JavaCompile.java:92)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:63)
at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.doExecute(AnnotationProcessingTaskFactory.java:235)
at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:211)
at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.execute(AnnotationProcessingTaskFactory.java:222)
at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:200)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
... 47 more
Caused by: java.lang.NoSuchMethodError: kotlin.KotlinPackage.split$default(Ljava/lang/String;[CZII)Ljava/util/List;
at org.jetbrains.kotlin.annotation.KotlinAnnotationProvider.readAnnotations(KotlinAnnotationProvider.kt:56)
at org.jetbrains.kotlin.annotation.KotlinAnnotationProvider.access$readAnnotations$0(KotlinAnnotationProvider.kt:22)
at org.jetbrains.kotlin.annotation.KotlinAnnotationProvider$annotatedKotlinElements$1.invoke(KotlinAnnotationProvider.kt:34)
at org.jetbrains.kotlin.annotation.KotlinAnnotationProvider$annotatedKotlinElements$1.invoke(KotlinAnnotationProvider.kt:22)
at kotlin.properties.LazyVal.get(Delegation.kt:169)
at org.jetbrains.kotlin.annotation.KotlinAnnotationProvider.getAnnotatedKotlinElements(KotlinAnnotationProvider.kt)
at org.jetbrains.kotlin.annotation.AnnotationProcessorWrapper.process(AnnotationProcessorWrapper.kt:94)
at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:856)
at com.sun.tools.javac.main.Main.compile(Main.java:523)
... 66 more

Having just educated myself on what Dagger and Dagger 2 are, it seems to me that this is the right direction, but ultimately not far enough. My philosophy (and one which resonates well with Kotlin) is that boilerplate is a sign that the language is missing a feature. Dagger 2 writes DI boilerplate for you.

This is too big a project for Kotlin 1.0, but there’s clearly a need for language features that do DI at compile time, so you don’t need a separate code generator like Dagger 2.

As I see from this example kotlin-dagger little java wrapper for DaggerApplicationComponent is not a problem at all.
Unpleasant restriction is that you need to use next line everywhere(getApplication() as DemoApplication).component().inject(this)

Everything worked like charm. So far… I just couldn’t make it to work in tests.
DaggerTestSomeComponent was not generated from TestSomeComponent : SomeComponent. I’ve tried various combinations. Including creating absolutely new component just in tests. Still nothing. Is there any workaround or could I provide some other details?