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

org.pkl.codegen.kotlin.KotlinCodeGenerator.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.kotlin

import org.pkl.thirdparty.kotlinpoet.*
import org.pkl.thirdparty.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import java.io.StringWriter
import java.net.URI
import java.util.*
import org.pkl.commons.NameMapper
import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils

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

  /** Whether to generate KDoc based on doc comments for Pkl modules, classes, and properties. */
  val generateKdoc: Boolean = false,

  /** Whether to generate config classes for use with Spring Boot. */
  val generateSpringBootConfig: Boolean = false,

  /** 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(),
)

class KotlinCodeGeneratorException(message: String) : RuntimeException(message)

/** Entrypoint for the Kotlin code generator API. */
class KotlinCodeGenerator(
  /** The schema for the module to generate */
  private val moduleSchema: ModuleSchema,

  /** The options to use for the code generator */
  private val options: KotlinCodegenOptions,
) {
  companion object {
    // Prevent class name from being replaced with shaded name
    // when pkl-codegen-kotlin is shaded and embedded in pkl-tools
    // (requires circumventing kotlinc constant folding).
    private val KOTLIN_TEXT_PACKAGE_NAME = buildString {
      append("kot")
      append("lin.")
      append("text")
    }

    // `StringBuilder::class.asClassName()` generates "java.lang.StringBuilder",
    // apparently because `StringBuilder` is an `expect class`.
    private val STRING_BUILDER = ClassName(KOTLIN_TEXT_PACKAGE_NAME, "StringBuilder")

    private val STRING = String::class.asClassName()
    private val ANY_NULL = ANY.copy(nullable = true)
    private val NOTHING = Nothing::class.asClassName()
    private val KOTLIN_PAIR = org.pkl.thirdparty.kotlin.Pair::class.asClassName()
    private val COLLECTION = Collection::class.asClassName()
    private val LIST = List::class.asClassName()
    private val SET = Set::class.asClassName()
    private val MAP = Map::class.asClassName()
    private val DURATION = Duration::class.asClassName()
    private val DURATION_UNIT = DurationUnit::class.asClassName()
    private val DATA_SIZE = DataSize::class.asClassName()
    private val DATA_SIZE_UNIT = DataSizeUnit::class.asClassName()
    private val PMODULE = PModule::class.asClassName()
    private val PCLASS = PClass::class.asClassName()
    private val REGEX = Regex::class.asClassName()
    private val URI = URI::class.asClassName()
    private val VERSION = Version::class.asClassName()

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

  val output: Map
    get() {
      return mapOf(kotlinFileName to kotlinFile, propertyFileName to propertiesFile)
    }

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

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

  private val kotlinFileName: String
    get() = buildString {
      val (packageName, className) = nameMapper.map(moduleSchema.moduleName)
      val dirPath = packageName.split('.').joinToString("/", transform = IoUtils::encodePath)
      val fileName = IoUtils.encodePath(className)
      append("kot")
      append("lin/")
      if (dirPath.isNotEmpty()) {
        append("$dirPath/")
      }
      append("$fileName.kt")
    }

  val kotlinFile: String
    get() {
      if (moduleSchema.moduleUri.scheme == "pkl") {
        throw KotlinCodeGeneratorException(
          "Cannot generate Kotlin code for a Pkl standard library module (`${moduleSchema.moduleUri}`)."
        )
      }

      val pModuleClass = moduleSchema.moduleClass

      val hasModuleProperties = pModuleClass.properties.any { !it.value.isHidden }
      val isGenerateModuleClass =
        hasModuleProperties || pModuleClass.isOpen || pModuleClass.isAbstract

      fun generateCompanionRelatedCode(
        builder: TypeSpec.Builder,
        isModuleType: Boolean = false
      ): TypeSpec.Builder {
        // ensure that at most one companion object is generated for this type
        val companionObjectBuilder: Lazy = lazy {
          TypeSpec.companionObjectBuilder()
        }

        // generate append method for module classes w/o parent class;
        // reuse in subclasses and nested classes
        val isGenerateAppendPropertyMethod =
          isModuleType &&
            // check if we inherit another module's append method
            pModuleClass.superclass!!.info == PClassInfo.Module &&
            // check if anyone is (potentially) going to use our append method
            (pModuleClass.isOpen ||
              pModuleClass.isAbstract ||
              (isGenerateModuleClass && !builder.modifiers.contains(KModifier.DATA)) ||
              builder.typeSpecs.any { !it.modifiers.contains(KModifier.DATA) })

        if (isGenerateAppendPropertyMethod) {
          val appendPropertyMethodModifier =
            if (pModuleClass.isOpen || pModuleClass.isAbstract) {
              // alternative is `@JvmStatic protected`
              // (`protected` alone isn't sufficient as of Kotlin 1.6)
              KModifier.PUBLIC
            } else KModifier.PRIVATE
          if (isGenerateModuleClass) {
            companionObjectBuilder.value.addFunction(
              appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
            )
          } else { // kotlin object
            builder.addFunction(
              appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
            )
          }
        }

        // generate serialization code
        if (
          options.implementSerializable &&
            (!isModuleType || isGenerateModuleClass) &&
            !builder.modifiers.contains(KModifier.ABSTRACT)
        ) {
          builder.addSuperinterface(java.io.Serializable::class.java)
          companionObjectBuilder.value.addProperty(
            PropertySpec.builder(
                "serialVersionUID",
                Long::class.java,
                KModifier.PRIVATE,
                KModifier.CONST
              )
              .initializer("0L")
              .build()
          )
        }

        if (companionObjectBuilder.isInitialized()) {
          builder.addType(companionObjectBuilder.value.build())
        }

        return builder
      }

      val moduleType =
        if (isGenerateModuleClass) {
          generateTypeSpec(pModuleClass, moduleSchema)
        } else {
          generateObjectSpec(pModuleClass)
        }

      for (pClass in moduleSchema.classes.values) {
        moduleType.addType(
          generateCompanionRelatedCode(generateTypeSpec(pClass, moduleSchema)).build()
        )
      }

      val moduleName = moduleSchema.moduleName

      val (packageName, moduleTypeName) = nameMapper.map(moduleName)

      val fileSpec = FileSpec.builder(packageName, moduleTypeName).indent(options.indent)

      for (typeAlias in moduleSchema.typeAliases.values) {
        if (typeAlias.aliasedType is PType.Alias) {
          // generate top-level type alias (Kotlin doesn't support nested type aliases)
          fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build())
        } else {
          val stringLiterals = mutableSetOf()
          if (CodeGeneratorUtils.isRepresentableAsEnum(typeAlias.aliasedType, stringLiterals)) {
            // generate nested enum class
            moduleType.addType(generateEnumTypeSpec(typeAlias, stringLiterals).build())
          } else {
            // generate top-level type alias (Kotlin doesn't support nested type aliases)
            fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build())
          }
        }
      }

      fileSpec.addType(generateCompanionRelatedCode(moduleType, isModuleType = true).build())
      return fileSpec.build().toString()
    }

  private fun generateObjectSpec(pClass: PClass): TypeSpec.Builder {
    val builder = TypeSpec.objectBuilder(pClass.toKotlinPoetName())
    val docComment = pClass.docComment
    if (docComment != null && options.generateKdoc) {
      builder.addKdoc(renderAsKdoc(docComment))
    }
    return builder
  }

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

    fun PClass.Property.isRegex(): Boolean =
      (this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex

    val containRegexProperty = properties.values.any { it.isRegex() }

    fun generateConstructor(): FunSpec {
      val builder = FunSpec.constructorBuilder()
      for ((name, property) in allProperties) {
        builder.addParameter(name, property.type.toKotlinPoetName())
      }
      return builder.build()
    }

    fun generateCopyMethod(parameters: Map, isOverride: Boolean): FunSpec {
      val methodBuilder = FunSpec.builder("copy").returns(kotlinPoetClassName)

      if (isOverride) {
        methodBuilder.addModifiers(KModifier.OVERRIDE)
      }
      if (pClass.isOpen || pClass.isAbstract) {
        methodBuilder.addModifiers(KModifier.OPEN)
      }

      for ((name, property) in parameters) {
        val paramBuilder = ParameterSpec.builder(name, property.type.toKotlinPoetName())
        if (!isOverride) {
          paramBuilder.defaultValue("this.%N", name)
        }
        methodBuilder.addParameter(paramBuilder.build())
      }

      val codeBuilder = CodeBlock.builder().add("return %T(", kotlinPoetClassName)
      var firstProperty = true
      for (name in allProperties.keys) {
        if (firstProperty) {
          codeBuilder.add("%N", name)
          firstProperty = false
        } else {
          codeBuilder.add(", %N", name)
        }
      }
      codeBuilder.add(")\n")

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

    fun inheritsCopyMethodWithSameArity(): Boolean {
      val nearestNonAbstractAncestor =
        generateSequence(pClass.superclass) { it.superclass }.firstOrNull { !it.isAbstract }
          ?: return false
      return nearestNonAbstractAncestor.allProperties.values.count { !it.isHidden } ==
        allProperties.size
    }

    // besides generating copy method for current class,
    // override copy methods inherited from parent classes
    fun generateCopyMethods(typeBuilder: TypeSpec.Builder) {
      // copy methods don't make sense for abstract classes
      if (pClass.isAbstract) return

      var prevParameterCount = Int.MAX_VALUE
      for (currClass in generateSequence(pClass) { it.superclass }) {
        if (currClass.isAbstract) continue

        val currParameters = currClass.allProperties.filter { !it.value.isHidden }

        // avoid generating multiple methods with same no. of parameters
        if (currParameters.size < prevParameterCount) {
          val isOverride = currClass !== pClass || inheritsCopyMethodWithSameArity()
          typeBuilder.addFunction(generateCopyMethod(currParameters, isOverride))
          prevParameterCount = currParameters.size
        }
      }
    }

    fun generateEqualsMethod(): FunSpec {
      val builder =
        FunSpec.builder("equals")
          .addModifiers(KModifier.OVERRIDE)
          .addParameter("other", ANY_NULL)
          .returns(BOOLEAN)
          .addStatement("if (this === other) return true")
          // generating this.javaClass instead of class literal avoids a SpotBugs warning
          .addStatement("if (this.javaClass != other?.javaClass) return false")
          .addStatement("other as %T", kotlinPoetClassName)

      for ((propertyName, property) in allProperties) {
        val accessor = if (property.isRegex()) "%N.pattern" else "%N"
        builder.addStatement(
          "if (this.$accessor != other.$accessor) return false",
          propertyName,
          propertyName
        )
      }

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

    fun generateHashCodeMethod(): FunSpec {
      val builder =
        FunSpec.builder("hashCode")
          .addModifiers(KModifier.OVERRIDE)
          .returns(INT)
          .addStatement("var result = 1")

      for (propertyName in allProperties.keys) {
        // use Objects.hashCode() because Kotlin's Any?.hashCode()
        // doesn't work for platform types (will get NPE if null)
        builder.addStatement(
          "result = 31 * result + %T.hashCode(this.%N)",
          Objects::class,
          propertyName
        )
      }

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

    fun generateToStringMethod(): FunSpec {
      val builder = FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).returns(STRING)

      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("val builder = %T(%L)", STRING_BUILDER, builderSize)
        .addStatement(
          // generate `::class.java.simpleName` instead of `::class.simpleName`
          // to avoid making user code depend on kotlin-reflect
          "builder.append(%T::class.java.simpleName).append(\" {\")",
          kotlinPoetClassName
        )
        .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()
    }

    fun generateDeprecation(
      annotations: Collection,
      addAnnotation: (AnnotationSpec) -> Unit
    ) {
      annotations
        .firstOrNull { it.classInfo == PClassInfo.Deprecated }
        ?.let { deprecation ->
          val builder = AnnotationSpec.builder(Deprecated::class)
          (deprecation["message"] as String?)?.let { builder.addMember("message = %S", it) }
          addAnnotation(builder.build())
        }
    }

    fun generateProperty(propertyName: String, property: PClass.Property): PropertySpec {
      val typeName = property.type.toKotlinPoetName()
      val builder = PropertySpec.builder(propertyName, typeName).initializer("%L", propertyName)

      generateDeprecation(property.annotations) { builder.addAnnotation(it) }

      val docComment = property.docComment
      if (docComment != null && options.generateKdoc) {
        builder.addKdoc(renderAsKdoc(docComment))
      }
      if (propertyName in superProperties) {
        builder.addModifiers(KModifier.OVERRIDE)
      }
      if (pClass.isOpen || pClass.isAbstract) {
        builder.addModifiers(KModifier.OPEN)
      }

      return builder.build()
    }

    fun generateSpringBootAnnotations(builder: TypeSpec.Builder) {
      if (isModuleClass) {
        builder.addAnnotation(
          ClassName("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("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("%S", modulePropertiesWithMatchingType.first().simpleName)
              .build()
          )
        }
      }
    }

    fun generateRegularClass(): TypeSpec.Builder {
      val builder = TypeSpec.classBuilder(kotlinPoetClassName)

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

      builder.primaryConstructor(generateConstructor())

      val docComment = pClass.docComment
      if (docComment != null && options.generateKdoc) {
        builder.addKdoc(renderAsKdoc(docComment))
      }

      if (pClass.isAbstract) {
        builder.addModifiers(KModifier.ABSTRACT)
      } else if (pClass.isOpen) {
        builder.addModifiers(KModifier.OPEN)
      }

      superclass?.let { superclass ->
        val superclassName = superclass.toKotlinPoetName()
        builder.superclass(superclassName)
        for (propertyName in superProperties.keys) {
          builder.addSuperclassConstructorParameter(propertyName)
        }
      }

      for ((name, property) in properties) {
        builder.addProperty(generateProperty(name, property))
      }

      if (!pClass.isAbstract) {
        generateCopyMethods(builder)
        builder
          .addFunction(generateEqualsMethod())
          .addFunction(generateHashCodeMethod())
          .addFunction(generateToStringMethod())
      }

      return builder
    }

    fun generateDataClass(): TypeSpec.Builder {
      val builder = TypeSpec.classBuilder(kotlinPoetClassName).addModifiers(KModifier.DATA)

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

      builder.primaryConstructor(generateConstructor())

      generateDeprecation(pClass.annotations) { builder.addAnnotation(it) }

      val docComment = pClass.docComment
      if (docComment != null && options.generateKdoc) {
        builder.addKdoc(renderAsKdoc(docComment))
      }

      for ((name, property) in properties) {
        builder.addProperty(generateProperty(name, property))
      }

      // Regex requires special approach when compared to another Regex
      // So we need to override `.equals` method even for kotlin's `data class`es if
      // any of the properties is of Regex type
      if (containRegexProperty) {
        builder.addFunction(generateEqualsMethod())
      }

      return builder
    }

    return if (superclass == null && !pClass.isAbstract && !pClass.isOpen) generateDataClass()
    else generateRegularClass()
  }

  private fun generateEnumTypeSpec(
    typeAlias: TypeAlias,
    stringLiterals: Set
  ): TypeSpec.Builder {
    val enumConstantToPklNames =
      stringLiterals
        .groupingBy { literal ->
          CodeGeneratorUtils.toEnumConstantName(literal)
            ?: throw KotlinCodeGeneratorException(
              "Cannot generate Kotlin 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 KotlinCodeGeneratorException(
            "Cannot generate Kotlin 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)
        .primaryConstructor(
          FunSpec.constructorBuilder().addParameter("value", String::class).build()
        )
        .addProperty(PropertySpec.builder("value", String::class).initializer("value").build())
        .addFunction(
          FunSpec.builder("toString")
            .addModifiers(KModifier.OVERRIDE)
            .addStatement("return value")
            .build()
        )
    for ((enumConstantName, pklName) in enumConstantToPklNames) {
      builder.addEnumConstant(
        enumConstantName,
        TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", pklName).build()
      )
    }

    return builder
  }

  private fun generateTypeAliasSpec(typeAlias: TypeAlias): TypeAliasSpec.Builder {
    val builder =
      TypeAliasSpec.builder(typeAlias.simpleName, typeAlias.aliasedType.toKotlinPoetName())
    for (typeParameter in typeAlias.typeParameters) {
      builder.addTypeVariable(
        TypeVariableName(typeParameter.name, typeParameter.variance.toKotlinPoet())
      )
    }

    val docComment = typeAlias.docComment
    if (docComment != null && options.generateKdoc) {
      builder.addKdoc(renderAsKdoc(docComment))
    }

    return builder
  }

  private fun TypeParameter.Variance.toKotlinPoet(): KModifier? =
    when (this) {
      TypeParameter.Variance.COVARIANT -> KModifier.OUT
      TypeParameter.Variance.CONTRAVARIANT -> KModifier.IN
      else -> null
    }

  // do the minimum work necessary to avoid kotlin compile errors
  // generating idiomatic KDoc would require parsing doc comments, converting member links, etc.
  private fun renderAsKdoc(docComment: String): String = docComment

  private fun appendPropertyMethod() =
    FunSpec.builder("appendProperty")
      .addParameter("builder", STRING_BUILDER)
      .addParameter("name", STRING)
      .addParameter("value", ANY_NULL)
      .addStatement("builder.append(\"\\n  \").append(name).append(\" = \")")
      .addStatement("val lines = value.toString().split(\"\\n\")")
      .addStatement("builder.append(lines[0])")
      .beginControlFlow("for (i in 1..lines.lastIndex)")
      .addStatement("builder.append(\"\\n  \").append(lines[i])")
      .endControlFlow()

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

  private fun TypeAlias.toKotlinPoetName(): ClassName {
    val (packageName, moduleTypeName) = nameMapper.map(moduleName)

    return when {
      aliasedType is PType.Alias -> {
        // Kotlin type generated for [this] is a top-level type alias
        ClassName(packageName, simpleName)
      }
      CodeGeneratorUtils.isRepresentableAsEnum(aliasedType, null) -> {
        if (isStandardLibraryMember) {
          throw KotlinCodeGeneratorException(
            "Standard library typealias `${qualifiedName}` is not supported by Kotlin code generator." +
              " If you think this is an omission, please let us know."
          )
        }
        // Kotlin type generated for [this] is a nested enum class
        ClassName(packageName, moduleTypeName, simpleName)
      }
      else -> {
        // Kotlin type generated for [this] is a top-level type alias
        ClassName(packageName, simpleName)
      }
    }
  }

  private fun PType.toKotlinPoetName(): TypeName =
    when (this) {
      PType.UNKNOWN -> ANY_NULL
      PType.NOTHING -> NOTHING
      is PType.StringLiteral -> STRING
      is PType.Class -> {
        // if in doubt, spell it out
        when (val classInfo = pClass.info) {
          PClassInfo.Any -> ANY_NULL
          PClassInfo.Typed,
          PClassInfo.Dynamic -> ANY
          PClassInfo.Boolean -> BOOLEAN
          PClassInfo.String -> STRING
          // seems more useful to generate `Double` than `org.pkl.thirdparty.kotlin.Number`
          PClassInfo.Number -> DOUBLE
          PClassInfo.Int -> LONG
          PClassInfo.Float -> DOUBLE
          PClassInfo.Duration -> DURATION
          PClassInfo.DataSize -> DATA_SIZE
          PClassInfo.Pair ->
            KOTLIN_PAIR.parameterizedBy(
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(),
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName()
            )
          PClassInfo.Collection ->
            COLLECTION.parameterizedBy(
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
            )
          PClassInfo.List,
          PClassInfo.Listing ->
            LIST.parameterizedBy(
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
            )
          PClassInfo.Set ->
            SET.parameterizedBy(
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
            )
          PClassInfo.Map,
          PClassInfo.Mapping ->
            MAP.parameterizedBy(
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(),
              if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName()
            )
          PClassInfo.Module -> PMODULE
          PClassInfo.Class -> PCLASS
          PClassInfo.Regex -> REGEX
          PClassInfo.Version -> VERSION
          else ->
            when {
              !classInfo.isStandardLibraryClass -> pClass.toKotlinPoetName()
              else ->
                throw KotlinCodeGeneratorException(
                  "Standard library class `${pClass.qualifiedName}` is not supported by Kotlin code generator. " +
                    "If you think this is an omission, please let us know."
                )
            }
        }
      }
      is PType.Nullable -> baseType.toKotlinPoetName().copy(nullable = true)
      is PType.Constrained -> baseType.toKotlinPoetName()
      is PType.Alias ->
        when (typeAlias.qualifiedName) {
          "pkl.base#NonNull" -> ANY
          // Not currently generating Kotlin unsigned types
          // because it's not clear if the benefits outweigh the drawbacks:
          // - breaking change
          // - Kotlin unsigned types aren't intended for domain modeling
          // - diverts from Java code generator
          // - doesn't increase safety
          //   - range already checked on Pkl side
          //   - conversion to signed type doesn't perform range check
          "pkl.base#Int8" -> BYTE
          "pkl.base#Int16",
          "pkl.base#UInt8" -> SHORT
          "pkl.base#Int32",
          "pkl.base#UInt16" -> INT
          "pkl.base#UInt",
          "pkl.base#UInt32" -> LONG
          "pkl.base#DurationUnit" -> DURATION_UNIT
          "pkl.base#DataSizeUnit" -> DATA_SIZE_UNIT
          "pkl.base#Uri" -> URI
          else -> {
            val className = typeAlias.toKotlinPoetName()
            when {
              typeAlias.typeParameters.isEmpty() -> className
              typeArguments.isEmpty() -> {
                // no type arguments provided for a type alias with type parameters -> fill in
                // `Any?` (equivalent of `unknown`)
                val typeArgs = Array(typeAlias.typeParameters.size) { ANY_NULL }
                className.parameterizedBy(*typeArgs)
              }
              else -> className.parameterizedBy(*typeArguments.toKotlinPoet())
            }
          }
        }
      is PType.Function ->
        throw KotlinCodeGeneratorException(
          "Pkl function types are not supported by the Kotlin code generator."
        )
      is PType.Union ->
        if (CodeGeneratorUtils.isRepresentableAsString(this)) STRING
        else
          throw KotlinCodeGeneratorException(
            "Pkl union types are not supported by the Kotlin code generator."
          )

      // occurs on RHS of generic type aliases
      is PType.TypeVariable -> TypeVariableName(typeParameter.name)
      else -> throw AssertionError("Encountered unexpected PType subclass: $this")
    }

  private fun List.toKotlinPoet(): Array =
    map { it.toKotlinPoetName() }.toTypedArray()

  private val nameMapper = NameMapper(options.renames)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy