diff --git a/build.gradle.kts b/build.gradle.kts index ff970cf..613573b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,2 +1,2 @@ group = "io.gitlab.jfronny" -version = "1.1-SNAPSHOT" +version = "1.2-SNAPSHOT" diff --git a/convention/build.gradle.kts b/convention/build.gradle.kts index 4e9d7d6..4dd8f94 100644 --- a/convention/build.gradle.kts +++ b/convention/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation("org.eclipse.jgit:org.eclipse.jgit:[6.0, 7.0)") + implementation("com.squareup:javapoet:1.13.0") } \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt index 248d555..2a62dc0 100644 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt @@ -1,21 +1,154 @@ +@file:Suppress("unused", "HasPlatformType") + package io.gitlab.jfronny.scripts -import io.gitlab.jfronny.scripts.codegen.ContentGenerator +import com.squareup.javapoet.* import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.tasks.SourceSet import org.gradle.kotlin.dsl.extra +import java.lang.reflect.Type +import java.util.function.Supplier +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Types val Project.codegenDir get() = buildDir.resolve("generated/sources/jfCodegen") fun SourceSet.generate(project: Project, generate: Action) { val generators = project.extra["codeGenerators"] as LinkedHashMap val exists = generators.containsKey(name) - val generator = ContentGenerator() - generate.execute(generator) + val generator = ContentGenerator().run(generate) generators[name] = if (exists) generators[name]!!.merge(generator.finalize()) else generator.finalize() if (!exists) { java.srcDir(project.codegenDir.resolve("java/$name")) resources.srcDir(project.codegenDir.resolve("resources/$name")) } -} \ No newline at end of file +} + +class ContentGenerator { + private var finalized = false + + data class Generated(val classes: Map, val resources: Map) { + fun merge(other: Generated): Generated = Generated(classes + other.classes, resources + other.resources) + } + + private data class ImmutableMap(private val inner: Map): Map by inner + + fun finalize(): Generated { + val fin = Generated(ImmutableMap(classes), ImmutableMap(resources)) + finalized = true + return fin + } + + private fun ensureMutable(action: () -> T): T { + if (finalized) throw IllegalAccessException("Attempted to access CodeGenerator after it was finalized") + return action() + } + + private val classes: MutableMap = LinkedHashMap() + private val resources: MutableMap = LinkedHashMap() + + fun `class`(`package`: String, name: String, generator: Action) = ensureMutable { + check(`package`, "package", packagePattern) + check(name, "class name", classNamePattern) + val builder = TypeSpec.classBuilder(ClassName.get(`package`, name)) + generator.execute(builder) + val javaFile: JavaFile = JavaFile.builder(`package`, builder.build()) + .skipJavaLangImports(true) + .indent(" ") + .addFileComment("Automatically generated through JfCodegen, do not edit!") + .build() + classes["${`package`.replace('.', '/')}/$name.java"] = javaFile.toString() + } + + fun text(path: String, generator: Action) = ensureMutable { + check(path, "path", pathPattern) + val builder = StringBuilder() + generator.execute(builder) + resources[path] = builder.toString().toByteArray() + } + + fun blob(path: String, generator: Supplier) = ensureMutable { + check(path, "path", pathPattern) + resources[path] = generator.get() + } + + companion object { + @JvmStatic private val packagePattern = Regex("(?:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*)") + @JvmStatic private val classNamePattern = Regex("(?:[A-Z][A-Za-z0-9_\$]*)") + @JvmStatic private val pathPattern = Regex("(?:([a-z]+/)*[a-zA-Z][a-zA-Z0-9_.]*)") + + @JvmStatic + private fun check(name: String, type: String, pattern: Regex) { + if (!pattern.matches(name)) throw IllegalArgumentException("$type \"$name\" is not a valid $type") + } + } +} + +// Extensions for TypeSpec.Builder +fun TypeSpec.Builder.javadoc(`import`: String) = addJavadoc(import) +fun TypeSpec.Builder.javadoc(generate: Action) = addJavadoc(CodeBlock.builder().run(generate).build()) +fun TypeSpec.Builder.annotation(annotation: Class<*>, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun TypeSpec.Builder.annotation(annotation: ClassName, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun TypeSpec.Builder.modifiers(vararg modifiers: Modifier) = addModifiers(*modifiers) +fun TypeSpec.Builder.typeVariable(typeVariable: TypeVariableName) = addTypeVariable(typeVariable) +fun TypeSpec.Builder.superInterface(superInterface: TypeName) = addSuperinterface(superInterface) +fun TypeSpec.Builder.superInterface(superInterface: Type) = addSuperinterface(superInterface) +fun TypeSpec.Builder.superInterface(superInterface: Type, avoidNestedTypeNameClashes: Boolean) = addSuperinterface(superInterface, avoidNestedTypeNameClashes) +fun TypeSpec.Builder.superInterface(superInterface: TypeMirror) = addSuperinterface(superInterface) +fun TypeSpec.Builder.superInterface(superInterface: TypeMirror, avoidNestedTypeNameClashes: Boolean) = addSuperinterface(superInterface, avoidNestedTypeNameClashes) +fun TypeSpec.Builder.enumConstant(name: String) = addEnumConstant(name) +fun TypeSpec.Builder.enumConstant(name: String, typeSpec: TypeSpec) = addEnumConstant(name, typeSpec) +fun TypeSpec.Builder.field(type: Type, name: String, vararg modifiers: Modifier, generate: Action? = null) = addField(FieldSpec.builder(type, name, *modifiers).run(generate).build()) +fun TypeSpec.Builder.field(type: TypeName, name: String, vararg modifiers: Modifier, generate: Action? = null) = addField(FieldSpec.builder(type, name, *modifiers).run(generate).build()) +fun TypeSpec.Builder.field(name: String, value: Boolean, vararg modifiers: Modifier) = field(Boolean::class.java, name, *modifiers) { initializer("$value") } +fun TypeSpec.Builder.field(name: String, value: Short, vararg modifiers: Modifier) = field(Short::class.java, name, *modifiers) { initializer("$value") } +fun TypeSpec.Builder.field(name: String, value: Int, vararg modifiers: Modifier) = field(Int::class.java, name, *modifiers) { initializer("$value") } +fun TypeSpec.Builder.field(name: String, value: Long, vararg modifiers: Modifier) = field(Long::class.java, name, *modifiers) { initializer("${value}L") } +fun TypeSpec.Builder.field(name: String, value: Float, vararg modifiers: Modifier) = field(Float::class.java, name, *modifiers) { initializer("${value}f") } +fun TypeSpec.Builder.field(name: String, value: Double, vararg modifiers: Modifier) = field(Double::class.java, name, *modifiers) { initializer("$value") } +fun TypeSpec.Builder.field(name: String, value: String, vararg modifiers: Modifier) = field(String::class.java, name, *modifiers) { initializer("\$S", value) } +fun TypeSpec.Builder.static(generate: Action) = addStaticBlock(CodeBlock.builder().run(generate).build()) +fun TypeSpec.Builder.initializer(generate: Action) = addInitializerBlock(CodeBlock.builder().run(generate).build()) +fun TypeSpec.Builder.method(name: String, generate: Action) = addMethod(MethodSpec.methodBuilder(name).run(generate).build()) +fun TypeSpec.Builder.override(element: ExecutableElement, generate: Action) = addMethod(MethodSpec.overriding(element).run(generate).build()) +fun TypeSpec.Builder.override(element: ExecutableElement, enclosing: DeclaredType, types: Types, generate: Action) = addMethod(MethodSpec.overriding(element, enclosing, types).run(generate).build()) +fun TypeSpec.Builder.constructor(generate: Action) = addMethod(MethodSpec.constructorBuilder().run(generate).build()) +fun TypeSpec.Builder.innerEnum(name: String, generate: Action) = addType(TypeSpec.enumBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerInterface(name: String, generate: Action) = addType(TypeSpec.interfaceBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerClass(name: String, generate: Action) = addType(TypeSpec.classBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerAnnotation(name: String, generate: Action) = addType(TypeSpec.annotationBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerEnum(name: ClassName, generate: Action) = addType(TypeSpec.enumBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerInterface(name: ClassName, generate: Action) = addType(TypeSpec.interfaceBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerClass(name: ClassName, generate: Action) = addType(TypeSpec.classBuilder(name).run(generate).build()) +fun TypeSpec.Builder.innerAnnotation(name: ClassName, generate: Action) = addType(TypeSpec.annotationBuilder(name).run(generate).build()) +fun TypeSpec.Builder.origin(origin: Element) = addOriginatingElement(origin) + +// Extensions for AnnotationSpec.Builder +fun AnnotationSpec.Builder.member(name: String, format: String, vararg args: Any) = addMember(name, format, args) +fun AnnotationSpec.Builder.member(name: String, generate: Action) = addMember(name, CodeBlock.builder().run(generate).build()) + +// Extensions for MethodSpec.Builder +fun MethodSpec.Builder.javadoc(`import`: String) = addJavadoc(import) +fun MethodSpec.Builder.javadoc(generate: Action) = addJavadoc(CodeBlock.builder().run(generate).build()) +fun MethodSpec.Builder.annotation(annotation: Class<*>, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun MethodSpec.Builder.annotation(annotation: ClassName, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun MethodSpec.Builder.modifiers(vararg modifiers: Modifier) = addModifiers(*modifiers) +fun MethodSpec.Builder.typeVariable(typeVariable: TypeVariableName) = addTypeVariable(typeVariable) +fun MethodSpec.Builder.parameter(type: Type, name: String, vararg modifiers: Modifier, generate: Action) = addParameter(ParameterSpec.builder(type, name, *modifiers).run(generate).build()) +fun MethodSpec.Builder.parameter(type: TypeName, name: String, vararg modifiers: Modifier, generate: Action) = addParameter(ParameterSpec.builder(type, name, *modifiers).run(generate).build()) +fun MethodSpec.Builder.exception(exception: Type) = addException(exception) +fun MethodSpec.Builder.exception(exception: TypeName) = addException(exception) +fun MethodSpec.Builder.code(generate: Action) = addCode(CodeBlock.builder().run(generate).build()) +fun MethodSpec.Builder.defaultValue(generate: Action) = defaultValue(CodeBlock.builder().run(generate).build()) + +// Extensions for ParameterSpec.Builder +fun ParameterSpec.Builder.javadoc(`import`: String) = addJavadoc(import) +fun ParameterSpec.Builder.javadoc(generate: Action) = addJavadoc(CodeBlock.builder().run(generate).build()) +fun ParameterSpec.Builder.annotation(annotation: Class<*>, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun ParameterSpec.Builder.annotation(annotation: ClassName, generate: Action? = null) = addAnnotation(AnnotationSpec.builder(annotation).run(generate).build()) +fun ParameterSpec.Builder.modifiers(vararg modifiers: Modifier) = addModifiers(*modifiers) diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt index 10a300a..c6fd7c4 100644 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/SharedProps.kt @@ -1,5 +1,6 @@ package io.gitlab.jfronny.scripts +import org.gradle.api.Action import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.Task @@ -26,4 +27,10 @@ val TaskContainer.deployRelease: Task get() = findByName("deployRelease") ?: reg fun Project.prop(name: String, default: String? = null): String = if (default == null || hasProperty(name)) property(name).toString() - else default \ No newline at end of file + else default + +// Utility to run actions on values +fun T.run(action: Action?): T { + action?.execute(this!!) + return this +} \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt deleted file mode 100644 index b1095b8..0000000 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt +++ /dev/null @@ -1,129 +0,0 @@ -package io.gitlab.jfronny.scripts.codegen - -class ClassGenerator(`package`: String, val name: String, val indent: String = " ") : Generator() { - override fun generateFinalized() = ensureBody { gen.appendLine("}").toString() } - private val gen: StringBuilder = StringBuilder("package $`package`;\n\n") - private var headerGenerated = false - private var extends: String? = null - private val implements: MutableList = ArrayList() - private var hadImport = false - - private fun ensureHeader(action: () -> T): T = ensureMutable { - if (headerGenerated) throw IllegalAccessException("Attempted to generate pre-header statement while in body") - action() - } - - private fun ensureBody(action: () -> T): T = ensureMutable { - if (!headerGenerated) { - headerGenerated = true - if (hadImport) gen.appendLine() - gen.append("public class $name") - if (extends != null) gen.append(" extends $extends") - if (implements.isNotEmpty()) gen.append(" implements ").append(implements.joinToString(", ") { it }) - gen.append(" {") - gen.appendLine() - } - action() - } - - fun rawHead(toWrite: String): Unit = ensureHeader { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) } - fun rawBody(toWrite: String): Unit = ensureBody { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) } - - fun import(`import`: String): Unit = ensureHeader { - check(import, "import", importPattern) - gen.appendLine("import $import;") - hadImport = true - } - - fun extends(extends: String): Unit = ensureHeader { - check(extends, "extended class", classPattern) - if (this.extends != null) throw IllegalAccessException("Attempted to extend multiple classes") - this.extends = extends - } - - fun implements(implements: String): Unit = ensureHeader { - check(implements, "implemented interface", classPattern) - this.implements.add(implements) - } - - fun fieldRaw(name: String, type: String, valueRaw: String, modifiers: String = defaultModifiers): Unit = ensureBody { - check(name, "field", classEntryPattern) - check(type, "field type", classPattern) - check(modifiers, "set of modifiers", modifierPattern) - gen.appendLine("$indent$modifiers $type $name = $valueRaw;") - } - - fun field(name: String, value: Boolean, modifiers: String = defaultModifiers) = fieldRaw(name, "boolean", value.toString(), modifiers) - fun field(name: String, value: Short, modifiers: String = defaultModifiers) = fieldRaw(name, "short", value.toString(), modifiers) - fun field(name: String, value: Int, modifiers: String = defaultModifiers) = fieldRaw(name, "int", value.toString(), modifiers) - fun field(name: String, value: Long, modifiers: String = defaultModifiers) = fieldRaw(name, "long", value.toString() + "L", modifiers) - fun field(name: String, value: Float, modifiers: String = defaultModifiers) = fieldRaw(name, "float", value.toString() + "f", modifiers) - fun field(name: String, value: Double, modifiers: String = defaultModifiers) = fieldRaw(name, "double", value.toString(), modifiers) - fun field(name: String, value: String, modifiers: String = defaultModifiers) = fieldRaw(name, "String", enquote(value), modifiers) - - fun procedure(name: String, argument: Map, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody { - check(name, "procedure", classEntryPattern) - if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure") - check(modifiers, "set of modifiers", modifierPattern) - ensureBrackets(body) - if (throws != null) check(throws, "thrown exceptions list", classListPattern) - gen.append(indent) - gen.append(modifiers) - gen.append(" void ") - gen.append(name) - gen.append("(") - gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" }) - gen.append(") ") - if (throws != null) gen.append(throws).append(' ') - gen.append('{') - gen.appendLine() - gen.appendLine(body.trimIndent().prependIndent("$indent ")) - gen.appendLine("$indent}") - } - - fun function(name: String, returnType: String, argument: Map, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody { - check(name, "procedure", classEntryPattern) - check(returnType, "return type", classPattern) - if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure") - check(modifiers, "set of modifiers", modifierPattern) - ensureBrackets(body) - if (throws != null) check(throws, "thrown exceptions list", classListPattern) - gen.append(indent) - gen.append(modifiers) - gen.append(" $returnType ") - gen.append(name) - gen.append("(") - gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" }) - gen.append(") ") - if (throws != null) gen.append(throws).append(' ') - gen.append('{') - gen.appendLine() - gen.appendLine(body.trimIndent().prependIndent("$indent ")) - gen.appendLine("$indent}") - } - - companion object { - @JvmStatic private val defaultModifiers = "public static" - @JvmStatic private fun enquote(value: String): String = "\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\"" - @JvmStatic private fun ensureBrackets(value: String): Unit { - var state = 0 // 0 -> outside of anything, 1 -> inside string, 2 -> inside block quote - var brackets = 0 - value.forEachIndexed { i, c -> - when (state) { - 0 -> { - if (c == '"') state = if (i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') 2 else 1 - if (c == '{') brackets++ - if (c == '}') brackets-- - } - 1 -> { - if (c == '"') state = 0 - } - 2 -> { - if (c == '"' && i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') state = 0 - } - } - } - if (brackets != 0) throw IllegalArgumentException("Unclosed brackets in method body") - } - } -} diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt deleted file mode 100644 index 8221025..0000000 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.gitlab.jfronny.scripts.codegen - -import org.gradle.api.Action -import java.util.function.Supplier - -class ContentGenerator : Generator() { - private val classes: MutableMap = LinkedHashMap() - private val resources: MutableMap = LinkedHashMap() - - override fun generateFinalized() = Generated(ImmutableMap(classes), ImmutableMap(resources)) - data class Generated(val classes: Map, val resources: Map) { - fun merge(other: Generated): Generated = Generated(classes + other.classes, resources + other.resources) - } - - fun `class`(`package`: String, name: String, generator: Action) = ensureMutable { - if (!packagePattern.matches(`package`)) throw IllegalArgumentException("package \"$`package`\" is not a valid package") - if (!classNamePattern.matches(name)) throw IllegalArgumentException("class name \"$name\" is not a valid class name") - val builder = ClassGenerator(`package`, name) - generator.execute(builder) - classes["${`package`.replace('.', '/')}/$name.java"] = builder.finalize() - } - - fun text(path: String, generator: Action) = ensureMutable { - if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path") - val builder = StringBuilder() - generator.execute(builder) - resources[path] = builder.toString().toByteArray() - } - - fun blob(path: String, generator: Supplier) = ensureMutable { - if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path") - resources[path] = generator.get() - } -} \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt deleted file mode 100644 index 7f4a998..0000000 --- a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.gitlab.jfronny.scripts.codegen - -abstract class Generator { - private var finalized = false - protected abstract fun generateFinalized(): T - - fun finalize(): T { - val fin = generateFinalized() - finalized = true - return fin - } - - protected fun ensureMutable(action: () -> T): T { - if (finalized) throw IllegalAccessException("Attempted to access CodeGenerator after it was finalized") - return action() - } - - protected data class ImmutableMap(private val inner: Map): Map by inner - - companion object { - @JvmStatic protected val primitivePattern = Regex("(?:byte|short|int|long|float|double|boolean|char)") - @JvmStatic protected val singleModifierPattern = Regex("(?:public|private|protected|static|final|abstract|transient|synchronized|volatile)") - @JvmStatic protected val modifierPattern = Regex("(?:$singleModifierPattern(?: $singleModifierPattern)*)") - - @JvmStatic protected val packagePattern = Regex("(?:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*)") - @JvmStatic protected val classNamePattern = Regex("(?:[A-Z][A-Za-z0-9_\$]*)") - @JvmStatic protected val pathPattern = Regex("(?:([a-z]+/)*[a-zA-Z][a-zA-Z0-9_.]*)") - @JvmStatic protected val classPattern = Regex("(?:(?:(?:$packagePattern\\.)?$classNamePattern)|$primitivePattern)") - @JvmStatic protected val importPattern = Regex("(?:$packagePattern\\.(?:\\*|$classNamePattern))") - @JvmStatic protected val classEntryPattern = Regex("(?:[a-zA-Z][a-zA-Z_\$0-9]*)") - @JvmStatic protected val classListPattern = Regex("(?:$classPattern(?:, $classPattern)*)") - @JvmStatic protected fun check(name: String, type: String, pattern: Regex): Unit { - if (!pattern.matches(name)) throw IllegalArgumentException("$type \"$name\" is not a valid $type") - } - } -} \ No newline at end of file diff --git a/convention/src/main/kotlin/jf.codegen.gradle.kts b/convention/src/main/kotlin/jf.codegen.gradle.kts index 56e001a..6da0094 100644 --- a/convention/src/main/kotlin/jf.codegen.gradle.kts +++ b/convention/src/main/kotlin/jf.codegen.gradle.kts @@ -1,16 +1,14 @@ -import gradle.kotlin.dsl.accessors._72efc76fad8c8cf3476d335fb6323bde.* -import io.gitlab.jfronny.scripts.codegen.ContentGenerator.Generated import io.gitlab.jfronny.scripts.* plugins { java } -extra["codeGenerators"] = LinkedHashMap() +extra["codeGenerators"] = LinkedHashMap() val jfCodegen by tasks.registering { doLast { - val generators = project.extra["codeGenerators"] as LinkedHashMap + val generators = project.extra["codeGenerators"] as LinkedHashMap if (codegenDir.exists()) codegenDir.deleteRecursively() generators.forEach { (name, generated) -> generated.classes.forEach { (filePath, content) ->