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

main.dev.zacsweers.moshix.ir.compiler.MoshiIrVisitor.kt Maven / Gradle / Ivy

There is a newer version: 1.7.20-Beta-0.18.3
Show newest version
/*
 * Copyright (C) 2021 Zac Sweers
 *
 * 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 dev.zacsweers.moshix.ir.compiler

import dev.zacsweers.moshix.ir.compiler.api.MoshiAdapterGenerator
import dev.zacsweers.moshix.ir.compiler.api.PropertyGenerator
import dev.zacsweers.moshix.ir.compiler.sealed.MoshiSealedSymbols
import dev.zacsweers.moshix.ir.compiler.sealed.SealedAdapterGenerator
import dev.zacsweers.moshix.ir.compiler.util.dumpSrc
import dev.zacsweers.moshix.ir.compiler.util.error
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.util.file
import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.name.FqName

private val JSON_CLASS_ANNOTATION = FqName("com.squareup.moshi.JsonClass")

internal data class GeneratedAdapter(val adapterClass: IrDeclaration, val irFile: IrFile)

internal class MoshiIrVisitor(
  moduleFragment: IrModuleFragment,
  private val pluginContext: IrPluginContext,
  private val messageCollector: MessageCollector,
  private val generatedAnnotation: IrClassSymbol?,
  private val enableSealed: Boolean,
  private val deferredAddedClasses: MutableList,
  private val debug: Boolean,
) : IrElementTransformerVoidWithContext() {

  private val moshiSymbols by lazy {
    MoshiSymbols(pluginContext.irBuiltIns, moduleFragment, pluginContext)
  }

  private val moshiSealedSymbols by lazy { MoshiSealedSymbols(moshiSymbols) }

  private fun adapterGenerator(
    originalType: IrClass,
  ): MoshiAdapterGenerator? {
    val type = targetType(originalType, pluginContext, messageCollector) ?: return null

    val properties = mutableMapOf()
    for (property in type.properties.values) {
      val errors = mutableListOf<(MessageCollector) -> Unit>()
      val generator = property.generator(originalType, errors)
      if (errors.isNotEmpty()) {
        for (error in errors) {
          error(messageCollector)
        }
        return null
      }
      if (generator != null) {
        properties[property.name] = generator
      }
    }

    for ((name, parameter) in type.constructor.parameters) {
      if (type.properties[parameter.name] == null && !parameter.hasDefault) {
        // TODO would be nice if we could pass the parameter node directly?
        messageCollector.error(originalType) {
          "No property for required constructor parameter $name"
        }
        return null
      }
    }

    // Sort properties so that those with constructor parameters come first.
    val sortedProperties =
      properties.values.sortedBy {
        if (it.hasConstructorParameter) {
          it.target.parameterIndex
        } else {
          Integer.MAX_VALUE
        }
      }

    return MoshiAdapterGenerator(pluginContext, moshiSymbols, type, sortedProperties)
  }

  override fun visitClassNew(declaration: IrClass): IrStatement {
    declaration.getAnnotation(JSON_CLASS_ANNOTATION)?.let { call ->
      call.getValueArgument(0)?.let { generateAdapter ->
        // This is generateAdapter
        @Suppress("UNCHECKED_CAST")
        if (!(generateAdapter as IrConst).value) {
          return super.visitClassNew(declaration)
        }

        // This is generator
        @Suppress("UNCHECKED_CAST")
        val generatorValue = (call.getValueArgument(1) as? IrConst?)?.value.orEmpty()
        val generator =
          if (generatorValue.isNotBlank()) {
            val labelKey = generatorValue.labelKey()
            if (enableSealed && labelKey != null) {
              SealedAdapterGenerator(
                pluginContext = pluginContext,
                logger = messageCollector,
                moshiSymbols = moshiSymbols,
                moshiSealedSymbols = moshiSealedSymbols,
                target = declaration,
                labelKey = labelKey
              )
            } else {
              return super.visitClassNew(declaration)
            }
          } else {
            // Unspecified/null - means it's empty/default.
            adapterGenerator(declaration)
          }

        val adapterGenerator = generator ?: return super.visitClassNew(declaration)
        try {
          val adapterClass = adapterGenerator.prepare() ?: return super.visitClassNew(declaration)
          if (generatedAnnotation != null) {
            // TODO add generated annotation
          }
          if (debug) {
            val irSrc = adapterClass.adapterClass.dumpSrc()
            messageCollector.report(
              CompilerMessageSeverity.STRONG_WARNING,
              "MOSHI: Dumping current IR src for ${adapterClass.adapterClass.name}\n$irSrc"
            )
          }
          deferredAddedClasses += GeneratedAdapter(adapterClass.adapterClass, declaration.file)
        } catch (e: Exception) {
          messageCollector.error(declaration) {
            "Error preparing adapter for ${declaration.fqNameWhenAvailable}"
          }
          throw e
        }
      }
    }
    return super.visitClassNew(declaration)
  }
}

internal fun IrConstructorCall.labelKey(checkGenerateAdapter: Boolean = true): String? {
  // This is generateAdapter
  @Suppress("UNCHECKED_CAST")
  if (checkGenerateAdapter && !(getValueArgument(0) as IrConst).value) {
    return null
  }

  // This is generator
  @Suppress("UNCHECKED_CAST")
  val generatorValue = (getValueArgument(1) as? IrConst?)?.value.orEmpty()
  return generatorValue.labelKey()
}

internal fun String.labelKey(): String? {
  return if (isNotBlank()) {
    if (startsWith("sealed:")) {
      removePrefix("sealed:")
    } else {
      null
    }
  } else {
    // Unspecified/null - means it's empty/default.
    null
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy