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

jvmMain.com.caesarealabs.rpc4k.processor.utils.KspUtils.kt Maven / Gradle / Ivy

The newest version!
package com.caesarealabs.rpc4k.processor.utils

import com.google.devtools.ksp.*
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.*
import com.caesarealabs.rpc4k.runtime.implementation.KotlinClassName
import kotlin.reflect.KClass

/**
 * Asserts that only classes have this annotation. Make sure the annotation is declared to only support class use site.
 */
@Suppress("UNCHECKED_CAST")
internal fun Resolver.getClassesWithAnnotation(annotation: KClass<*>): Sequence = getSymbolsWithAnnotation(annotation.qualifiedName!!)
        as Sequence


internal fun KSClassDeclaration.getPublicApiFunctions() = getDeclaredFunctions()
    .filter { !it.isConstructor() && it.isPublic() }

/**
 * Only checks the short name of annotations for performance, may not work well with annotation name conflicts.
 */
internal fun KSAnnotated.hasAnnotation(annotation: KClass<*>) = annotations.any { it.shortName.asString() == annotation.simpleName }


/**
 * Extract from `com.foo.bar.Inner$Thing` the pair `[com.foo.bar, inner.Thing]`
 */
internal fun KSDeclaration.getKotlinName(): KotlinClassName? {
    val qualifiedName = getQualifiedName()
    val packageName = packageName.asString()
    val className = qualifiedName?.removePrefix("$packageName.") ?: return null
    return KotlinClassName(pkg = packageName, simple = className)
}

/**
 * Extract from `com.foo.bar.Inner$Thing` , `Inner.Thing`
 */
internal fun KSDeclaration.getSimpleName(): String? = if (this is KSClassDeclaration) {
    val qualifiedName = getQualifiedName()
    val packageName = packageName.asString()
    qualifiedName?.removePrefix("$packageName.")
} else {
    getTopLevelSimpleName()
}

/**
 * Extract from `com.foo.bar.Inner$Thing` , `Thing`
 */
internal fun KSDeclaration.getTopLevelSimpleName(): String = simpleName.asString()


/**
 * Will mark the [KSNode] itself as the cause of the failure if this check fails
 * Returns [requirement]
 */
internal inline fun KSNode.checkRequirement(environment: SymbolProcessorEnvironment, requirement: Boolean, msg: () -> String): Boolean {
    if (!requirement) environment.logger.error(msg(), this)
    return requirement
}

internal fun KSTypeArgument.nonNullType() =
    type
        ?: error(
            "There's no reason why a type of a type argument would be null. If you encounter this error, open a bug report ASAP! This happened for '$this'."
        )

internal fun KSDeclaration.getQualifiedName() =
    qualifiedName?.asString()
//        ?: error(
//            "There's no reason why the qualified name of a type would be null. If you encounter this error, open a bug report ASAP! This happened for '$this'."
//        )

internal fun KSFunctionDeclaration.nonNullReturnType() =
    returnType
        ?: error(
            "There's no reason why the return type of a function would be null. If you encounter this error, open a bug report ASAP! This happened for '$this'."
        )

/**
 * Handles type aliases as well
 */
internal fun KSTypeReference.resolveToUnderlying(): KSType {
    var candidate = resolve()
    var declaration = candidate.declaration
    while (declaration is KSTypeAlias) {
        candidate = declaration.type.resolve()
        declaration = candidate.declaration
    }
    return candidate
}


internal fun CodeGenerator.writeFile(
    contents: String,
    dependencies: Dependencies,
    path: String,
    extensionName: String = "kt"
) {
    createNewFileByPath(dependencies, path, extensionName).use { it.write(contents.toByteArray()) }
}

/**
 * When you have methods like
 * ```
 * fun foo(param: SomeClass)
 * ```
 *
 * It will return everything referenced by `SomeClass` and other such referenced classes.
 *
 * @param filter Allows denying certain classes, which will make it so things they have referenced will not get visited.
 */
internal fun KSClassDeclaration.getReferencedClasses(resolver: Resolver, filter: (KSTypeReference) -> Boolean = { true }): Set {
    val types = hashSetOf()
    // Add things referenced in methods
    for (method in getPublicApiFunctions()) {
        addReferencedTypes(method.nonNullReturnType(), types, resolver, filter)
        for (arg in method.parameters) {
            addReferencedTypes(arg.type, types, resolver, filter)
        }
    }

    return types
}


/**
 * When you have a reference like
 * ```
 * class SomeClass {
 *     val anotherThing: AnotherClass
 * }
 * ```
 * It will return everything referenced by `SomeClass`, including `SomeOtherClass` and `AnotherClass`.
 */
private fun addReferencedTypes(
    type: KSTypeReference,
    addTo: MutableSet,
    resolver: Resolver,
    filter: (KSTypeReference) -> Boolean
) {
    if (!filter(type)) return
    val resolved = type.resolveToUnderlying()
    for (typeArg in resolved.arguments) {
        if (typeArg.type != null) addReferencedTypes(typeArg.type!!, addTo, resolver, filter)
    }
    val declaration = resolved.declaration
    // We really don't want to iterate over builtin types, and we need to be careful to only process everything once or this will be infinite recursion.
    if (!resolved.isBuiltinSerializableType() && declaration !in addTo && declaration is KSClassDeclaration) {
        for (arg in resolved.arguments) {
            addReferencedTypes(arg.nonNullType(), addTo, resolver, filter)
        }
        addReferencedTypes(declaration, addTo, resolver, filter)
    }
}

private fun addReferencedTypes(
    declaration: KSClassDeclaration,
    addTo: MutableSet,
    resolver: Resolver,
    filter: (KSTypeReference) -> Boolean
) {
    addTo.add(declaration)
    // Include types referenced in properties of models as well
    for (property in declaration.getAllProperties()) {
        addReferencedTypes(property.type, addTo, resolver, filter)
    }
    // Add sealed subclasses as well
    for (sealedSubClass in declaration.fastGetSealedSubclasses(resolver)) {
        addReferencedTypes(sealedSubClass, addTo, resolver, filter)
    }
}


/**
 * For some reason, when you call getSealedSubclasses it invalidates the ksp caches and basically forces ksp to reprocess everything every time.
 * This is a known limitation and may be fixed in KSP 2.
 * This version does not cause the cache to be invalidated.
 *
 */
@OptIn(KspExperimental::class) internal fun KSClassDeclaration.fastGetSealedSubclasses(resolver: Resolver): Sequence {
    if (Modifier.SEALED !in modifiers) return emptySequence()
    val packageName = containingFile?.packageName ?: return emptySequence()

    val filesInSamePackage = resolver.getDeclarationsFromPackage(packageName.asString())
    return filesInSamePackage.filterIsInstance()
        .flatMap { it.getAllDeclarations() }
        .filterIsInstance()
        .filter { superType -> superType.getAllSuperTypes().any { it.declaration == this } }
}



private fun KSClassDeclaration.getAllDeclarations(): Sequence {
    return sequence {
        yield(this@getAllDeclarations)
        for (declaration in declarations) {
            if (declaration is KSClassDeclaration) {
                yieldAll(declaration.getAllDeclarations())
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy