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

app.softwork.validation.plugin.kotlin.ValidationTransformer.kt Maven / Gradle / Ivy

package app.softwork.validation.plugin.kotlin

import org.jetbrains.kotlin.backend.common.extensions.*
import org.jetbrains.kotlin.backend.common.lower.*
import org.jetbrains.kotlin.fir.extensions.*
import org.jetbrains.kotlin.ir.*
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.symbols.impl.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.*
import org.jetbrains.kotlin.name.*

internal class ValidationTransformer(
    private val pluginContext: IrPluginContext,
    private val dump: ((String) -> Unit)?,
) : IrElementTransformerVoid() {

    private val MinLength = AnnotationFqn("app.softwork.validation.MinLength")
    private val MaxLength = AnnotationFqn("app.softwork.validation.MaxLength")
    private val validationExceptionSymbol: IrClassSymbol? =
        pluginContext.referenceClass(ClassId(FqName("app.softwork.validation"), FqName("ValidationException"), false))
    private val unit = pluginContext.symbols.irBuiltIns.unitClass
    private val unitType = pluginContext.symbols.irBuiltIns.unitType
    private val booleanType = pluginContext.symbols.irBuiltIns.booleanType
    private val less =
        pluginContext.symbols.irBuiltIns.lessFunByOperandType[pluginContext.symbols.irBuiltIns.intClass]!!
    private val greater =
        pluginContext.symbols.irBuiltIns.greaterFunByOperandType[pluginContext.symbols.irBuiltIns.intClass]!!
    private val STRINGlength = pluginContext.irBuiltIns.stringClass.getPropertyGetter("length")!!

    private var newInitBlock: IrAnonymousInitializer? = null

    override fun visitClass(declaration: IrClass): IrStatement {

        val newInitBlock: IrAnonymousInitializer = declaration.factory.createAnonymousInitializer(
            startOffset = UNDEFINED_OFFSET,
            endOffset = UNDEFINED_OFFSET,
            origin = IrDeclarationOrigin.DEFINED,
            symbol = IrAnonymousInitializerSymbolImpl(declaration.symbol),
        )
        newInitBlock.parent = declaration
        newInitBlock.body = declaration.factory.createBlockBody(
            startOffset = UNDEFINED_OFFSET,
            endOffset = UNDEFINED_OFFSET,
        )
        this.newInitBlock = newInitBlock

        return declaration.transformPostfix {
            if (newInitBlock.body.statements.isNotEmpty()) {
                val dump = dump
                if (dump != null) {
                    for (stmt in newInitBlock.body.statements) {
                        dump(stmt.dump())
                    }
                }
                // move generated checks before user init code
                val previousInitBlocks = declarations.indexOfFirst {
                    it is IrAnonymousInitializer
                }.takeUnless { it == -1 }
                if (previousInitBlocks == null) {
                    addMember(newInitBlock)
                } else {
                    declarations.add(previousInitBlocks, newInitBlock)
                }
            }
            [email protected] = null
        }
    }

    override fun visitProperty(declaration: IrProperty): IrProperty {
        val type = declaration.getter!!.returnType
        if (!type.isStringClassType()) {
            return declaration
        }
        declaration.getAnnotation(MinLength)?.addInit(
            declaration = declaration,
            declarationName = declaration.name.asString(),
            comp = less,
            compHuman = ">=",
            originParam = IrStatementOrigin.LT
        )
        declaration.getAnnotation(MaxLength)?.addInit(
            declaration = declaration,
            declarationName = declaration.name.asString(),
            comp = greater,
            compHuman = "<=",
            originParam = IrStatementOrigin.GT,
        )

        return declaration
    }

    private fun IrConstructorCall.addInit(
        declaration: IrProperty,
        declarationName: String,
        comp: IrSimpleFunctionSymbol,
        compHuman: String,
        originParam: IrStatementOrigin,
    ) {
        val value = getValueArgument(0)!!
        val klass = declaration.parentClassOrNull ?: return

        with(
            IrBlockBodyBuilder(
                context = pluginContext,
                scope = Scope(unit),
                startOffset = startOffset,
                endOffset = endOffset,
            )
        ) {
            val prop = irCall(
                declaration.getter!!
            ).apply {
                dispatchReceiver = irGet(klass.thisReceiver!!)
                origin = IrStatementOrigin.GET_PROPERTY
            }
            val isNullable = declaration.getter!!.returnType.isNullable()
            val checkLength = irCall(comp).apply {
                putValueArgument(0, irCall(STRINGlength).apply {
                    dispatchReceiver = prop
                    origin = IrStatementOrigin.GET_PROPERTY
                })
                putValueArgument(1, value)
                origin = originParam
            }
            val newInitBlock = newInitBlock ?: return@with
            newInitBlock.body.statements += irIfThen(
                type = unitType,
                condition = if (isNullable) {
                    irIfThen(
                        type = booleanType,
                        condition = irNot(
                            irEqualsNull(
                                prop
                            ),
                        ),
                        thenPart = checkLength
                    ).apply {
                        origin = IrStatementOrigin.ANDAND
                        branches.add(irBranch(irTrue(), irFalse()))
                    }
                } else checkLength,
                thenPart = irThrow(irCallConstructor(
                    validationExceptionSymbol!!.constructors.first {
                        if (it.owner.valueParameters.size == 1) {
                            val singleParameter = it.owner.valueParameters.single()
                            singleParameter.type.isString()
                        } else false
                    },
                    emptyList(),
                ).apply {
                    putValueArgument(0, irConcat().apply {
                        arguments += irString("$declarationName.length $compHuman ")
                        arguments += value
                        arguments += irString(", was ")
                        arguments += prop
                    })
                },
            )).apply {
                origin = IrStatementOrigin.IF
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy