Current scope: kronos-compiler-plugin| all classes
|
com.kotlinorm.compiler.plugin.utils.kTableForCondition
Coverage Summary for Class: KTableForConditionUtilKt (com.kotlinorm.compiler.plugin.utils.kTableForCondition)
Class | Class, % | Method, % | Branch, % | Line, % | Instruction, % |
---|---|---|---|---|---|
KTableForConditionUtilKt | 100% (1/1) | 81.2% (13/16) | 56.4% (149/264) | 75.4% (178/236) | 75.6% (1536/2031) |
/**
* 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.kTableForCondition
import com.kotlinorm.compiler.helpers.dispatchReceiverArgument
import com.kotlinorm.compiler.helpers.extensionReceiver
import com.kotlinorm.compiler.helpers.extensionReceiverArgument
import com.kotlinorm.compiler.helpers.invoke
import com.kotlinorm.compiler.helpers.irCast
import com.kotlinorm.compiler.helpers.sub
import com.kotlinorm.compiler.helpers.valueArguments
import com.kotlinorm.compiler.plugin.beans.CriteriaIR
import com.kotlinorm.compiler.plugin.utils.ARRAY_OR_COLLECTION_FQ_NAMES
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.getColumnOrValue
import com.kotlinorm.compiler.plugin.utils.getTableName
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.builders.IrBlockBuilder
import org.jetbrains.kotlin.ir.builders.irGet
import org.jetbrains.kotlin.ir.builders.irString
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrFunction
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.IrBlockBody
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.IrPropertyReference
import org.jetbrains.kotlin.ir.expressions.IrReturn
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.expressions.IrWhen
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrWhenImpl
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.fqNameWhenAvailable
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.util.kotlinFqName
import org.jetbrains.kotlin.ir.util.properties
import org.jetbrains.kotlin.ir.util.superTypes
import org.jetbrains.kotlin.name.FqName
/**
* Generates IR for setting simple criteria.
*
* This function applies an IR call to set the criteria using the criteriaSetterSymbol.
* It takes the result of building the criteria using the body of the current function.
* The criteria are then dispatched by getting the extension receiver parameter.
*
* @return the IR expression for setting the criteria
* @author OUSC
*/
context(_: IrPluginContext, builder: IrBlockBuilder)
fun updateCriteriaIr(irFunction: IrFunction) = criteriaSetterSymbol(
builder.irGet(irFunction.parameters.extensionReceiver!!),
builder.irGet(buildCriteria(irFunction, irFunction.body!!)!!)
)
/**
* Builds a criteria IR variable based on the given element.
*
* This function recursively builds a criteria IR variable based on the given element and its children.
* It handles different types of elements such as IrBlockBody, IrIfThenElseImpl, IrCall, IrReturn, and IrConstImpl.
* The criteria are built based on the function name and the value arguments of the IrCall element.
* The criteria type and not flag are determined based on the function name.
* The column name, table name, value, and children are extracted from the element and its arguments.
* The criteria IR variable is then returned.
*
* @param element The element to build the criteria IR from.
* @param setNot Whether to set the not flag. Default is false.
* @param noValueStrategyType The strategy to use when there is no value. Default is null.
* @return The built criteria IR variable, or null if the element is a constant.
*/
@OptIn(UnsafeDuringIrConstructionAPI::class)
context(_: IrPluginContext, builder: IrBlockBuilder)
fun buildCriteria(
irFunction: IrFunction,
element: IrElement,
setNot: Boolean = false,
noValueStrategyType: IrExpression? = null
): IrVariable? {
with(builder) {
var paramName: IrExpression? = null
var type = "ROOT"
var not = setNot
var value: IrExpression? = null
val children: MutableList<IrVariable?> = mutableListOf()
var tableName: IrExpression? = null
var strategy = noValueStrategyType
when (element) {
is IrBlockBody -> {
element.statements.forEach { statement ->
children.add(buildCriteria(irFunction, statement))
}
}
is IrWhenImpl -> {
type = element.funcName(setNot)
element.branches.forEach {
children.add(buildCriteria(irFunction, it.condition, setNot))
children.add(buildCriteria(irFunction, it.result, setNot))
}
}
is IrCall -> {
val funcName = element.funcName()
var args = element.valueArguments
val dispatchReceiver = element.dispatchReceiverArgument
val extensionReceiver = element.extensionReceiverArgument
if (args.isEmpty() && dispatchReceiver != null && dispatchReceiver is IrCall) {
args = dispatchReceiver.arguments
}
if ("not" == funcName) {
return buildCriteria(irFunction, dispatchReceiver!!, !not)
}
val (conditionType, isNot) = parseConditionType(funcName)
type = conditionType
not = not xor isNot
when (funcName) {
"isNull", "notNull" -> {
paramName = getColumnOrValue(extensionReceiver!!)
// 形如 it.<property>.isNull的写法
// Write like it.<property>.isNull
tableName = getTableName(dispatchReceiver!!.type.sub()!!.getClass()!!)
}
"lt", "gt", "le", "ge" -> {
if (args.isEmpty()) {
// 形如it.<property>.lt的写法
// Write like it.<property>.lt with no arguments
paramName = getColumnOrValue(extensionReceiver!!)
tableName = getTableName(dispatchReceiver!!.type.sub()!!.getClass()!!)
value = getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(extensionReceiver.irCast<IrCall>().funcName())
)
} else {
// it.xxx > xx 或 xx > it.xxx
// it.xxx > xx or xx > it.xxx
val irCall = args.first()!!.irCast<IrCall>()
// 提供fun(a, b)形式和A.B.C形式的函数调用支持(!!属于fun(a, b))
// Provides support for function calls of the form fun(a, b)
// and of the form A.B.C (!!!). belongs to fun(a, b))
val (left, operator, right) = runExpressionAnalysis(
irCall.extensionReceiverArgument,
funcName,
irCall.valueArguments.firstOrNull() ?: args.getOrNull(1)
)
paramName = left
type = operator
value = right
tableName =
getTableName(irCall.findKronosColumn()!!.irCast<IrCall>().dispatchReceiverArgument!!)
}
}
"equal" -> {
not = not xor element.valueArguments.isEmpty()
val index = when {
args[0].isKPojo() || args[0].isKronosFunction() -> 0
args[1].isKPojo() || args[0].isKronosFunction() -> 1
else -> {
type = "sql"
value = element
-1
}
}
if (index != -1) {
val irCall = args[index]!!.irCast<IrCall>()
val (left, _, right) = runExpressionAnalysis(
irCall,
funcName,
args[1 - index]
)
paramName = left
value = right
tableName = getTableName(irCall.dispatchReceiverArgument!!)
}
}
"eq", "neq" -> {
if (extensionReceiver != null && extensionReceiver.isKPojo()) {
paramName = getColumnOrValue(extensionReceiver)
value = getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(
extensionReceiver.irCast<IrCall>().correspondingName!!.asString()
)
)
tableName = getTableName(extensionReceiver.irCast<IrCall>().dispatchReceiverArgument!!)
} else if (extensionReceiver != null) {
val irClass = element.extensionReceiverArgument?.type?.getClass()
fun generateEq(kPojo: IrClass, receiver: IrExpression, excludes: List<String>) {
kPojo.properties.forEach { prop ->
if (prop.isColumn() && prop.name.asString() !in excludes) {
children.add(
buildCriteria(
irFunction,
ComparableEq.getter!!.symbol(
extensionReceiver,
irGet(
prop.backingField!!.type, receiver,
prop.getter!!.symbol
)
),
setNot
)
)
}
}
}
if (irClass?.kotlinFqName == KPojoFqName) {
if (extensionReceiver is IrCallImpl && extensionReceiver.irCast<IrCall>().origin == IrStatementOrigin.MINUS) {
type = "AND"
val (kPojoClass, kPojo, excludes) = analyzeMinusExpression(extensionReceiver.irCast<IrCall>())
generateEq(kPojoClass, kPojo, excludes)
} else if (irClass.superTypes.any { it.classFqName == KPojoFqName }) {
type = "AND"
generateEq(irClass, extensionReceiver, listOf())
}
}
}
}
"between", "like", "regexp", "notBetween", "notLike", "notRegexp" -> {
paramName = getColumnOrValue(extensionReceiver!!)
value = if (args.isEmpty()) {
getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(extensionReceiver.irCast<IrCall>().correspondingName!!.asString()),
)
} else {
getColumnOrValue(args.first())
}
tableName = getTableName(element.dispatchReceiver!!.type.sub()!!.getClass()!!)
}
"startsWith" -> {
val str = if (args.isEmpty()) {
getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(extensionReceiver!!.irCast<IrCall>().correspondingName!!.asString())
)
} else {
getColumnOrValue(args.first())
}
paramName = getColumnOrValue(extensionReceiver!!)
value = stringPlusSymbol(str, irString("%"))
tableName = getTableName(element.dispatchReceiver!!.type.sub()!!.getClass()!!)
}
"endsWith" -> {
val str = if (args.isEmpty()) {
getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(extensionReceiver!!.irCast<IrCall>().correspondingName!!.asString())
)
} else {
getColumnOrValue(args.first())
}
paramName = getColumnOrValue(extensionReceiver!!)
value = stringPlusSymbol(irString("%"), str)
tableName = getTableName(dispatchReceiver!!.type.sub()!!.getClass()!!)
}
"contains" -> {
val left = extensionReceiver ?: dispatchReceiver
if (left!!.type.classFqName in ARRAY_OR_COLLECTION_FQ_NAMES || left.type.superTypes()
.any { it.classFqName in ARRAY_OR_COLLECTION_FQ_NAMES }
) {
tableName =
getTableName(args.first()!!.irCast<IrCall>().dispatchReceiver!!.type.getClass()!!)
// 形如 it.<property> in [1, 2, 3]的写法
// Write like it.<property> in listOf(1, 2, 3)
paramName = getColumnOrValue(args.first()!!)
value = left
} else {
tableName = getTableName(left.irCast<IrCall>().dispatchReceiver!!.type.getClass()!!)
type = "like"
val str = if (args.isEmpty()) {
paramName = getColumnOrValue(left)
// 形如 it.<property>.contains后面不加参数的写法
// Write it as it.<property>.contains with no arguments after it
getValueByFieldNameSymbol(
irGet(irFunction.parameters.extensionReceiver!!),
irString(left.irCast<IrCall>().correspondingName!!.asString())
)
} else {
paramName = getColumnOrValue(left)
// 形如 it.<property>.contains("xx")的写法
// Writes like it.<property>.contains("xx") or "xx" in it.<property>
getColumnOrValue(args.first())
}
value = if (str is IrConstImpl && str.value is String) {
irString("%${str.value}%")
} else {
buildContainsStrSymbol(irGet(irFunction.parameters.extensionReceiver!!), str)
}
}
}
"asSql" -> {
value = extensionReceiver
}
"ifNoValue" -> {
strategy = args.first()
return buildCriteria(irFunction, extensionReceiver!!, not, strategy)
}
}
}
is IrReturn -> {
return buildCriteria(irFunction, element.value)
}
is IrConstImpl -> {
if (element.type == context.irBuiltIns.stringType) {
type = "sql"
value = element
} else {
null
}
}
}
return CriteriaIR(
paramName, type, not, value, children.filterNotNull(), tableName, strategy
).toIrVariable()
}
}
context(_: IrPluginContext)
fun analyzeMinusExpression(irCall: IrCall): Triple<IrClass, IrExpression, List<String>> {
val (kPojo, properties) = getIrMinusParent(irCall)
return Triple(
kPojo.type.getClass()!!,
kPojo,
properties
)
}
context(_: IrPluginContext)
fun getIrMinusParent(irCall: IrCall): Pair<IrExpression, List<String>> {
val property = listOfNotNull(
irCall.valueArguments.find { it is IrCallImpl && it.origin == IrStatementOrigin.GET_PROPERTY }?.funcName()
)
val extensionReceiver = irCall.extensionReceiverArgument
val (kPojo, properties) = if (extensionReceiver is IrCallImpl) {
getIrMinusParent(extensionReceiver.irCast<IrCall>())
} else {
extensionReceiver!! to listOf()
}
return kPojo to (properties + property)
}
@OptIn(UnsafeDuringIrConstructionAPI::class)
internal val IrCall.correspondingName
get() = symbol.owner.correspondingPropertySymbol?.owner?.name
/**
* Returns a string representing the function name based on the IrExpression type and origin, with optional logic for setNot parameter.
*
* @param setNot a boolean value indicating whether to add the "not" prefix to the function name
* @return a string representing the function name
*/
@OptIn(UnsafeDuringIrConstructionAPI::class)
fun IrExpression.funcName(setNot: Boolean = false): String {
return when (this) {
is IrCall -> when (origin) {
IrStatementOrigin.EQEQ, IrStatementOrigin.EXCLEQ -> "equal"
IrStatementOrigin.GT -> "gt"
IrStatementOrigin.LT -> "lt"
IrStatementOrigin.GTEQ -> "ge"
IrStatementOrigin.LTEQ -> "le"
else -> correspondingName?.asString() ?: symbol.owner.name.asString()
}
is IrWhen -> when {
(origin == IrStatementOrigin.OROR && !setNot) || (origin == IrStatementOrigin.ANDAND && setNot) -> "OR"
(origin == IrStatementOrigin.ANDAND && !setNot) || (origin == IrStatementOrigin.OROR && setNot) -> "AND"
else -> origin.toString()
}
else -> ""
}
}
/**
* 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.
*/
context(_: IrPluginContext)
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 (extensionReceiverArgument is IrCall) {
return extensionReceiverArgument!!.findKronosColumn()
} else if (dispatchReceiverArgument is IrCall) {
return dispatchReceiverArgument!!.findKronosColumn()
} else {
for (arg in valueArguments) {
if (arg is IrCall) {
return arg.findKronosColumn()
}
}
return this
}
}
/**
* 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)
context(_: IrPluginContext)
fun IrExpression?.isKPojo(): Boolean {
if (this == null) return false
return (
this is IrCallImpl &&
this.symbol.owner.correspondingPropertySymbol?.owner is IrProperty &&
this.let {
val propertyName = 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()
)
}
context(_: IrPluginContext)
fun IrType.isKPojo(): Boolean {
return superTypes().any { it.classFqName == KPojoFqName }
}
context(_: IrPluginContext)
fun IrClass.isKPojo(): Boolean {
return superTypes.any { it.classFqName == KPojoFqName }
}
/**
* 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".
*/
context(_: IrPluginContext)
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."))
}
}
/**
* Checks if the given IrExpression is a Kronos function.
* This function checks if the IrExpression is an instance of IrCallImpl and if its extension receiver argument's type has the fully qualified name "com.kotlinorm.functions.FunctionHandler".
* "com.kotlinorm.functions.FunctionHandler" is the marker interface for all Kronos functions.
*
* @return true if the IrExpression is a Kronos function, false otherwise.
*/
context(_: IrPluginContext)
fun IrExpression?.isKronosFunction(): Boolean {
if (this == null) return false
return this is IrCallImpl && this.extensionReceiverArgument?.type?.classFqName == FqName("com.kotlinorm.functions.FunctionHandler")
}
/**
* 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.
*/
context(context: IrPluginContext)
fun IrProperty.isColumn(
irPropertyType: IrType = this.backingField?.type ?: context.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.sub()?.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 = valueArguments[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()
)
}