codegen-java: Support not annotating constructor parameters (#792)

Motivation:
Spring Boot configuration classes neither require nor benefit from annotating constructor parameters with their name.
The same is true for pkl-config-java configuration classes compiled with `-parameter`.

Changes:
- Change CLI parameter `--params-annotation` to accept a `none` value.
  This is recommended in https://clig.dev/#arguments-and-flags and is how the `-F` parameter of the `ssh` command works.
- Change `paramsAnnotation` property in Gradle plugin, CliJavaCodeGeneratorOptions, and JavaCodegenOptions as follows:
  - Change meaning of `null` from "generate org.pkl.java.config.mapper.Named annotations" to "do not generate annotations".
    This is a breaking change (only) affecting users who explicitly set the property to `null` instead of omitting it.
  - Change property default from `null` to:
    `null` if `generateSpringBootConfig` is `true` and `org.pkl.java.config.mapper.Named` otherwise
- add tests
- update docs of this and other codegen options

Result:
Generated code does not contain unnecessary annotations.
This commit is contained in:
odenix
2024-12-13 14:29:18 -08:00
committed by GitHub
parent 70aaa6322e
commit 01bf844a96
13 changed files with 185 additions and 114 deletions

View File

@@ -35,25 +35,35 @@ data class CliJavaCodeGeneratorOptions(
*/
val generateGetters: Boolean = false,
/** Whether to generate Javadoc based on doc comments for Pkl modules, classes, and properties. */
/** Whether to preserve Pkl doc comments by generating corresponding Javadoc comments. */
val generateJavadoc: Boolean = false,
/** Whether to generate config classes for use with Spring Boot. */
val generateSpringBootConfig: Boolean = false,
/**
* Fully qualified name of the annotation to use on constructor parameters. If this options is not
* set, [org.pkl.config.java.mapper.Named] will be used.
* Fully qualified name of the annotation type to use for annotating constructor parameters with
* their name.
*
* The specified annotation type must have a `value` parameter of type [java.lang.String] or the
* generated code may not compile.
*
* If set to `null`, constructor parameters are not annotated. The default value is `null` if
* [generateSpringBootConfig] is `true` and `"org.pkl.config.java.mapper.Named"` otherwise.
*/
val paramsAnnotation: String? = null,
val paramsAnnotation: String? =
if (generateSpringBootConfig) null else "org.pkl.config.java.mapper.Named",
/**
* Fully qualified name of the annotation to use on non-null properties. If this option is not
* set, [org.pkl.config.java.mapper.NonNull] will be used.
* Fully qualified name of the annotation type to use for annotating non-null types.
*
* The specified annotation type must have a [java.lang.annotation.Target] of
* [java.lang.annotation.ElementType.TYPE_USE] or the generated code may not compile. If set to
* `null`, [org.pkl.config.java.mapper.NonNull] will be used.
*/
val nonNullAnnotation: String? = null,
/** Whether to make generated classes implement [java.io.Serializable] */
/** Whether to generate classes that implement [java.io.Serializable]. */
val implementSerializable: Boolean = false,
/**

View File

@@ -54,25 +54,35 @@ data class JavaCodeGeneratorOptions(
*/
val generateGetters: Boolean = false,
/** Whether to generate Javadoc based on doc comments for Pkl modules, classes, and properties. */
/** Whether to preserve Pkl doc comments by generating corresponding Javadoc comments. */
val generateJavadoc: Boolean = false,
/** Whether to generate config classes for use with Spring Boot. */
val generateSpringBootConfig: Boolean = false,
/**
* Fully qualified name of the annotation to use on constructor parameters. If this options is not
* set, [org.pkl.config.java.mapper.Named] will be used.
* Fully qualified name of the annotation type to use for annotating constructor parameters with
* their name.
*
* The specified annotation type must have a `value` parameter of type [java.lang.String] or the
* generated code may not compile.
*
* If set to `null`, constructor parameters are not annotated. The default value is `null` if
* [generateSpringBootConfig] is `true` and `"org.pkl.config.java.mapper.Named"` otherwise.
*/
val paramsAnnotation: String? = null,
val paramsAnnotation: String? =
if (generateSpringBootConfig) null else "org.pkl.config.java.mapper.Named",
/**
* Fully qualified name of the annotation to use on non-null properties. If this option is not
* set, [org.pkl.config.java.mapper.NonNull] will be used.
* Fully qualified name of the annotation type to use for annotating non-null types.
*
* The specified annotation type must have a [java.lang.annotation.Target] of
* [java.lang.annotation.ElementType.TYPE_USE] or the generated code may not compile. If set to
* `null`, [org.pkl.config.java.mapper.NonNull] will be used.
*/
val nonNullAnnotation: String? = null,
/** Whether to make generated classes implement [java.io.Serializable] */
/** Whether to generate classes that implement [java.io.Serializable]. */
val implementSerializable: Boolean = false,
/**
@@ -233,15 +243,15 @@ class JavaCodeGenerator(
propJavaName: String,
property: PClass.Property
) {
builder.addParameter(
ParameterSpec.builder(property.type.toJavaPoetName(), propJavaName)
.addAnnotation(
AnnotationSpec.builder(namedAnnotationName)
.addMember("value", "\$S", property.simpleName)
.build()
)
.build()
)
val paramBuilder = ParameterSpec.builder(property.type.toJavaPoetName(), propJavaName)
if (paramsAnnotationName != null) {
paramBuilder.addAnnotation(
AnnotationSpec.builder(paramsAnnotationName)
.addMember("value", "\$S", property.simpleName)
.build()
)
}
builder.addParameter(paramBuilder.build())
}
fun generateConstructor(isInstantiable: Boolean): MethodSpec {
@@ -671,12 +681,8 @@ class JavaCodeGenerator(
return builder
}
private val namedAnnotationName =
if (codegenOptions.paramsAnnotation != null) {
toClassName(codegenOptions.paramsAnnotation)
} else {
ClassName.get("org.pkl.config.java.mapper", "Named")
}
private val paramsAnnotationName: ClassName? =
codegenOptions.paramsAnnotation?.let { toClassName(it) }
private fun appendPropertyMethod() =
MethodSpec.methodBuilder("appendProperty")

View File

@@ -17,10 +17,7 @@
package org.pkl.codegen.java
import com.github.ajalt.clikt.parameters.options.associate
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
@@ -71,33 +68,37 @@ class PklJavaCodegenCommand :
private val generateJavadoc: Boolean by
option(
names = arrayOf("--generate-javadoc"),
help =
"Whether to generate Javadoc based on doc comments " +
"for Pkl modules, classes, and properties."
help = "Whether to preserve Pkl doc comments by generating corresponding Javadoc comments."
)
.flag()
private val generateSpringboot: Boolean by
private val generateSpringBoot: Boolean by
option(
names = arrayOf("--generate-spring-boot"),
help = "Whether to generate config classes for use with Spring boot."
help = "Whether to generate config classes for use with Spring Boot."
)
.flag()
private val paramsAnnotation: String? by
private val paramsAnnotation: String by
option(
names = arrayOf("--params-annotation"),
help = "Fully qualified name of the annotation to use on constructor parameters."
)
names = arrayOf("--params-annotation"),
help =
"Fully qualified name of the annotation type to use for annotating constructor parameters with their name."
)
.defaultLazy(
"`none` if `--generate-spring-boot` is set, `org.pkl.config.java.mapper.Named` otherwise"
) {
if (generateSpringBoot) "none" else "org.pkl.config.java.mapper.Named"
}
private val nonNullAnnotation: String? by
option(
names = arrayOf("--non-null-annotation"),
help =
"""
Fully qualified named of the annotation class to use for non-null types.
This annotation is required to have `java.lang.annotation.ElementType.TYPE_USE` as a `@Target`
or it may generate code that does not compile.
Fully qualified name of the annotation type to use for annotating non-null types.
The specified annotation type must be annotated with `@java.lang.annotation.Target(ElementType.TYPE_USE)`
or the generated code may not compile.
"""
.trimIndent()
)
@@ -105,7 +106,7 @@ class PklJavaCodegenCommand :
private val implementSerializable: Boolean by
option(
names = arrayOf("--implement-serializable"),
help = "Whether to make generated classes implement java.io.Serializable."
help = "Whether to generate classes that implement java.io.Serializable."
)
.flag()
@@ -117,7 +118,7 @@ class PklJavaCodegenCommand :
"""
Replace a prefix in the names of the generated Java classes (repeatable).
By default, the names of generated classes are derived from the Pkl module names.
With this option, you can override the modify the default names, renaming entire
With this option, you can override or modify the default names, renaming entire
classes or just their packages.
"""
.trimIndent()
@@ -132,8 +133,8 @@ class PklJavaCodegenCommand :
indent = indent,
generateGetters = generateGetters,
generateJavadoc = generateJavadoc,
generateSpringBootConfig = generateSpringboot,
paramsAnnotation = paramsAnnotation,
generateSpringBootConfig = generateSpringBoot,
paramsAnnotation = if (paramsAnnotation == "none") null else paramsAnnotation,
nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable,
renames = renames

View File

@@ -104,26 +104,10 @@ class JavaCodeGeneratorTest {
private fun generateJavaCode(
pklCode: String,
generateGetters: Boolean = false,
generateJavadoc: Boolean = false,
generateSpringBootConfig: Boolean = false,
nonNullAnnotation: String? = null,
implementSerializable: Boolean = false,
renames: Map<String, String> = emptyMap()
options: JavaCodeGeneratorOptions = JavaCodeGeneratorOptions()
): JavaSourceCode {
val module = Evaluator.preconfigured().evaluateSchema(text(pklCode))
val generator =
JavaCodeGenerator(
module,
JavaCodeGeneratorOptions(
generateGetters = generateGetters,
generateJavadoc = generateJavadoc,
generateSpringBootConfig = generateSpringBootConfig,
nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable,
renames = renames
)
)
val generator = JavaCodeGenerator(module, options)
return JavaSourceCode(generator.javaFile)
}
}
@@ -235,7 +219,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateJavadoc = true
JavaCodeGeneratorOptions(generateJavadoc = true)
)
assertThat(javaCode)
.contains(
@@ -274,8 +258,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true,
generateJavadoc = true
JavaCodeGeneratorOptions(generateGetters = true, generateJavadoc = true)
)
assertThat(javaCode)
.contains(
@@ -322,7 +305,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateJavadoc = true
JavaCodeGeneratorOptions(generateJavadoc = true)
)
assertThat(javaCode)
.contains(
@@ -349,7 +332,7 @@ class JavaCodeGeneratorTest {
propertyInDeprecatedModuleClass : Int = 42
"""
.trimIndent(),
generateJavadoc = generateJavadoc
JavaCodeGeneratorOptions(generateJavadoc = generateJavadoc)
)
assertThat(javaCode)
@@ -389,7 +372,7 @@ class JavaCodeGeneratorTest {
"""
.trimIndent(),
// no message, so no Javadoc, regardless of flag
generateJavadoc = generateJavadoc
JavaCodeGeneratorOptions(generateJavadoc = generateJavadoc)
)
assertThat(javaCode)
@@ -424,7 +407,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true
JavaCodeGeneratorOptions(generateGetters = true)
)
assertThat(javaCode)
@@ -513,7 +496,7 @@ class JavaCodeGeneratorTest {
@Deprecated { message = "property is deprecated" }
deprecatedProperty: Int
""",
generateJavadoc = true
JavaCodeGeneratorOptions(generateJavadoc = true)
)
assertThat(javaCode)
@@ -735,7 +718,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true
JavaCodeGeneratorOptions(generateGetters = true)
)
assertThat(javaCode)
@@ -889,7 +872,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true
JavaCodeGeneratorOptions(generateGetters = true)
)
assertThat(javaCode).compilesSuccessfully().isEqualToResourceFile("GenerateGetters.jva")
@@ -1002,7 +985,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateJavadoc = true
JavaCodeGeneratorOptions(generateJavadoc = true)
)
assertThat(javaCode).compilesSuccessfully().isEqualToResourceFile("Javadoc.jva")
@@ -1026,8 +1009,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true,
generateJavadoc = true
JavaCodeGeneratorOptions(generateGetters = true, generateJavadoc = true)
)
assertThat(javaCode)
@@ -1169,7 +1151,7 @@ class JavaCodeGeneratorTest {
foo: String
"""
.trimIndent(),
nonNullAnnotation = "com.example.Annotations\$NonNull"
JavaCodeGeneratorOptions(nonNullAnnotation = "com.example.Annotations\$NonNull")
)
assertThat(javaCode)
@@ -1463,6 +1445,40 @@ class JavaCodeGeneratorTest {
.contains("public final @NonNull String v6;")
}
@Test
fun `custom constructor parameter annotation`() {
val javaCode =
generateJavaCode(
"""
module my.mod
name: String
"""
.trimIndent(),
JavaCodeGeneratorOptions(paramsAnnotation = "org.project.MyAnnotation")
)
assertThat(javaCode)
.contains("import org.project.MyAnnotation;")
.contains("public Mod(@MyAnnotation(\"name\") @NonNull String name)")
}
@Test
fun `no constructor parameter annotation`() {
val javaCode =
generateJavaCode(
"""
module my.mod
name: String
"""
.trimIndent(),
JavaCodeGeneratorOptions(paramsAnnotation = null)
)
assertThat(javaCode).contains("public Mod(@NonNull String name)")
}
@Test
fun `spring boot config`() {
val javaCode =
@@ -1478,7 +1494,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateSpringBootConfig = true
JavaCodeGeneratorOptions(generateSpringBootConfig = true)
)
assertThat(javaCode)
@@ -1511,6 +1527,7 @@ class JavaCodeGeneratorTest {
.trimMargin()
)
.doesNotContain("@ConstructorBinding")
.doesNotContain("@Named")
// not worthwhile to add spring & spring boot dependency just so that this test can compile
// their annotations
@@ -1765,7 +1782,7 @@ class JavaCodeGeneratorTest {
typealias Direction = "north"|"east"|"south"|"west"
"""
.trimIndent(),
implementSerializable = true
JavaCodeGeneratorOptions(implementSerializable = true)
)
assertThat(javaCode)
@@ -1846,7 +1863,7 @@ class JavaCodeGeneratorTest {
abstract class Foo { str: String }
"""
.trimIndent(),
implementSerializable = true
JavaCodeGeneratorOptions(implementSerializable = true)
)
assertThat(javaCode).doesNotContain("Serializable")
@@ -1857,7 +1874,7 @@ class JavaCodeGeneratorTest {
module my.mod
"""
.trimIndent(),
implementSerializable = true
JavaCodeGeneratorOptions(implementSerializable = true)
)
assertThat(javaCode).doesNotContain("Serializable")
@@ -1874,7 +1891,7 @@ class JavaCodeGeneratorTest {
class Address { city: String }
"""
.trimIndent(),
implementSerializable = true
JavaCodeGeneratorOptions(implementSerializable = true)
)
assertThat(javaCode)
@@ -1960,7 +1977,7 @@ class JavaCodeGeneratorTest {
}
"""
.trimIndent(),
generateGetters = true
JavaCodeGeneratorOptions(generateGetters = true)
)
assertThat(javaCode)