Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin-friendly Annotation Processing (AppsConf 2018)

Kotlin-friendly Annotation Processing (AppsConf 2018)

When promoting Kotlin over Java to your friend the first argument you’ll probably use will be its powerful syntax: in-house nullability support, default arguments, delegated properties, data & sealed classes, etc. These features are really the stuff we’re benefiting from everyday, but if we’ll take Java annotation processors then sadly the only information they can get about our code will be Java-related and no info about Kotlin-specific features.

In this talk we’ll find out how to use Kotlin metadata added by the compiler to get info about Kotlin language features in the code and utilise it during annotation processing.

Video: https://youtu.be/rJ9BRDuvJ1I

Sergey Ryabov

October 08, 2018
Tweet

More Decks by Sergey Ryabov

Other Decks in Programming

Transcript

  1. • Annotation processing works inside javac • Separate Annotation Processors

    are pluggable • Gradle’s annotationProcessor dependencies configuration JSR-269
  2. • Annotation processing works inside javac • Separate Annotation Processors

    are pluggable • Gradle’s annotationProcessor dependencies configuration • /META-INF/services/javax.annotation.processing.Processor 
 declares your AP’s main entry point JSR-269
  3. • Annotation processing works inside javac • Separate Annotation Processors

    are pluggable • Gradle’s annotationProcessor dependencies configuration • /META-INF/services/javax.annotation.processing.Processor 
 declares your AP’s main entry point • Multiple rounds of processing JSR-269
  4. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? Kotlin-way
  5. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? Kotlin-way
  6. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? • Precompile Kotlin classes and feed them to javac? Kotlin-way
  7. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? • Precompile Kotlin classes and feed them to javac? Kotlin-way
  8. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? • Precompile Kotlin classes and feed them to javac? Kotlin-way
  9. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? • Precompile Kotlin classes and feed them to javac? Kotlin-way
  10. • APT can’t process Kotlin sources — build KAPT •

    Kotlin’s own JSR-269 implementation? • Generate Java code/stubs from Kotlin? • Precompile Kotlin classes and feed them to javac? • No multiple APT rounds over generated Kotlin files Kotlin-way
  11. class KotlinProcessor : AbstractProcessor() { override fun getSupportedOptions(): Set<String> override

    fun getSupportedAnnotationTypes(): Set<String> override fun getSupportedSourceVersion(): SourceVersion } JSR-269
  12. class KotlinProcessor : AbstractProcessor() { override fun getSupportedOptions(): Set<String> override

    fun getSupportedAnnotationTypes(): Set<String> override fun getSupportedSourceVersion(): SourceVersion override fun init(processingEnv: ProcessingEnvironment) } JSR-269
  13. class KotlinProcessor : AbstractProcessor() { override fun getSupportedOptions(): Set<String> override

    fun getSupportedAnnotationTypes(): Set<String> override fun getSupportedSourceVersion(): SourceVersion override fun init(processingEnv: ProcessingEnvironment) override fun process( annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean } JSR-269
  14. class KotlinProcessor : AbstractProcessor() { override fun getSupportedOptions(): Set<String> override

    fun getSupportedAnnotationTypes(): Set<String> override fun getSupportedSourceVersion(): SourceVersion override fun init(processingEnv: ProcessingEnvironment) override fun process( annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean } JSR-269
  15. Problems • No info about Kotlin-specifics is available for Annotation

    Processors • No Nullability • No default arguments
  16. Problems • No info about Kotlin-specifics is available for Annotation

    Processors • No Nullability • No default arguments • No sealed & data classes
  17. Problems • No info about Kotlin-specifics is available for Annotation

    Processors • No Nullability • No default arguments • No sealed & data classes • …
  18. Problems • No info about Kotlin-specifics is available for Annotation

    Processors • No Nullability • No default arguments • No sealed & data classes • … • No nothing!
  19. Dad! APT can’t get any Kotlin info. And StackOverflow can’t

    help! If even StackOverflow keeps silence…
  20. Dad! APT can’t get any Kotlin info. And StackOverflow can’t

    help! If even StackOverflow keeps silence… B-b-b-but compiler..?
  21. Dad! APT can’t get any Kotlin info. And StackOverflow can’t

    help! If even StackOverflow keeps silence… B-b-b-but compiler..? Well, this city has a Hero!
  22. @Metadata( mv = {1, 1, 11}, bv = {1, 0,

    2}, k = 1, d1 = {"\u0000\u0080\u0001\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u00… d2 = {""Lxyz/ryabov/gsonkot/User;", "", "id", "", "username", "", "isAdmin ) public final class User { ... }
  23. @Metadata • Provides Kotlin-specific info • Class-level annotation • Accessible

    both at compile time and runtime • Used by compiler as well
  24. @Metadata • metadataVersion • bytecodeVersion • kind • data1 •

    data2 • extraString • extraInt • packageName
  25. What can we do with that? Parse data1 Protocol Buffers

    using kind to define its format https://github.com/JetBrains/kotlin/blob/master/core/metadata/src/metadata.proto
  26. What tools do we have? • Hands • Use kotlin-metadata

    by Eugenio Marletti
 https://github.com/Takhion/kotlin-metadata
  27. What tools do we have? • Hands • Use kotlin-metadata

    by Eugenio Marletti
 https://github.com/Takhion/kotlin-metadata • Or…

  28. What tools do we have? • Hands • Use kotlin-metadata

    by Eugenio Marletti
 https://github.com/Takhion/kotlin-metadata • Or… kotlinx-metadata by JetBrains
 https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm
  29. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); }
  30. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); R visitType(TypeElement e, P p); }
  31. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); R visitType(TypeElement e, P p); R visitVariable(VariableElement e, P p); }
  32. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); R visitType(TypeElement e, P p); R visitVariable(VariableElement e, P p); R visitExecutable(ExecutableElement e, P p); }
  33. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); R visitType(TypeElement e, P p); R visitVariable(VariableElement e, P p); R visitExecutable(ExecutableElement e, P p); R visitTypeParameter(TypeParameterElement e, P p); }
  34. Visitors API public interface ElementVisitor<R, P> { R visit(Element e,

    P p); R visitPackage(PackageElement e, P p); R visitType(TypeElement e, P p); R visitVariable(VariableElement e, P p); R visitExecutable(ExecutableElement e, P p); R visitTypeParameter(TypeParameterElement e, P p); R visitUnknown(Element e, P p); }
  35. Visitors API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { return false } }
  36. Visitors API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(Builder::class.java)) { }. return false } }
  37. Visitors API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(Builder::class.java)) { element.accept(visitor, null) }.. return false } }
  38. Visitors API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(Builder::class.java)) { val visitor = object : ElementScanner7<Unit, Unit>() { override fun visitVariable(e: VariableElement, p: Unit?) { handleVariable(e) } } element.accept(visitor, null) }. return false } }
  39. Visitors API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(Builder::class.java)) { val visitor = object : ElementScanner7<Unit, Unit>() { override fun visitVariable(e: VariableElement, p: Unit?) { handleVariable(e) } } element.accept(visitor, null) }. return false } }
  40. DOM-like API public interface Element { Element getEnclosingElement(); List<? extends

    Element> getEnclosedElements(); ElementKind getKind(); }
  41. DOM-like API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(BindView::class.java)) { } return false } }
  42. DOM-like API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(BindView::class.java)) { } return false } }
  43. DOM-like API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(BindView::class.java)) { if (element.kind == ElementKind.FIELD) { handleVariable(element as VariableElement) }. } return false } }
  44. DOM-like API class KotlinProcessor : AbstractProcessor() { override fun process(

    annotations: Set<TypeElement>, roundEnv: RoundEnvironment ): Boolean { for (element in roundEnv.getElementsAnnotatedWith(BindView::class.java)) { if (element.kind == ElementKind.FIELD) { handleVariable(element as VariableElement) }. } return false } }
  45. Visitors API abstract class KmFunctionVisitor abstract class KmPropertyVisitor abstract class

    KmPackageVisitor abstract class KmClassVisitor abstract class KmConstructorVisitor abstract class KmValueParameterVisitor abstract class KmLambdaVisitor abstract class KmTypeVisitor abstract class KmTypeAliasVisitor abstract class KmTypeParameterVisitor
  46. Visitors API abstract class KmClassVisitor { open fun visitTypeParameter( flags:

    Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  47. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  48. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  49. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  50. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  51. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  52. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  53. Visitors API class Type<out E : UpperBoundType> abstract class KmClassVisitor

    { open fun visitTypeParameter( flags: Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  54. Visitors API abstract class KmClassVisitor { open fun visitTypeParameter( flags:

    Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }.
  55. Visitors API abstract class KmClassVisitor { open fun visitTypeParameter( flags:

    Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }. interface ElementVisitor<R, P> { R visitTypeParameter( TypeParameterElement e, P p ); }
  56. Visitors API abstract class KmClassVisitor { open fun visitTypeParameter( flags:

    Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }. interface ElementVisitor<R, P> { R visitTypeParameter( TypeParameterElement e, P p ); }
  57. Visitors API abstract class KmClassVisitor { open fun visitTypeParameter( flags:

    Flags, name: String, id: Int, variance: KmVariance ): KmTypeParameterVisitor? }. interface ElementVisitor<R, P> { R visitTypeParameter( TypeParameterElement e, P p ); }
  58. val annotation = GsonAdapter::class.java for (element in roundEnv.getElementsAnnotatedWith(annotation)) { val

    input = getInputFrom(element) ?: continue if (!input.generateAndWrite()) return true }
  59. val annotation = GsonAdapter::class.java for (element in roundEnv.getElementsAnnotatedWith(annotation)) { val

    input = getInputFrom(element) ?: continue if (!input.generateAndWrite()) return true }
  60. fun getMetadata(element: Element): KotlinClassMetadata? { val annotation = element.getAnnotation(Metadata::class.java) val

    header = annotation.run { KotlinClassHeader(k, mv, bv, d1, d2, xs, pn, xi) }. return KotlinClassMetadata.read(header) }.
  61. fun getMetadata(element: Element): KotlinClassMetadata? { val annotation = element.getAnnotation(Metadata::class.java) val

    header = annotation.run { KotlinClassHeader(k, mv, bv, d1, d2, xs, pn, xi) }. return KotlinClassMetadata.read(header) }.
  62. fun getMetadata(element: Element): KotlinClassMetadata? { val annotation = element.getAnnotation(Metadata::class.java) val

    header = annotation.run { KotlinClassHeader(k, mv, bv, d1, d2, xs, pn, xi) }. return KotlinClassMetadata.read(header) }.
  63. fun getMetadata(element: Element): KotlinClassMetadata? { val annotation = element.getAnnotation(Metadata::class.java) val

    header = annotation.run { KotlinClassHeader(k, mv, bv, d1, d2, xs, pn, xi) }. return KotlinClassMetadata.read(header) }.
  64. sealed class KotlinClassMetadata { class Class : KotlinClassMetadata class FileFacade

    : KotlinClassMetadata class SyntheticClass : KotlinClassMetadata class MultiFileClassFacade : KotlinClassMetadata class MultiFileClassPart : KotlinClassMetadata class Unknown : KotlinClassMetadata }
  65. sealed class KotlinClassMetadata { class Class : KotlinClassMetadata class FileFacade

    : KotlinClassMetadata class SyntheticClass : KotlinClassMetadata class MultiFileClassFacade : KotlinClassMetadata class MultiFileClassPart : KotlinClassMetadata class Unknown : KotlinClassMetadata }
  66. val metadata = getMetadata(element) if (metadata !is KotlinClassMetadata.Class) { errorMustBeKotlinClass(element)

    return null }. metadata.accept(object : KmClassVisitor() { override fun visit(flags: Flags, name: ClassName) { } })
  67. val metadata = getMetadata(element) if (metadata !is KotlinClassMetadata.Class) { errorMustBeKotlinClass(element)

    return null }. metadata.accept(object : KmClassVisitor() { override fun visit(flags: Flags, name: ClassName) { pkg = name.substringBeforeLast('/').fqName() className = name.substringAfterLast('/') fqClassName = name.fqName() } })
  68. return null }. metadata.accept(object : KmClassVisitor() { override fun visit(flags:

    Flags, name: ClassName) { pkg = name.substringBeforeLast('/').fqName() className = name.substringAfterLast('/') fqClassName = name.fqName() } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { } })
  69. return null }. metadata.accept(object : KmClassVisitor() { override fun visit(flags:

    Flags, name: ClassName) { pkg = name.substringBeforeLast('/').fqName() className = name.substringAfterLast('/') fqClassName = name.fqName() } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return null } })
  70. val Flags.isNullableType: Boolean get() = Flag.Type.IS_NULLABLE(this) val Flags.isDataClass: Boolean get()

    = Flag.Class.IS_DATA(this) val Flags.isPrimaryConstructor: Boolean get() = Flag.Constructor.IS_PRIMARY(this) val Flags.hasDefaultValue: Boolean get() = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(this)
  71. return null }. metadata.accept(object : KmClassVisitor() { override fun visit(flags:

    Flags, name: ClassName) { pkg = name.substringBeforeLast('/').fqName() className = name.substringAfterLast('/') fqClassName = name.fqName() } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return null } })
  72. pkg = name.substringBeforeLast('/').fqName() className = name.substringAfterLast('/') fqClassName = name.fqName() }

    override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return null return object : KmConstructorVisitor() { override fun visitValueParameter( flags: Flags, name: String ): KmValueParameterVisitor? { }. }. } })
  73. } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return

    null return object : KmConstructorVisitor() { override fun visitValueParameter( flags: Flags, name: String ): KmValueParameterVisitor? { return object : KmValueParameterVisitor() { override fun visitType(flags: Flags): KmTypeVisitor? { return ... } } }. }. } })
  74. return object :.KmTypeVisitor().{. override fun visitAbbreviatedType(flags: Flags): KmTypeVisitor? { }

    override fun visitArgument( flags: Flags, variance: KmVariance ): KmTypeVisitor? { } }.
  75. class KmTypeInfoVisitor( private val flags: Flags, private val onVisitEnd: (KmTypeInfoVisitor)

    -> Unit ) : KmTypeVisitor() {. lateinit var fqName: String var isNullable: Boolean = false private val typeArgs = arrayListOf<String>() }.
  76. class KmTypeInfoVisitor( private val flags: Flags, private val onVisitEnd: (KmTypeInfoVisitor)

    -> Unit ) : KmTypeVisitor() { lateinit var fqName: String var isNullable: Boolean = false private val typeArgs = arrayListOf<String>() override fun visitEnd() { } }.
  77. private val onVisitEnd: (KmTypeInfoVisitor) -> Unit ) : KmTypeVisitor() {

    lateinit var fqName: String var isNullable: Boolean = false private val typeArgs = arrayListOf<String>() override fun visitEnd() { if (typeArgs.isNotEmpty()) { fqName += typeArgs.joinToString(prefix = "<", postfix = ">") typeArgs.clear() }, isNullable = flags.isNullableType onVisitEnd(this) } }.
  78. fqName += typeArgs.joinToString(prefix = "<", postfix = ">") typeArgs.clear() },

    isNullable = flags.isNullableType onVisitEnd(this) } override fun visitClass(name: ClassName) { fqName = name.fqName() }. }.
  79. onVisitEnd(this) } override fun visitClass(name: ClassName) { fqName = name.fqName()

    }. override fun visitAbbreviatedType(flags: Flags): KmTypeVisitor? { return KmTypeInfoVisitor(flags, typeResolver) { fqName = it.fqName typeArgs.clear() }. }, }.
  80. onVisitEnd(this) } override fun visitClass(name: ClassName) { fqName = name.fqName()

    }. override fun visitAbbreviatedType(flags: Flags): KmTypeVisitor? { return KmTypeInfoVisitor(flags, typeResolver) { fqName = it.fqName typeArgs.clear() }. }, }.
  81. fqName = name.fqName() }. override fun visitAbbreviatedType(flags: Flags): KmTypeVisitor? {

    return KmTypeInfoVisitor(flags, typeResolver) { fqName = it.fqName typeArgs.clear() }. },’ override fun visitTypeAlias(name: ClassName) { fqName = name.fqName() }. }.
  82. return KmTypeInfoVisitor(flags, typeResolver) { fqName = it.fqName typeArgs.clear() }. },

    override fun visitTypeAlias(name: ClassName) { fqName = name.fqName() }. override fun visitTypeParameter(id: Int) { fqName = typeResolver(id) ?: throw error("Missing param: id=$id") }, }.
  83. override fun visitTypeAlias(name: ClassName) { fqName = name.fqName() }. override

    fun visitTypeParameter(id: Int) { fqName = typeResolver(id) ?: throw error("Missing param: id=$id") }, override fun visitArgument( flags: Flags, variance: KmVariance ): KmTypeVisitor? { return KmTypeInfoVisitor(flags, typeResolver) { typeArgs += it.fqName }, }, }.
  84. override fun visitTypeAlias(name: ClassName) { fqName = name.fqName() }. override

    fun visitTypeParameter(id: Int) { fqName = typeResolver(id) ?: throw error("Missing param: id=$id") }, override fun visitArgument( flags: Flags, variance: KmVariance ): KmTypeVisitor? { return KmTypeInfoVisitor(flags, typeResolver) { typeArgs += it.fqName },. }, }.
  85. } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return

    null return object : KmConstructorVisitor() { override fun visitValueParameter( flags: Flags, name: String ): KmValueParameterVisitor? { return object : KmValueParameterVisitor() { override fun visitType(flags: Flags): KmTypeVisitor? { return ... }. }. }. }. } })
  86. } override fun visitConstructor(flags: Flags): KmConstructorVisitor? { if (!flags.isPrimaryConstructor) return

    null return object : KmConstructorVisitor() { override fun visitValueParameter( flags: Flags, name: String ): KmValueParameterVisitor? { return object : KmValueParameterVisitor() { override fun visitType(flags: Flags): KmTypeVisitor? { return KmTypeInfoVisitor(flags, typeArgsRegistry::get) { parameters += Parameter(name, it.fqName, it.isNullable) } }. }. }. }. } })
  87. Worth noting • Metadata has only partial info • Separate

    code model from generation • Both Kotlin & Java can be supported in one place
  88. Worth noting • Metadata has only partial info • Separate

    code model from generation • Both Kotlin & Java can be supported in one place • Multiplatform support will follow
  89. Other applications? • Annotation processing with codegen • Bytecode weaving

    • Code validation • Generating headers for interop with other languages: TypeScript, C/C++
  90. Links • Metadata ProtoBuffer spec: github.com/JetBrains/kotlin/blob/master/core/metadata/src/metadata.proto • kotlin-metadata by Eugenio

    Marletti: github.com/Takhion/kotlin-metadata • kotlinx-metadata by JetBrains: github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm • Value-based API for kotlinx-metadata: youtrack.jetbrains.com/issue/KT-26602 • Gson TypeAdapters codegen: github.com/colriot/GsonKotgen • KotlinPoet: github.com/square/kotlinpoet • Annotation Processing in a Kotlin World by Zac Sweers: youtu.be/_yaaCtWF8aE?t=18968 • Annotation Processing Boilerplate Destruction by Jake Wharton: youtu.be/dOcs-NKK-RA • Multiple KAPT rounds demo: github.com/Takhion/generate-kotlin-multiple-rounds