All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pkl.codegen.java.JavaCodeGenerator.kt Maven / Gradle / Ivy

Go to download

Fat Jar containing pkl-cli, pkl-codegen-java, pkl-codegen-kotlin, pkl-config-java, pkl-core, pkl-doc, and their shaded third-party dependencies.

There is a newer version: 0.27.1
Show newest version
/*
 * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.pkl.codegen.java

import org.pkl.thirdparty.javapoet.*
import java.io.StringWriter
import java.lang.Deprecated
import java.net.URI
import java.util.*
import java.util.regex.Pattern
import javax.lang.model.element.Modifier
import org.pkl.thirdparty.kotlin.AssertionError
import org.pkl.thirdparty.kotlin.Boolean
import org.pkl.thirdparty.kotlin.Int
import org.pkl.thirdparty.kotlin.Long
import org.pkl.thirdparty.kotlin.RuntimeException
import org.pkl.thirdparty.kotlin.String
import org.pkl.thirdparty.kotlin.Suppress
import org.pkl.thirdparty.kotlin.Unit
import org.pkl.thirdparty.kotlin.apply
import org.pkl.thirdparty.kotlin.let
import org.pkl.thirdparty.kotlin.takeIf
import org.pkl.thirdparty.kotlin.to
import org.pkl.commons.NameMapper
import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils

class JavaCodeGeneratorException(message: String) : RuntimeException(message)

data class JavaCodegenOptions(
  /** The characters to use for indenting generated Java code. */
  val indent: String = "  ",

  /**
   * Whether to generate public getter methods and protected final fields instead of public final
   * fields.
   */
  val generateGetters: Boolean = false,

  /** Whether to generate Javadoc based on doc comments for Pkl modules, classes, and properties. */
  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.
   */
  val paramsAnnotation: String? = null,

  /**
   * 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.
   */
  val nonNullAnnotation: String? = null,

  /** Whether to make generated classes implement [java.io.Serializable] */
  val implementSerializable: Boolean = false,

  /**
   * A mapping from Pkl module name prefixes to their replacements.
   *
   * Can be used when the class or package name in the generated source code should be different
   * from the corresponding name derived from the Pkl module declaration .
   */
  val renames: Map = emptyMap()
)

/** Entrypoint for the Java code generator API. */
class JavaCodeGenerator(
  private val schema: ModuleSchema,
  private val codegenOptions: JavaCodegenOptions
) {

  companion object {
    private val STRING = ClassName.get(String::class.java)
    private val DURATION = ClassName.get(Duration::class.java)
    private val DURATION_UNIT = ClassName.get(DurationUnit::class.java)
    private val DATA_SIZE = ClassName.get(DataSize::class.java)
    private val DATASIZE_UNIT = ClassName.get(DataSizeUnit::class.java)
    private val PAIR = ClassName.get(Pair::class.java)
    private val COLLECTION = ClassName.get(Collection::class.java)
    private val LIST = ClassName.get(List::class.java)
    private val SET = ClassName.get(Set::class.java)
    private val MAP = ClassName.get(Map::class.java)
    private val PMODULE = ClassName.get(PModule::class.java)
    private val PCLASS = ClassName.get(PClass::class.java)
    private val PATTERN = ClassName.get(Pattern::class.java)
    private val URI = ClassName.get(URI::class.java)
    private val VERSION = ClassName.get(Version::class.java)

    private const val PROPERTY_PREFIX: String = "org.pkl.config.java.mapper."

    private fun toClassName(fqn: String): ClassName {
      val index = fqn.lastIndexOf(".")
      if (index == -1) {
        throw JavaCodeGeneratorException(
          """
            Annotation `$fqn` is not a valid Java class.
            The name of the annotation should be the canonical Java name of the class, for example, `com.example.Foo`.
          """
            .trimIndent()
        )
      }
      val packageName = fqn.substring(0, index)
      val classParts = fqn.substring(index + 1).split('$')
      return if (classParts.size == 1) {
        ClassName.get(packageName, classParts.first())
      } else {
        ClassName.get(packageName, classParts.first(), *classParts.drop(1).toTypedArray())
      }
    }
  }

  val output: Map
    get() {
      return mapOf(javaFileName to javaFile, propertyFileName to propertiesFile)
    }

  private val propertyFileName: String
    get() =
      "resources/META-INF/org/pkl/config/java/mapper/classes/${IoUtils.encodePath(schema.moduleName)}.properties"

  private val propertiesFile: String
    get() {
      val props = Properties()
      props["$PROPERTY_PREFIX${schema.moduleClass.qualifiedName}"] =
        schema.moduleClass.toJavaPoetName().reflectionName()
      for (pClass in schema.classes.values) {
        props["$PROPERTY_PREFIX${pClass.qualifiedName}"] = pClass.toJavaPoetName().reflectionName()
      }
      return StringWriter()
        .apply { props.store(this, "Java mappings for Pkl module `${schema.moduleName}`") }
        .toString()
    }

  private val nonNullAnnotation: AnnotationSpec
    get() {
      val annotation = codegenOptions.nonNullAnnotation
      val className =
        if (annotation == null) {
          ClassName.get("org.pkl.config.java.mapper", "NonNull")
        } else {
          toClassName(annotation)
        }
      return AnnotationSpec.builder(className).build()
    }

  private val javaFileName: String
    get() {
      val (packageName, className) = nameMapper.map(schema.moduleName)
      val dirPath = packageName.replace('.', '/')
      return if (dirPath.isEmpty()) {
        "java/$className.java"
      } else {
        "java/$dirPath/$className.java"
      }
    }

  val javaFile: String
    get() {
      if (schema.moduleUri.scheme == "pkl") {
        throw JavaCodeGeneratorException(
          "Cannot generate Java code for a Pkl standard library module (`${schema.moduleUri}`)."
        )
      }

      val pModuleClass = schema.moduleClass
      val moduleClass = generateTypeSpec(pModuleClass, schema)

      for (pClass in schema.classes.values) {
        moduleClass.addType(generateTypeSpec(pClass, schema).build())
      }

      for (typeAlias in schema.typeAliases.values) {
        val stringLiterals = mutableSetOf()
        if (CodeGeneratorUtils.isRepresentableAsEnum(typeAlias.aliasedType, stringLiterals)) {
          moduleClass.addType(generateEnumTypeSpec(typeAlias, stringLiterals).build())
        }
      }
      // generate static append method for module classes w/o parent class; reuse in subclasses and
      // nested classes
      if (pModuleClass.superclass!!.info == PClassInfo.Module) {
        val modifier =
          if (pModuleClass.isOpen || pModuleClass.isAbstract) Modifier.PROTECTED
          else Modifier.PRIVATE
        moduleClass.addMethod(appendPropertyMethod().addModifiers(modifier).build())
      }

      val (packageName, _) = nameMapper.map(schema.moduleName)

      return JavaFile.builder(packageName, moduleClass.build())
        .indent(codegenOptions.indent)
        .build()
        .toString()
    }

  private fun generateTypeSpec(pClass: PClass, schema: ModuleSchema): TypeSpec.Builder {
    val isModuleClass = pClass == schema.moduleClass
    val javaPoetClassName = pClass.toJavaPoetName()
    val superclass =
      pClass.superclass?.takeIf { it.info != PClassInfo.Typed && it.info != PClassInfo.Module }
    val superProperties =
      superclass?.let { renameIfReservedWord(it.allProperties) }?.filterValues { !it.isHidden }
        ?: mapOf()
    val properties = renameIfReservedWord(pClass.properties).filterValues { !it.isHidden }
    val allProperties = superProperties + properties

    fun addCtorParameter(
      builder: MethodSpec.Builder,
      propJavaName: String,
      property: PClass.Property
    ) {
      builder.addParameter(
        ParameterSpec.builder(property.type.toJavaPoetName(), propJavaName)
          .addAnnotation(
            AnnotationSpec.builder(namedAnnotationName)
              .addMember("value", "\$S", property.simpleName)
              .build()
          )
          .build()
      )
    }

    fun generateConstructor(isInstantiable: Boolean): MethodSpec {
      val builder =
        MethodSpec.constructorBuilder()
          // choose most restrictive access modifier possible
          .addModifiers(
            when {
              isInstantiable -> Modifier.PUBLIC
              pClass.isAbstract || pClass.isOpen -> Modifier.PROTECTED
              else -> Modifier.PRIVATE
            }
          )

      if (superProperties.isNotEmpty()) {
        for ((name, property) in superProperties) {
          if (properties.containsKey(name)) continue
          addCtorParameter(builder, name, property)
        }
        // $W inserts space or newline (automatic line wrapping)
        val callArgsStr = superProperties.keys.joinToString(",\$W")
        // use kotlin interpolation rather than javapoet $L interpolation
        // as otherwise the $W won't get processed
        builder.addStatement("super($callArgsStr)")
      }

      for ((name, property) in properties) {
        addCtorParameter(builder, name, property)
        builder.addStatement("this.\$N = \$N", name, name)
      }

      return builder.build()
    }

    fun generateEqualsMethod(): MethodSpec {
      val builder =
        MethodSpec.methodBuilder("equals")
          .addModifiers(Modifier.PUBLIC)
          .addAnnotation(Override::class.java)
          .addParameter(Object::class.java, "obj")
          .returns(Boolean::class.java)
          .addStatement("if (this == obj) return true")
          .addStatement("if (obj == null) return false")
          // generating this.getClass() instead of class literal avoids a SpotBugs warning
          .addStatement("if (this.getClass() != obj.getClass()) return false")
          .addStatement("\$T other = (\$T) obj", javaPoetClassName, javaPoetClassName)

      for ((propertyName, property) in allProperties) {
        val accessor =
          if ((property.type as? PType.Class)?.pClass?.info == PClassInfo.Regex) "\$N.pattern()"
          else "\$N"
        builder.addStatement(
          "if (!\$T.equals(this.$accessor, other.$accessor)) return false",
          Objects::class.java,
          propertyName,
          propertyName
        )
      }

      builder.addStatement("return true")
      return builder.build()
    }

    fun generateHashCodeMethod(): MethodSpec {
      val builder =
        MethodSpec.methodBuilder("hashCode")
          .addModifiers(Modifier.PUBLIC)
          .addAnnotation(Override::class.java)
          .returns(Int::class.java)
          .addStatement("int result = 1")

      for (propertyName in allProperties.keys) {
        builder.addStatement(
          "result = 31 * result + \$T.hashCode(this.\$N)",
          Objects::class.java,
          propertyName
        )
      }

      builder.addStatement("return result")
      return builder.build()
    }

    fun generateToStringMethod(): MethodSpec {
      val builder =
        MethodSpec.methodBuilder("toString")
          .addModifiers(Modifier.PUBLIC)
          .addAnnotation(Override::class.java)
          .returns(String::class.java)

      var builderSize = 50
      val appendBuilder = CodeBlock.builder()
      for (propertyName in allProperties.keys) {
        builderSize += 50
        appendBuilder.addStatement(
          "appendProperty(builder, \$S, this.\$N)",
          propertyName,
          propertyName
        )
      }

      builder
        .addStatement(
          "\$T builder = new \$T(\$L)",
          StringBuilder::class.java,
          StringBuilder::class.java,
          builderSize
        )
        .addStatement("builder.append(\$T.class.getSimpleName()).append(\" {\")", javaPoetClassName)
        .addCode(appendBuilder.build())
        // not using $S here because it generates `"\n" + "{"`
        // with a line break in the generated code after `+`
        .addStatement("builder.append(\"\\n}\")")
        .addStatement("return builder.toString()")

      return builder.build()
    }

    // do the minimum work necessary to avoid (most) java compile errors
    // generating idiomatic Javadoc would require parsing doc comments, converting member links,
    // etc.
    fun renderAsJavadoc(docComment: String): String {
      val escaped = docComment.replace("*/", "*/")
      return if (escaped[escaped.length - 1] != '\n') escaped + '\n' else escaped
    }

    fun generateDeprecation(
      annotations: Collection,
      hasJavadoc: Boolean,
      addAnnotation: (Class<*>) -> Unit,
      addJavadoc: (String) -> Unit
    ) {
      annotations
        .firstOrNull { it.classInfo == PClassInfo.Deprecated }
        ?.let { deprecation ->
          addAnnotation(Deprecated::class.java)
          if (codegenOptions.generateJavadoc) {
            (deprecation["message"] as String?)?.let {
              if (hasJavadoc) {
                addJavadoc("\n")
              }
              addJavadoc(renderAsJavadoc("@deprecated $it"))
            }
          }
        }
    }

    fun generateField(propertyName: String, property: PClass.Property): FieldSpec {
      val builder = FieldSpec.builder(property.type.toJavaPoetName(), propertyName)

      val docComment = property.docComment
      val hasJavadoc =
        docComment != null && codegenOptions.generateJavadoc && !codegenOptions.generateGetters
      if (hasJavadoc) {
        builder.addJavadoc(renderAsJavadoc(docComment!!))
      }

      if (codegenOptions.generateGetters) {
        builder.addModifiers(
          if (pClass.isAbstract || pClass.isOpen) Modifier.PROTECTED else Modifier.PRIVATE
        )
      } else {
        generateDeprecation(
          property.annotations,
          hasJavadoc,
          { builder.addAnnotation(it) },
          { builder.addJavadoc(it) }
        )
        builder.addModifiers(Modifier.PUBLIC)
      }
      builder.addModifiers(Modifier.FINAL)

      return builder.build()
    }

    @Suppress("DuplicatedCode")
    fun generateGetter(
      propertyName: String,
      property: PClass.Property,
      isOverridden: Boolean
    ): MethodSpec {
      val propertyType = property.type
      val isBooleanProperty =
        propertyType is PType.Class && propertyType.pClass.info == PClassInfo.Boolean
      val methodName =
        (if (isBooleanProperty) "is" else "get") +
          // can use original name here (property.name rather than propertyName)
          // because getter name cannot possibly conflict with reserved words
          property.simpleName.replaceFirstChar { it.titlecaseChar() }

      val builder =
        MethodSpec.methodBuilder(methodName)
          .addModifiers(Modifier.PUBLIC)
          .returns(propertyType.toJavaPoetName())
          .addStatement("return \$N", propertyName)
      if (isOverridden) {
        builder.addAnnotation(Override::class.java)
      }

      val docComment = property.docComment
      val hasJavadoc = docComment != null && codegenOptions.generateJavadoc
      if (hasJavadoc) {
        builder.addJavadoc(renderAsJavadoc(docComment!!))
      }

      generateDeprecation(
        property.annotations,
        hasJavadoc,
        { builder.addAnnotation(it) },
        { builder.addJavadoc(it) }
      )

      return builder.build()
    }

    fun generateWithMethod(propertyName: String, property: PClass.Property): MethodSpec {
      val methodName = "with" + property.simpleName.replaceFirstChar { it.titlecaseChar() }

      val methodBuilder =
        MethodSpec.methodBuilder(methodName)
          .addModifiers(Modifier.PUBLIC)
          .addParameter(property.type.toJavaPoetName(), propertyName)
          .returns(javaPoetClassName)

      generateDeprecation(
        property.annotations,
        false,
        { methodBuilder.addAnnotation(it) },
        { methodBuilder.addJavadoc(it) }
      )

      val codeBuilder = CodeBlock.builder()
      codeBuilder.add("return new \$T(", javaPoetClassName)
      var firstProperty = true
      for (name in superProperties.keys) {
        if (name in properties) continue
        if (firstProperty) {
          firstProperty = false
          codeBuilder.add("\$N", name)
        } else {
          codeBuilder.add(", \$N", name)
        }
      }
      for (name in properties.keys) {
        if (firstProperty) {
          firstProperty = false
          codeBuilder.add("\$N", name)
        } else {
          codeBuilder.add(", \$N", name)
        }
      }
      codeBuilder.add(");\n")

      methodBuilder.addCode(codeBuilder.build())
      return methodBuilder.build()
    }

    fun generateSpringBootAnnotations(builder: TypeSpec.Builder) {
      if (isModuleClass) {
        builder.addAnnotation(
          ClassName.get("org.springframework.boot.context.properties", "ConfigurationProperties")
        )
      } else {
        // not very efficient to repeat computing module property base types for every class
        val modulePropertiesWithMatchingType =
          schema.moduleClass.allProperties.values.filter { property ->
            var propertyType = property.type
            while (propertyType is PType.Constrained || propertyType is PType.Nullable) {
              if (propertyType is PType.Constrained) {
                propertyType = propertyType.baseType
              } else if (propertyType is PType.Nullable) {
                propertyType = propertyType.baseType
              }
            }
            propertyType is PType.Class && propertyType.pClass == pClass
          }
        if (modulePropertiesWithMatchingType.size == 1) {
          // exactly one module property has this type -> make it available for direct injection
          // (potential improvement: make type available for direct injection if it occurs exactly
          // once in property tree)
          builder.addAnnotation(
            AnnotationSpec.builder(
                ClassName.get(
                  "org.springframework.boot.context.properties",
                  "ConfigurationProperties"
                )
              )
              // use "value" instead of "prefix" to entice JavaPoet to generate a single-line
              // annotation
              // that can easily be filtered out by JavaCodeGeneratorTest.`spring boot config`
              .addMember("value", "\$S", modulePropertiesWithMatchingType.first().simpleName)
              .build()
          )
        }
      }
    }

    @Suppress("DuplicatedCode")
    fun generateClass(): TypeSpec.Builder {
      val builder =
        TypeSpec.classBuilder(javaPoetClassName.simpleName()).addModifiers(Modifier.PUBLIC)

      // stateless final module classes are non-instantiable by choice
      val isInstantiable =
        !(pClass.isAbstract || (isModuleClass && !pClass.isOpen && allProperties.isEmpty()))

      if (codegenOptions.implementSerializable && isInstantiable) {
        builder.addSuperinterface(java.io.Serializable::class.java)
        builder.addField(generateSerialVersionUIDField())
      }

      val docComment = pClass.docComment
      val hasJavadoc = docComment != null && codegenOptions.generateJavadoc
      if (hasJavadoc) {
        builder.addJavadoc(renderAsJavadoc(docComment!!))
      }

      generateDeprecation(
        pClass.annotations,
        hasJavadoc,
        { builder.addAnnotation(it) },
        { builder.addJavadoc(it) }
      )

      if (!isModuleClass) {
        builder.addModifiers(Modifier.STATIC)
      }

      if (pClass.isAbstract) {
        builder.addModifiers(Modifier.ABSTRACT)
      } else if (!pClass.isOpen) {
        builder.addModifiers(Modifier.FINAL)
      }

      if (codegenOptions.generateSpringBootConfig) {
        generateSpringBootAnnotations(builder)
      }

      builder.addMethod(generateConstructor(isInstantiable))

      superclass?.let { builder.superclass(it.toJavaPoetName()) }

      // generate fields, plus getter methods and either setters or `with` methods in alternating
      // order
      // `with` methods also need to be generated for superclass properties so that return type is
      // self type
      for ((name, property) in allProperties) {
        if (name in properties) {
          builder.addField(generateField(name, property))
          if (codegenOptions.generateGetters) {
            val isOverridden = name in superProperties
            builder.addMethod(generateGetter(name, property, isOverridden))
          }
        }
        if (isInstantiable) {
          builder.addMethod(generateWithMethod(name, property))
        }
      }

      if (isInstantiable) {
        builder
          .addMethod(generateEqualsMethod())
          .addMethod(generateHashCodeMethod())
          .addMethod(generateToStringMethod())
      }

      return builder
    }

    return generateClass()
  }

  private fun generateSerialVersionUIDField(): FieldSpec {
    return FieldSpec.builder(Long::class.java, "serialVersionUID", Modifier.PRIVATE)
      .addModifiers(Modifier.STATIC, Modifier.FINAL)
      .initializer("0L")
      .build()
  }

  private fun generateEnumTypeSpec(
    typeAlias: TypeAlias,
    stringLiterals: Set
  ): TypeSpec.Builder {
    val enumConstantToPklNames =
      stringLiterals
        .groupingBy { literal ->
          CodeGeneratorUtils.toEnumConstantName(literal)
            ?: throw JavaCodeGeneratorException(
              "Cannot generate Java enum class for Pkl type alias `${typeAlias.displayName}` " +
                "because string literal type \"$literal\" cannot be converted to a valid enum constant name."
            )
        }
        .reduce { enumConstantName, firstLiteral, secondLiteral ->
          throw JavaCodeGeneratorException(
            "Cannot generate Java enum class for Pkl type alias `${typeAlias.displayName}` " +
              "because string literal types \"$firstLiteral\" and \"$secondLiteral\" " +
              "would both be converted to enum constant name `$enumConstantName`."
          )
        }

    val builder =
      TypeSpec.enumBuilder(typeAlias.simpleName)
        .addModifiers(Modifier.PUBLIC)
        .addField(String::class.java, "value", Modifier.PRIVATE)
        .addMethod(
          MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PRIVATE)
            .addParameter(String::class.java, "value")
            .addStatement("this.value = value")
            .build()
        )
        .addMethod(
          MethodSpec.methodBuilder("toString")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override::class.java)
            .returns(String::class.java)
            .addStatement("return this.value")
            .build()
        )

    for ((enumConstantName, pklName) in enumConstantToPklNames) {
      builder.addEnumConstant(
        enumConstantName,
        TypeSpec.anonymousClassBuilder("\$S", pklName).build()
      )
    }

    return builder
  }

  private val namedAnnotationName =
    if (codegenOptions.paramsAnnotation != null) {
      toClassName(codegenOptions.paramsAnnotation)
    } else {
      ClassName.get("org.pkl.config.java.mapper", "Named")
    }

  private fun appendPropertyMethod() =
    MethodSpec.methodBuilder("appendProperty")
      .addModifiers(Modifier.STATIC)
      .addParameter(StringBuilder::class.java, "builder")
      .addParameter(String::class.java, "name")
      .addParameter(Object::class.java, "value")
      .addStatement("builder.append(\"\\n  \").append(name).append(\" = \")")
      .addStatement(
        "\$T lines = \$T.toString(value).split(\"\\n\")",
        ArrayTypeName.of(String::class.java),
        Objects::class.java
      )
      .addStatement("builder.append(lines[0])")
      .beginControlFlow("for (int i = 1; i < lines.length; i++)")
      .addStatement("builder.append(\"\\n  \").append(lines[i])")
      .endControlFlow()

  private fun PClass.toJavaPoetName(): ClassName {
    val (packageName, moduleClassName) = nameMapper.map(moduleName)
    return if (isModuleClass) {
      ClassName.get(packageName, moduleClassName)
    } else {
      ClassName.get(packageName, moduleClassName, simpleName)
    }
  }

  // generated type is a nested enum class
  private fun TypeAlias.toJavaPoetName(): ClassName {
    val (packageName, moduleClassName) = nameMapper.map(moduleName)
    return ClassName.get(packageName, moduleClassName, simpleName)
  }

  /** Generate `List` if `Foo` is `abstract` or `open`, to allow subclassing. */
  private fun PType.toJavaPoetTypeArgumentName(): TypeName {
    val baseName = toJavaPoetName(nullable = false, boxed = true)
    return if (this is PType.Class && (pClass.isAbstract || pClass.isOpen)) {
      WildcardTypeName.subtypeOf(baseName)
    } else {
      baseName
    }
  }

  private fun PType.toJavaPoetName(nullable: Boolean = false, boxed: Boolean = false): TypeName =
    when (this) {
      PType.UNKNOWN -> TypeName.OBJECT.nullableIf(nullable)
      PType.NOTHING -> TypeName.VOID
      is PType.StringLiteral -> STRING.nullableIf(nullable)
      is PType.Class -> {
        // if in doubt, spell it out
        when (val classInfo = pClass.info) {
          PClassInfo.Any -> TypeName.OBJECT
          PClassInfo.Typed,
          PClassInfo.Dynamic -> TypeName.OBJECT.nullableIf(nullable)
          PClassInfo.Boolean -> TypeName.BOOLEAN.boxIf(boxed).nullableIf(nullable)
          PClassInfo.String -> STRING.nullableIf(nullable)
          // seems more useful to generate `double` than `java.lang.Number`
          PClassInfo.Number -> TypeName.DOUBLE.boxIf(boxed).nullableIf(nullable)
          PClassInfo.Int -> TypeName.LONG.boxIf(boxed).nullableIf(nullable)
          PClassInfo.Float -> TypeName.DOUBLE.boxIf(boxed).nullableIf(nullable)
          PClassInfo.Duration -> DURATION.nullableIf(nullable)
          PClassInfo.DataSize -> DATA_SIZE.nullableIf(nullable)
          PClassInfo.Pair ->
            ParameterizedTypeName.get(
                PAIR,
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[0].toJavaPoetTypeArgumentName()
                },
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[1].toJavaPoetTypeArgumentName()
                }
              )
              .nullableIf(nullable)
          PClassInfo.Collection ->
            ParameterizedTypeName.get(
                COLLECTION,
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[0].toJavaPoetTypeArgumentName()
                }
              )
              .nullableIf(nullable)
          PClassInfo.List,
          PClassInfo.Listing -> {
            ParameterizedTypeName.get(
                LIST,
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[0].toJavaPoetTypeArgumentName()
                }
              )
              .nullableIf(nullable)
          }
          PClassInfo.Set ->
            ParameterizedTypeName.get(
                SET,
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[0].toJavaPoetTypeArgumentName()
                }
              )
              .nullableIf(nullable)
          PClassInfo.Map,
          PClassInfo.Mapping ->
            ParameterizedTypeName.get(
                MAP,
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[0].toJavaPoetTypeArgumentName()
                },
                if (typeArguments.isEmpty()) {
                  TypeName.OBJECT
                } else {
                  typeArguments[1].toJavaPoetTypeArgumentName()
                }
              )
              .nullableIf(nullable)
          PClassInfo.Module -> PMODULE.nullableIf(nullable)
          PClassInfo.Class -> PCLASS.nullableIf(nullable)
          PClassInfo.Regex -> PATTERN.nullableIf(nullable)
          PClassInfo.Version -> VERSION.nullableIf(nullable)
          else ->
            when {
              !classInfo.isStandardLibraryClass -> pClass.toJavaPoetName().nullableIf(nullable)
              else ->
                throw JavaCodeGeneratorException(
                  "Standard library class `${pClass.qualifiedName}` is not supported by Java code generator. " +
                    "If you think this is an omission, please let us know."
                )
            }
        }
      }
      is PType.Nullable -> baseType.toJavaPoetName(nullable = true, boxed = true)
      is PType.Constrained -> baseType.toJavaPoetName(nullable = nullable, boxed = boxed)
      is PType.Alias ->
        when (typeAlias.qualifiedName) {
          "pkl.base#NonNull" -> TypeName.OBJECT.nullableIf(nullable)
          "pkl.base#Int8" -> TypeName.BYTE.boxIf(boxed).nullableIf(nullable)
          "pkl.base#Int16",
          "pkl.base#UInt8" -> TypeName.SHORT.boxIf(boxed).nullableIf(nullable)
          "pkl.base#Int32",
          "pkl.base#UInt16" -> TypeName.INT.boxIf(boxed).nullableIf(nullable)
          "pkl.base#UInt",
          "pkl.base#UInt32" -> TypeName.LONG.boxIf(boxed).nullableIf(nullable)
          "pkl.base#DurationUnit" -> DURATION_UNIT.nullableIf(nullable)
          "pkl.base#DataSizeUnit" -> DATASIZE_UNIT.nullableIf(nullable)
          "pkl.base#Uri" -> URI.nullableIf(nullable)
          else -> {
            if (CodeGeneratorUtils.isRepresentableAsEnum(aliasedType, null)) {
              if (typeAlias.isStandardLibraryMember) {
                throw JavaCodeGeneratorException(
                  "Standard library typealias `${typeAlias.qualifiedName}` is not supported by Java code generator. " +
                    "If you think this is an omission, please let us know."
                )
              } else {
                // reference generated enum class
                typeAlias.toJavaPoetName().nullableIf(nullable)
              }
            } else {
              // inline type alias
              aliasedType.toJavaPoetName(nullable)
            }
          }
        }
      is PType.Function ->
        throw JavaCodeGeneratorException(
          "Pkl function types are not supported by the Java code generator."
        )
      is PType.Union ->
        if (CodeGeneratorUtils.isRepresentableAsString(this)) STRING.nullableIf(nullable)
        else
          throw JavaCodeGeneratorException(
            "Pkl union types are not supported by the Java code generator."
          )
      else ->
        // should never encounter PType.TypeVariableNode because it can only occur in stdlib classes
        throw AssertionError("Encountered unexpected PType subclass: $this")
    }

  private fun TypeName.nullableIf(isNullable: Boolean): TypeName =
    if (isPrimitive && isNullable) box()
    else if (isPrimitive || isNullable) this else annotated(nonNullAnnotation)

  private fun TypeName.boxIf(shouldBox: Boolean): TypeName = if (shouldBox) box() else this

  private fun  renameIfReservedWord(map: Map): Map {
    return map.mapKeys { (key, _) ->
      if (key in javaReservedWords) {
        generateSequence("_$key") { "_$it" }.first { it !in map.keys }
      } else key
    }
  }

  private val nameMapper = NameMapper(codegenOptions.renames)
}

internal val javaReservedWords =
  setOf(
    "_", // java 9+
    "abstract",
    "assert",
    "boolean",
    "break",
    "byte",
    "case",
    "catch",
    "char",
    "class",
    "const",
    "continue",
    "default",
    "double",
    "do",
    "else",
    "enum",
    "extends",
    "false",
    "final",
    "finally",
    "float",
    "for",
    "goto",
    "if",
    "implements",
    "import",
    "instanceof",
    "int",
    "interface",
    "long",
    "native",
    "new",
    "null",
    "package",
    "private",
    "protected",
    "public",
    "return",
    "short",
    "static",
    "strictfp",
    "super",
    "switch",
    "synchronized",
    "this",
    "throw",
    "throws",
    "transient",
    "true",
    "try",
    "void",
    "volatile",
    "while"
  )




© 2015 - 2024 Weber Informatics LLC | Privacy Policy