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

arrow.core.extensions.ExtensionProcessor.kt Maven / Gradle / Ivy

package arrow.core.extensions

import arrow.common.utils.knownError
import arrow.extension
import arrow.meta.ast.Code
import arrow.meta.ast.Func
import arrow.meta.ast.Modifier
import arrow.meta.ast.PackageName
import arrow.meta.ast.Property
import arrow.meta.ast.Type
import arrow.meta.ast.TypeName
import arrow.meta.encoder.TypeClassInstance
import arrow.meta.processor.MetaProcessor
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.FileSpec
import javax.annotation.processing.Processor
import javax.lang.model.element.TypeElement

@AutoService(Processor::class)
class ExtensionProcessor : MetaProcessor(extension::class), PolyTemplateGenerator {

  override fun transform(annotatedElement: AnnotatedElement): List =
    when (annotatedElement) {
      is AnnotatedElement.Interface -> {
        val info = annotatedElement.typeElement.typeClassInstance()
        // log("[${info?.instance?.name?.simpleName}] : Generating [${info?.typeClass?.name?.simpleName}] extensions for [${info?.projectedCompanion}]")
        val fileSpec = annotatedElement.fileSpecBuilder(info)
        info.processTypeClassExtensions(fileSpec, annotatedElement)
      }
      else -> notAnInstanceError()
    }

  private fun TypeClassInstance?.processTypeClassExtensions(
    fileSpec: FileSpec.Builder?,
    annotatedElement: AnnotatedElement.Interface
  ): List =
    when {
      this != null && fileSpec != null -> {
        val wrappedType = dataType.kindWrapper
        val wrappedExtensions = wrappedTypeExtensions(wrappedType)
        val properties = if (supportsCache()) listOf(genCachedExtension()) else emptyList()
        val functions = genDataTypeExtensions() + listOf(genCompanionFactory(dataType.name))
        val mainFileSpec: FileSpec.Builder = functions.inMainFileSpec(fileSpec.addProperties(properties))
        val wrappedFileSpec = wrappedExtensions.inWrappedFileSpec(wrappedType, annotatedElement, this, properties)
        listOfNotNull(mainFileSpec, wrappedFileSpec)
      }
      else -> emptyList()
    }

  private fun List.inWrappedFileSpec(
    wrappedType: Pair?,
    annotatedElement: AnnotatedElement.Interface,
    info: TypeClassInstance,
    properties: List
  ): FileSpec.Builder? =
    if (wrappedType != null && isNotEmpty()) {
      val wrappedFileBuilder = info.wrappedFileBuilder(annotatedElement, wrappedType)
      val wrappedFunctionSpec = inMainFileSpec(wrappedFileBuilder).addProperties(properties)
      val wrappedSimpleName = wrappedType.second.rawType.simpleName
      val wrappedPackage = PackageName("${info.instance.packageName.value}.${wrappedSimpleName.toLowerCase()}")
      wrappedType.fakeCompanion(wrappedFunctionSpec, wrappedSimpleName, wrappedPackage, info, annotatedElement)
    } else null

  private fun Pair.fakeCompanion(
    wrappedFunctionSpec: FileSpec.Builder,
    wrappedSimpleName: String,
    wrappedPackage: PackageName,
    info: TypeClassInstance,
    annotatedElement: AnnotatedElement.Interface
  ): FileSpec.Builder =
    wrappedFunctionSpec.addType(
      Type(
        name = TypeName.Classy(
          wrappedSimpleName,
          "${wrappedPackage.value}.${wrappedSimpleName.toLowerCase()}",
          PackageName("${info.instance.packageName.value}.${info.typeClass.name.simpleName.decapitalize()}.${wrappedSimpleName.toLowerCase()}")
        ),
        kind = Type.Shape.Object,
        packageName = annotatedElement.type.packageName,
        declaredFunctions = listOf(info.genCompanionFactory(second.rawType).copy(receiverType = null))
      ).lyrics()
    )

  private fun TypeClassInstance.wrappedFileBuilder(annotatedElement: AnnotatedElement.Interface, wrappedType: Pair): FileSpec.Builder =
    FileSpec.builder(
      packageName = annotatedElement.type.packageName.value + "." +
        wrappedType.second.rawType.simpleName.substringAfterLast(".").toLowerCase() + "." +
        typeClass.name.simpleName.decapitalize(),
      fileName = annotatedElement.type.name.simpleName
    )

  private fun List.inMainFileSpec(fileSpec: FileSpec.Builder): FileSpec.Builder =
    fold(fileSpec) { spec, func ->
      spec.addFunction(func.lyrics().toBuilder().build())
    }

  private fun FileSpec.Builder.addProperties(properties: List): FileSpec.Builder =
    properties.fold(this) { spec, prop -> spec.addProperty(prop.lyrics().toBuilder().build()) }

  private fun TypeClassInstance.wrappedTypeExtensions(wrappedType: Pair?): List =
    if (wrappedType != null) {
      val wrappedTypeElement = elementUtils.getTypeElement(wrappedType.second.rawType.asPlatform().fqName)
      val wrappedTypeFunctions = wrappedTypeElement.wrappedTypeFunctions()
      wrappedTypeExtensions(wrappedType, wrappedTypeFunctions)
    } else emptyList()

  private fun TypeClassInstance.wrappedTypeExtensions(
    wrappedType: Pair?,
    wrappedTypeFunctions: List
  ): List =
    genDataTypeExtensions(wrappedType)
      .asSequence()
      .filter { ext -> ext.name !in wrappedTypeFunctions }
      .toList()

  /**
   * Not all wrapped types can be introspected as TypeElement
   */
  private fun TypeElement?.wrappedTypeFunctions(): List =
    @Suppress("SwallowedException")
    try {
      elementUtils.getAllMembers(this).map { it.simpleName.toString() }
    } catch (e: Exception) {
      emptyList()
    }

  private fun AnnotatedElement.Interface.fileSpecBuilder(info: TypeClassInstance?): FileSpec.Builder? =
    info?.let {
      FileSpec.builder(
        type.packageName.value +
          "." + info.projectedCompanion.simpleName.substringAfterLast(".").toLowerCase() +
          "." + info.typeClass.name.simpleName.decapitalize(),
        type.name.simpleName
      ).indent("  ")
    }

  private fun notAnInstanceError(): Nothing =
    knownError("@instance is only allowed on `interface` extending another interface of at least one type argument (type class) as first declaration in the instance list")

  private fun TypeClassInstance.supportsCache(): Boolean =
    requiredAbstractFunctions.isEmpty()

  private fun TypeClassInstance.cachedInstanceName(): String =
    typeClass.name.simpleName.decapitalize() + "_singleton"

  private fun TypeClassInstance.genCachedExtension(): Property =
    Property(
      kdoc = Code { "cached extension" },
      name = cachedInstanceName(),
      annotations = listOf(PublishedApi()),
      modifiers = listOf(Modifier.Internal),
      type = instance.name.widenTypeArgs(),
      initializer = Code {
        if (instance.typeVariables.isEmpty()) "object : ${+instance.name} {}"
        else "object : ${+instance.name.simpleName}<${instance.typeVariables.joinToString { it.widenTypeArgs().rawName }}> {}"
      }
    )

  private fun TypeClassInstance.genCompanionFactory(targetType: TypeName): Func {
    val target = when (projectedCompanion) {
      is TypeName.Classy -> projectedCompanion.companion()
      else -> TypeName.Classy(
        simpleName = "Companion",
        fqName = "${targetType.rawName}.Companion",
        pckg = PackageName(targetType.rawName.substringBeforeLast(".") + "." + targetType.downKind.simpleName)
      )
    }
    return Func(
      kdoc = typeClass.kdoc?.eval(this),
      modifiers = listOf(Modifier.Inline),
      annotations = listOf(
        SuppressAnnotation(
          """"UNCHECKED_CAST"""",
          """"NOTHING_TO_INLINE""""
        )
      ),
      name = typeClass.name.simpleName.decapitalize(),
      parameters = requiredParameters,
      receiverType = target,
      typeVariables = instance.typeVariables.map { it.removeConstrains() },
      returnType = instance.name,
      body = Code {
        if (supportsCache() && instance.typeVariables.isEmpty()) "return ${cachedInstanceName()}"
        else if (supportsCache() && instance.typeVariables.isNotEmpty()) "return ${cachedInstanceName()} as ${+instance.name}"
        else "return object : ${+instance.name} { ${requiredAbstractFunctions.code()} }"
      }
    )
  }

