Coverage Summary for Class: KotlinBlockBuilderContext (com.kotlinorm.compiler.plugin.utils.context)

Class Class, % Method, % Branch, % Line, % Instruction, %
KotlinBlockBuilderContext 100% (1/1) 100% (2/2) 100% (4/4) 100% (37/37)


 /**
  * Copyright 2022-2025 kronos-orm
  *
  * 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
  *
  *     http://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 com.kotlinorm.compiler.plugin.utils.context
 
 import com.kotlinorm.compiler.helpers.Receivers
 import com.kotlinorm.compiler.helpers.applyIrCall
 import com.kotlinorm.compiler.helpers.irEnum
 import com.kotlinorm.compiler.helpers.irListOf
 import com.kotlinorm.compiler.helpers.subType
 import com.kotlinorm.compiler.helpers.valueArguments
 import com.kotlinorm.compiler.plugin.beans.CriteriaIR
 import com.kotlinorm.compiler.plugin.beans.FieldIR
 import com.kotlinorm.compiler.plugin.beans.primaryKeyTypeSymbol
 import com.kotlinorm.compiler.plugin.utils.CascadeAnnotationsFqName
 import com.kotlinorm.compiler.plugin.utils.IgnoreAnnotationsFqName
 import com.kotlinorm.compiler.plugin.utils.KPojoFqName
 import com.kotlinorm.compiler.plugin.utils.KronosColumnValueType
 import com.kotlinorm.compiler.plugin.utils.SerializeAnnotationsFqName
 import com.kotlinorm.compiler.plugin.utils.extractDeclarationComment
 import com.kotlinorm.compiler.plugin.utils.fieldSymbol
 import com.kotlinorm.compiler.plugin.utils.getColumnName
 import com.kotlinorm.compiler.plugin.utils.kTableForCondition.createCriteria
 import com.kotlinorm.compiler.plugin.utils.realStartOffset
 import com.kotlinorm.compiler.plugin.utils.sourceFileCache
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.ir.builders.IrBlockBuilder
 import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope
 import org.jetbrains.kotlin.ir.builders.irBoolean
 import org.jetbrains.kotlin.ir.builders.irGetField
 import org.jetbrains.kotlin.ir.builders.irInt
 import org.jetbrains.kotlin.ir.builders.irNull
 import org.jetbrains.kotlin.ir.builders.irSetField
 import org.jetbrains.kotlin.ir.builders.irString
 import org.jetbrains.kotlin.ir.declarations.IrClass
 import org.jetbrains.kotlin.ir.declarations.IrDeclaration
 import org.jetbrains.kotlin.ir.declarations.IrProperty
 import org.jetbrains.kotlin.ir.declarations.IrVariable
 import org.jetbrains.kotlin.ir.expressions.IrBlock
 import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
 import org.jetbrains.kotlin.ir.expressions.IrExpression
 import org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression
 import org.jetbrains.kotlin.ir.expressions.IrPropertyReference
 import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
 import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl
 import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
 import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
 import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.types.classFqName
 import org.jetbrains.kotlin.ir.types.getClass
 import org.jetbrains.kotlin.ir.util.constructors
 import org.jetbrains.kotlin.ir.util.file
 import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
 import org.jetbrains.kotlin.ir.util.hasAnnotation
 import org.jetbrains.kotlin.ir.util.properties
 import org.jetbrains.kotlin.ir.util.sourceElement
 import org.jetbrains.kotlin.ir.util.superTypes
 import org.jetbrains.kotlin.name.FqName
 import java.io.File
 import kotlin.text.Charsets.UTF_8
 
 open class KotlinBuilderWithScopeContext<out T : IrBuilderWithScope>(
     open val pluginContext: IrPluginContext,
     open val builder: T
 ) {
     /**
      * Checks if the given IrExpression is a Kronos Column.
      *
      * This function checks if the given IrExpression is an instance of IrCallImpl and if its origin is either GET_PROPERTY or EQ.
      * If these conditions are met, it retrieves the property name from the IrExpression and finds the corresponding property in the class.
      * It then checks if any of the super types of the parent class of the property is "com.kotlinorm.interfaces.KPojo".
      *
      * @receiver the `IrExpression` to check. It can be null.
      * @return returns true if the IrExpression is a Kronos Column, false otherwise.
      */
     @OptIn(UnsafeDuringIrConstructionAPI::class)
     fun IrExpression?.isKPojo(): Boolean {
         if (this == null) return false
         return (this is IrCallImpl && this.symbol.owner.correspondingPropertySymbol?.owner is IrProperty && this.let {
             val propertyName = pluginContext.withContext{ correspondingName!!.asString() }
             (dispatchReceiver!!.type.getClass()!!.properties.first { it.name.asString() == propertyName }.parent as IrClass).isKPojo()
         }) || this is IrPropertyReference && this.symbol.owner.parent is IrClass && (this.symbol.owner.parent as IrClass).isKPojo()
     }
 
     fun IrType.isKPojo(): Boolean {
         return superTypes().any { it.classFqName == pluginContext.KPojoFqName }
     }
 
     /**
      * Finds a Kronos Column in the given IrExpression.
      *
      * This function checks if the given IrExpression is a Kronos Column. If it is, it returns the expression itself.
      * If the expression is an instance of IrBlock and its origin is SAFE_CALL, it returns null.
      * If the expression is not an instance of IrCall, it returns the expression itself.
      * If the extension receiver or the dispatch receiver of the expression is an instance of IrCall, it recursively calls this function with the receiver.
      * If none of the above conditions are met, it iterates over the value arguments of the expression. If it finds an argument that is an instance of IrCall, it recursively calls this function with the argument.
      * If no Kronos Column is found, it returns the expression itself.
      *
      * @receiver the `IrExpression` to find the Kronos Column in.
      * @return returns the found Kronos Column `IrExpression`, or null if no Kronos Column is found.
      */
     fun IrExpression.findKronosColumn(): IrExpression? {
         if (this is IrBlock && origin == IrStatementOrigin.SAFE_CALL) return null
         if (this !is IrCall) return this
         if (isKPojo()) {
             return this
         } else if (extensionReceiver is IrCall) {
             return extensionReceiver!!.findKronosColumn()
         } else if (dispatchReceiver is IrCall) {
             return dispatchReceiver!!.findKronosColumn()
         } else {
             for (arg in valueArguments) {
                 if (arg is IrCall) {
                     return arg.findKronosColumn()
                 }
             }
             return this
         }
     }
 
     /**
      * Determines the type and value of a Kronos Column.
      *
      * This function checks if the given IrExpression is a Kronos Column. If it is, it returns a pair with the type as ColumnName and the expression itself.
      * If the function name of the expression is "value", it returns a pair with the type as Value and the expression itself.
      * Otherwise, it tries to find a Kronos Column in the expression and returns a pair with the type as ColumnName and the found Kronos Column.
      * If no Kronos Column is found, it throws an IllegalStateException.
      *
      * @receiver the `IrExpression` to check.
      * @return returns a pair with the type and value of the Kronos Column.
      * @throws IllegalStateException if no Kronos Column is found in the expression and the function name is not "value".
      */
     fun IrExpression.columnValueGetter(): Pair<KronosColumnValueType, IrExpression> {
         return if (this.isKPojo()) {
             KronosColumnValueType.ColumnName to this
         } else if (this.funcName() == "value") {
             KronosColumnValueType.Value to this
         } else if (this.isKronosFunction()) {
             KronosColumnValueType.Function to this
         } else {
             KronosColumnValueType.ColumnName to (findKronosColumn()
                 ?: throw IllegalStateException("`?.` is not supported in CriteriaBuilder. Unless using `.value to get the real expression value."))
         }
     }
 
     fun IrExpression?.isKronosFunction(): Boolean {
         if (this == null) return false
         return this is IrCallImpl && this.extensionReceiver?.type?.classFqName == FqName("com.kotlinorm.functions.FunctionHandler")
     }
 
     fun IrExpression.setValue(property: IrProperty, value: IrExpression): IrExpression? {
         with(builder) {
             if (property.isDelegated) return null
             return if (property.setter != null) {
                 applyIrCall(
                     property.setter!!.symbol, value
                 ) {
                     dispatchReceiver = this@setValue
                 }
             } else {
                 irSetField(
                     this@setValue, property.backingField!!, value
                 )
             }
         }
     }
 
     fun IrExpression.getValue(property: IrProperty): IrExpression {
         with(builder) {
             return if (property.getter != null) {
                 applyIrCall(
                     property.getter!!.symbol
                 ) {
                     dispatchReceiver = this@getValue
                 }
             } else {
                 irGetField(
                     this@getValue, property.backingField!!
                 )
             }
         }
     }
 
     /**
      * For properties that are not columns, we need to check if :
      * 1. the field using @Ignore annotation
      * 2. the type is a KPojo or its super types are KPojo
      * 3. is a Collection of KPojo, such as List<KPojo>
      * 4. has Annotation `@Cascade`
      *
      * Specially, if the property is using `@Serialize` annotation, it will be treated as a column.
      * but the priority of `@Serialize` is lower than `Ignore` annotation and `@Cascade` annotation.
      */
     fun IrProperty.isColumn(
         irPropertyType: IrType = this.backingField?.type ?: pluginContext.irBuiltIns.anyNType,
         ignored: IrConstructorCall? = ignoreAnnotationValue()
     ): Boolean {
         if (ignored.ignore("all")) return false
         if (hasAnnotation(CascadeAnnotationsFqName)) return false
         if (hasAnnotation(SerializeAnnotationsFqName)) return true
         if (irPropertyType.isKPojo() || irPropertyType.subType()?.isKPojo() == true) return false
         return true
     }
 
     @OptIn(UnsafeDuringIrConstructionAPI::class)
     fun IrProperty.ignoreAnnotationValue(): IrConstructorCall? {
         return annotations.find { it.symbol.owner.returnType.getClass()!!.fqNameWhenAvailable == IgnoreAnnotationsFqName }
     }
 
     @OptIn(UnsafeDuringIrConstructionAPI::class)
     fun IrConstructorCall?.ignore(name: String): Boolean {
         if (this == null) return false
         val action = this.getValueArgument(0)
         if(action == null) return true
         return (action is IrVarargImpl &&
                 action.elements.isNotEmpty() &&
                 (action.elements.first() is IrGetEnumValueImpl) &&
                 (action.elements.first() as IrGetEnumValueImpl).symbol.owner.name.asString() == name.uppercase()
                 )
     }
 
     fun IrClass.isKPojo(): Boolean {
         with(pluginContext) {
             with(builder) {
                 return superTypes.any { it.classFqName == KPojoFqName }
             }
         }
     }
 
     fun IrExpression.funcName(setNot: Boolean = false) = pluginContext.withContext{
         funcName(setNot = setNot)
     }
 
     /**
      * Retrieves the KDoc string for the current IR declaration.
      *
      * This function attempts to extract the KDoc comment associated with the current IR declaration.
      * It uses the source offsets to locate the relevant lines in the source file and then extracts
      * the comment content.
      *
      * @receiver The IR declaration for which to retrieve the KDoc string.
      * @return An IR expression containing the KDoc string, or null if no KDoc comment is found.
      */
     fun IrDeclaration.getKDocString(): IrExpression {
         val declaration = this
         with(pluginContext){
             with(builder){
                 val sourceOffsets = sourceElement()
                 if (sourceOffsets != null) {
                     val startOffset = sourceOffsets.startOffset
                     val endOffset = sourceOffsets.endOffset
                     val fileEntry = file.fileEntry
                     val sourceRange = fileEntry.getSourceRangeInfo(startOffset, endOffset)
                     val source = sourceFileCache.getOrPut(fileEntry.name) {
                         File(sourceRange.filePath).readLines(UTF_8)
                     }
                     val realStartOffset = realStartOffset(source, sourceRange.startLineNumber)
                     val comment = when (declaration) {
                             is IrProperty -> extractDeclarationComment(
                                 source,
                                 realStartOffset..sourceRange.endLineNumber
                             )
 
                             is IrClass -> extractDeclarationComment(
                                 source,
                                 sourceRange.startLineNumber..realStartOffset
                             )
 
                             else -> null
                         }
 
                     if (comment != null) {
                         return irString(comment)
                     }
                 }
                 return irString("")
             }
         }
     }
 
     internal fun IrSimpleFunctionSymbol.invoke(
         vararg values: IrExpression?,
         typeArguments: Array<IrType> = emptyArray(),
         setReceivers: Receivers.() -> Unit = { }
     ): IrFunctionAccessExpression {
         return builder.applyIrCall(this, *values, typeArguments = typeArguments, setReceivers = setReceivers)
     }
 
     fun IrExpression?.irFieldOrNull(): IrExpression {
         return if (this != null && this.isKPojo()) getColumnName(this) else builder.irNull()
     }
 
     /**
      * Converts the current object to an IrVariable by creating a criteria using the provided propertyeters.
      *
      * @return an IrVariable representing the created criteria
      */
     @OptIn(UnsafeDuringIrConstructionAPI::class)
     fun FieldIR.build(): IrExpression {
         with(pluginContext){
             with(builder){
                 return applyIrCall(
                     fieldSymbol.constructors.first(),
                     columnName,
                     irString(name),
                     type,
                     irEnum(primaryKeyTypeSymbol, primaryKey),
                     dateTimeFormat ?: irNull(),
                     tableName,
                     cascade,
                     irBoolean(cascadeIsArrayOrCollection),
                     kClass,
                     irListOf(irBuiltIns.stringType, superTypes),
                     ignore ?: irNull(),
                     irBoolean(isColumn),
                     columnTypeLength ?: irInt(0),
                     columnTypeScale ?: irInt(0),
                     columnDefaultValue ?: irNull(),
                     irBoolean(nullable),
                     irBoolean(serializable),
                     kDoc
                 )
             }
         }
     }
 }
 
 typealias KotlinBuilderContext = KotlinBuilderWithScopeContext<IrBuilderWithScope>
 
 class KotlinBlockBuilderContext(
     override var pluginContext: IrPluginContext,
     override var builder: IrBlockBuilder
 ) : KotlinBuilderWithScopeContext<IrBlockBuilder>(pluginContext, builder){
     /**
      * Converts the current object to an IrVariable by creating a criteria using the provided parameters.
      *
      * @return an IrVariable representing the created criteria
      */
     fun CriteriaIR.toIrVariable(): IrVariable {
         return createCriteria(parameterName, type, not, value, children, tableName, noValueStrategyType)
     }
 }
 
 fun <R> IrBuilderWithScope.withBuilder(
     pluginContext: IrPluginContext,
     action: KotlinBuilderContext.() -> R
 ): R = action(
     KotlinBuilderContext(pluginContext, this)
 )
 
 fun <R> IrBlockBuilder.withBlock(
     pluginContext: IrPluginContext,
     action: KotlinBlockBuilderContext.() -> R
 ): R = action(
     KotlinBlockBuilderContext(pluginContext, this)
 )