  private fun TypeClassInstance.genDataTypeExtensions(wrappedType: Pair? = null): List {
    val extensionSet = typeClass.declaredFunctions.map { it.jvmMethodSignature }
    return instance
      .allFunctions
      .asSequence()
      .filter { extensionSet.contains(it.jvmMethodSignature) }
      .filterNot { it.receiverType?.simpleName == "MonadContinuation" }
      .distinctBy { func ->
        func.name + func.parameters.asSequence()
          .filterNot { it.type is TypeName.TypeVariable }
          .map { p -> p.type.simpleName }.toList()
      }
      .map { it.removeConstrains(keepModifiers = setOf(Modifier.Infix, Modifier.Operator, Modifier.Suspend)) }
      .map { f ->
        val func = f.removeDummyArgs().downKindReturnType().wrap(wrappedType)
        val dummyArgsCount = f.countDummyArgs()
        val allArgs = requiredParameters + func.parameters
        val typeVariables = extensionTypeVariables(func)
        func
          .copy(
            kdoc = func.kdoc?.eval(this),
            modifiers =
            func.modifiers.filter { it == Modifier.Suspend } +
              when {
                allArgs.size > 1 -> emptyList()
                func.receiverType == null -> emptyList()
                else -> func.modifiers
              }, // remove infix and operator mods
            typeVariables = typeVariables,
            annotations = listOf(
              JvmName(func.name + if (dummyArgsCount > 0) dummyArgsCount else ""),
              SuppressAnnotation(
                """"UNCHECKED_CAST"""",
                """"USELESS_CAST"""",
                """"EXTENSION_SHADOWED_BY_MEMBER"""",
                """"UNUSED_PARAMETER""""
              )
            ),
            parameters = allArgs,
            body = Code {
              val receiverType = func.receiverType
              val wrappedArgs =
                if (wrappedType != null) {
                  func.parameters.code {
                    if (it.type.rawName == wrappedType.second.rawName) Code("${dataType.name.rawName}(${it.name})")
                    else Code(it.name)
                  }
                } else func.parameters.codeNames()
              // Const is a special case because it uses a phantom type arg and the Kotlin compiler chooses to
              // recursively call the extension if you ascribe the call types
              val typeVars =
                if (instance.name.simpleName == "ConstFunctor") emptyList()
                else func.typeVariables
              val companionOrFactory = extensionsCompanionOrFactory(wrappedType)
              if (receiverType != null) {
                val impl = receiverTypeExtensionImpl(wrappedType, receiverType, func, wrappedArgs, typeVars)
                receiverTypeExtensionCode(companionOrFactory, impl)
              } else {
                staticExtensionImpl(companionOrFactory, func, typeVars, wrappedArgs)
              }
            })
      }
      .toList()
  }

  private fun TypeClassInstance.extensionTypeVariables(func: Func): List = (instance.typeVariables + func.typeVariables)
    .asSequence()
    .map { it.removeConstrains() }
    .distinctBy { it.name }
    .toList()

  private fun TypeClassInstance.staticExtensionImpl(
    companionOrFactory: TypeName,
    func: Func,
    typeVars: List,
    wrappedArgs: Code
  ): String =
    """|return ${+companionOrFactory}
           |   .${typeClass.name.simpleName.decapitalize()}${+instance.typeVariables}(${requiredParameters.code { +it.name }})
           |   .${+func.name}${+typeVars}($wrappedArgs) as ${+func.returnType}
           |
         """

  private fun TypeClassInstance.receiverTypeExtensionCode(
    companionOrFactory: TypeName,
    impl: String
  ): String =
    """|return ${+companionOrFactory}.${typeClass.name.simpleName.decapitalize()}${+instance.typeVariables}(${requiredParameters.code { +it.name }}).run·{
           |  $impl
           |}
           |
         """

  private fun TypeClassInstance.receiverTypeExtensionImpl(
    wrappedType: Pair?,
    receiverType: TypeName,
    func: Func,
    wrappedArgs: Code,
    typeVars: List
  ): String =
    if (wrappedType != null && receiverType.rawName == wrappedType.second.rawName) {
      "${dataType.name.rawName}(this@${+func.name}).${+func.name}${+func.typeVariables}($wrappedArgs) as ${+func.returnType}"
    } else {
      "this@${+func.name}.${+func.name}${+typeVars}($wrappedArgs) as ${+func.returnType}"
    }

  private fun TypeClassInstance.extensionsCompanionOrFactory(wrappedType: Pair?): TypeName = if (wrappedType != null) {
    val wrappedSimpleName = wrappedType.second.rawType.simpleName
    val wrappedPackage = PackageName("${instance.packageName.value}.${wrappedSimpleName.toLowerCase()}")
    TypeName.Classy(
      wrappedSimpleName,
      "${wrappedPackage.value}.${wrappedSimpleName.toLowerCase()}",
      PackageName("${instance.packageName.value}.${wrappedSimpleName.toLowerCase()}.${typeClass.name.simpleName.decapitalize()}")
    )
  } else projectedCompanion
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy