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

org.jetbrains.kotlin.analysis.api.symbols.DebugSymbolRenderer.kt Maven / Gradle / Ivy

/*
 * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.analysis.api.symbols

import com.intellij.psi.PsiElement
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.analysis.api.*
import org.jetbrains.kotlin.analysis.api.annotations.*
import org.jetbrains.kotlin.analysis.api.base.KaContextReceiver
import org.jetbrains.kotlin.analysis.api.contracts.description.Context
import org.jetbrains.kotlin.analysis.api.contracts.description.KaContractEffectDeclaration
import org.jetbrains.kotlin.analysis.api.contracts.description.renderKaContractEffectDeclaration
import org.jetbrains.kotlin.analysis.api.projectStructure.*
import org.jetbrains.kotlin.analysis.api.symbols.markers.KaNamedSymbol
import org.jetbrains.kotlin.analysis.api.types.*
import org.jetbrains.kotlin.analysis.api.types.KaStarTypeProjection
import org.jetbrains.kotlin.analysis.api.types.KaTypeArgumentWithVariance
import org.jetbrains.kotlin.analysis.api.types.KaTypeProjection
import org.jetbrains.kotlin.analysis.api.utils.getApiKClassOf
import org.jetbrains.kotlin.analysis.utils.printer.PrettyPrinter
import org.jetbrains.kotlin.analysis.utils.printer.prettyPrint
import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.resolve.deprecation.DeprecationInfo
import org.jetbrains.kotlin.types.Variance
import java.lang.reflect.InvocationTargetException
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.KVisibility
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.isAccessible

@KaNonPublicApi
@OptIn(KaExperimentalApi::class, KaImplementationDetail::class)
public class DebugSymbolRenderer(
    public val renderExtra: Boolean = false,
    public val renderTypeByProperties: Boolean = false,
    public val renderExpandedTypes: Boolean = false,
) {

    public fun render(analysisSession: KaSession, symbol: KaSymbol): String = prettyPrint {
        analysisSession.renderSymbol(symbol, this, linkedSetOf())
    }

    public fun renderAnnotationApplication(analysisSession: KaSession, application: KaAnnotation): String = prettyPrint {
        analysisSession.renderAnnotationApplication(application, this, linkedSetOf())
    }

    public fun renderType(analysisSession: KaSession, type: KaType): String = prettyPrint {
        analysisSession.renderType(type, this, linkedSetOf())
    }

    @TestOnly
    public fun renderForSubstitutionOverrideUnwrappingTest(analysisSession: KaSession, symbol: KaSymbol): String = prettyPrint {
        analysisSession.renderForSubstitutionOverrideUnwrappingTest(symbol, this, linkedSetOf())
    }

    private fun KaSession.renderSymbol(symbol: KaSymbol, printer: PrettyPrinter, currentSymbolStack: LinkedHashSet) {
        currentSymbolStack += symbol
        try {
            renderSymbolInternals(symbol, printer, currentSymbolStack)

            if (!renderExtra) return
            printer.withIndent {
                @Suppress("DEPRECATION")
                (symbol as? KaCallableSymbol)?.dispatchReceiverType?.let { dispatchType ->
                    appendLine().append("getDispatchReceiver()").append(": ")
                    renderType(dispatchType, printer, currentSymbolStack)
                }

                renderComputedValue("getContainingFileSymbol", printer, currentSymbolStack) { symbol.containingFile }

                if (symbol is KaCallableSymbol) {
                    renderComputedValue("getContainingJvmClassName", printer, currentSymbolStack) { symbol.containingJvmClassName }
                }

                renderComputedValue("getContainingModule", printer, currentSymbolStack) { symbol.containingModule }

                if (symbol is KaClassSymbol) {
                    renderComputedValue("annotationApplicableTargets", printer, currentSymbolStack) { symbol.annotationApplicableTargets }
                }

                renderComputedValue("deprecationStatus", printer, currentSymbolStack) { symbol.deprecationStatus }

                if (symbol is KaPropertySymbol) {
                    renderComputedValue("getterDeprecationStatus", printer, currentSymbolStack) { symbol.getterDeprecationStatus }
                    renderComputedValue("javaGetterName", printer, currentSymbolStack) { symbol.javaGetterName }
                    renderComputedValue("javaSetterName", printer, currentSymbolStack) { symbol.javaSetterName }
                    renderComputedValue("setterDeprecationStatus", printer, currentSymbolStack) { symbol.setterDeprecationStatus }
                }
            }
        } finally {
            currentSymbolStack -= symbol
        }
    }

    private fun KaSession.renderForSubstitutionOverrideUnwrappingTest(
        symbol: KaSymbol,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ): Unit = with(printer) {
        if (symbol !is KaCallableSymbol) return

        renderFrontendIndependentKClassNameOf(symbol, printer)

        withIndent {
            appendLine()
            renderProperty(KaCallableSymbol::callableId, printer, renderSymbolsFully = false, currentSymbolStack, symbol)
            if (symbol is KaNamedSymbol) {
                appendLine()
                renderProperty(KaNamedSymbol::name, printer, renderSymbolsFully = false, currentSymbolStack, symbol)
            }
            appendLine()
            renderProperty(KaCallableSymbol::origin, printer, renderSymbolsFully = false, currentSymbolStack, symbol)
        }
    }

    private fun KaSession.renderComputedValue(
        name: String,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
        block: () -> Any?,
    ) {
        printer.appendLine()
        printer.append(name).append(": ")

        val value = try {
            block()
        } catch (e: Throwable) {
            printer.append("Could not render due to ").appendLine(e.toString())
            return
        }

        renderValue(value, printer, renderSymbolsFully = false, currentSymbolStack)
    }

    private fun KaSession.renderProperty(
        property: KProperty<*>,
        printer: PrettyPrinter,
        renderSymbolsFully: Boolean,
        currentSymbolStack: LinkedHashSet,
        vararg args: Any,
    ) {
        printer.append(property.name).append(": ")
        renderFunctionCall(property.getter, printer, renderSymbolsFully, currentSymbolStack, args)
    }

    private fun KaSession.renderFunctionCall(
        function: KFunction<*>,
        printer: PrettyPrinter,
        renderSymbolsFully: Boolean,
        currentSymbolStack: LinkedHashSet,
        args: Array,
    ) {
        try {
            function.isAccessible = true
            renderValue(function.call(*args), printer, renderSymbolsFully, currentSymbolStack)
        } catch (e: InvocationTargetException) {
            printer.append("Could not render due to ").appendLine(e.cause.toString())
        }
    }

    private fun KaSession.renderSymbolInternals(symbol: KaSymbol, printer: PrettyPrinter, currentSymbolStack: LinkedHashSet) {
        renderFrontendIndependentKClassNameOf(symbol, printer)
        val apiClass = getApiKClassOf(symbol)
        printer.withIndent {
            val members = apiClass.members
                .filterIsInstance>()
                .filter { !it.hasAnnotation() && it.name !in ignoredPropertyNames }
                .sortedBy { it.name }
            appendLine()
            printCollectionIfNotEmpty(members, separator = "\n") { member ->
                val renderSymbolsFully = member.name == KaValueParameterSymbol::generatedPrimaryConstructorProperty.name
                renderProperty(member, printer, renderSymbolsFully, currentSymbolStack, symbol)
            }
        }
    }

    private fun renderFrontendIndependentKClassNameOf(instanceOfClassToRender: Any, printer: PrettyPrinter) {
        val apiClass = getApiKClassOf(instanceOfClassToRender)
        printer.append(apiClass.simpleName).append(':')
    }

    private fun KaSession.renderList(
        values: List<*>,
        printer: PrettyPrinter,
        renderSymbolsFully: Boolean,
        currentSymbolStack: LinkedHashSet,
    ) {
        if (values.isEmpty()) {
            printer.append("[]")
            return
        }

        printer.withIndentInSquareBrackets {
            printCollection(values, separator = "\n") { renderValue(it, printer, renderSymbolsFully, currentSymbolStack) }
        }
    }

    private fun KaSession.renderSymbolTag(
        symbol: KaSymbol,
        printer: PrettyPrinter,
        renderSymbolsFully: Boolean,
        currentSymbolStack: LinkedHashSet,
    ) {
        fun renderId(id: Any?, symbol: KaSymbol) {
            if (id != null) {
                renderValue(id, printer, renderSymbolsFully, currentSymbolStack)
            } else {
                val outerName = symbol.name ?: SpecialNames.NO_NAME_PROVIDED
                printer.append("/" + outerName.asString())
            }
        }

        if (symbol !in currentSymbolStack && (renderSymbolsFully || symbol is KaBackingFieldSymbol || symbol is KaPropertyAccessorSymbol || symbol is KaParameterSymbol)) {
            renderSymbol(symbol, printer, currentSymbolStack)
            return
        }

        with(printer) {
            append(getApiKClassOf(symbol).simpleName)
            append("(")
            when (symbol) {
                is KaClassLikeSymbol -> renderId(symbol.classId, symbol)
                is KaCallableSymbol -> renderId(symbol.callableId, symbol)
                is KaNamedSymbol -> renderValue(symbol.name, printer, renderSymbolsFully = false, currentSymbolStack)
                is KaFileSymbol -> renderValue((symbol.psi as KtFile).name, printer, renderSymbolsFully = false, currentSymbolStack)
                else -> error("Unsupported symbol ${symbol::class}")
            }
            append(")")
        }
    }

    private fun renderAnnotationValue(value: KaAnnotationValue, printer: PrettyPrinter) {
        printer.append(KaAnnotationValueRenderer.render(value))
    }

    private fun KaSession.renderNamedConstantValue(
        value: KaNamedAnnotationValue,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        printer.append(value.name.render()).append(" = ")
        renderValue(value.expression, printer, renderSymbolsFully = false, currentSymbolStack)
    }

    private fun KaSession.renderType(type: KaType, printer: PrettyPrinter, currentSymbolStack: LinkedHashSet) {
        val typeToRender = if (renderExpandedTypes) type.fullyExpandedType else type

        renderFrontendIndependentKClassNameOf(typeToRender, printer)
        printer.withIndent {
            appendLine()
            if (renderTypeByProperties) {
                renderByPropertyNames(typeToRender, printer, currentSymbolStack)
            } else {
                append("annotations: ")
                renderAnnotationsList(typeToRender.annotations, printer, currentSymbolStack)

                if (typeToRender is KaClassType) {
                    appendLine()
                    append("typeArguments: ")
                    renderList(typeToRender.typeArguments, printer, renderSymbolsFully = false, currentSymbolStack)
                }

                appendLine()
                append("type: ")
                when (typeToRender) {
                    is KaErrorType -> append("ERROR_TYPE")
                    else -> append(typeToRender.toString())
                }
            }
        }
    }

    private fun KaSession.renderByPropertyNames(value: Any, printer: PrettyPrinter, currentSymbolStack: LinkedHashSet) {
        val members = value::class.members
            .filter { it.name !in ignoredPropertyNames }
            .filter { it.visibility != KVisibility.PRIVATE && it.visibility != KVisibility.INTERNAL }
            .filter { !it.hasAnnotation() }
            .sortedBy { it.name }
            .filterIsInstance>()

        printer.printCollectionIfNotEmpty(members, separator = "\n") { member ->
            renderProperty(member, printer, renderSymbolsFully = false, currentSymbolStack, value)
        }
    }

    private fun KaSession.renderAnnotationApplication(
        call: KaAnnotation,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        with(printer) {
            renderValue(call.classId, printer, renderSymbolsFully = false, currentSymbolStack)
            append('(')
            call.arguments.sortedBy { it.name }.forEachIndexed { index, value ->
                if (index > 0) {
                    append(", ")
                }
                renderValue(value, printer, renderSymbolsFully = false, currentSymbolStack)
            }
            append(')')

            withIndent {
                appendLine().append("psi: ")
                val psi =
                    if (call.psi?.containingKtFile?.isCompiled == true) {
                        null
                    } else call.psi
                renderValue(psi?.javaClass?.simpleName, printer, renderSymbolsFully = false, currentSymbolStack)
            }
        }
    }

    private fun renderDeprecationInfo(info: DeprecationInfo, printer: PrettyPrinter) {
        with(printer) {
            append("DeprecationInfo(")
            append("deprecationLevel=${info.deprecationLevel}, ")
            append("propagatesToOverrides=${info.propagatesToOverrides}, ")
            append("message=${info.message}")
            append(")")
        }
    }

    private fun KaSession.renderValue(
        value: Any?,
        printer: PrettyPrinter,
        renderSymbolsFully: Boolean,
        currentSymbolStack: LinkedHashSet,
    ) {
        when (value) {
            // Symbol-related values
            is KaSymbol -> renderSymbolTag(value, printer, renderSymbolsFully, currentSymbolStack)
            is KaType -> renderType(value, printer, currentSymbolStack)
            is KaTypeProjection -> renderTypeProjection(value, printer, currentSymbolStack)
            is KaClassTypeQualifier -> renderTypeQualifier(value, printer, currentSymbolStack)
            is KaAnnotationValue -> renderAnnotationValue(value, printer)
            is KaContractEffectDeclaration -> Context(this@KaSession, printer, this@DebugSymbolRenderer)
                .renderKaContractEffectDeclaration(value, endWithNewLine = false)
            is KaNamedAnnotationValue -> renderNamedConstantValue(value, printer, currentSymbolStack)
            is KaInitializerValue -> renderKtInitializerValue(value, printer)
            is KaContextReceiver -> renderContextReceiver(value, printer, currentSymbolStack)
            is KaAnnotation -> renderAnnotationApplication(value, printer, currentSymbolStack)
            is KaAnnotationList -> renderAnnotationsList(value, printer, currentSymbolStack)
            is KaModule -> renderModule(value, printer)
            // Other custom values
            is Name -> printer.append(value.asString())
            is FqName -> printer.append(value.asString())
            is ClassId -> printer.append(value.asString())
            is DeprecationInfo -> renderDeprecationInfo(value, printer)
            is Visibility -> printer.append(value::class.java.simpleName)
            // Unsigned integers
            is UByte -> printer.append(value.toString())
            is UShort -> printer.append(value.toString())
            is UInt -> printer.append(value.toString())
            is ULong -> printer.append(value.toString())
            // Java values
            is Enum<*> -> printer.append(value.name)
            is List<*> -> renderList(value, printer, renderSymbolsFully = false, currentSymbolStack)
            else -> printer.append(value.toString())
        }
    }

    private fun KaSession.renderTypeProjection(
        value: KaTypeProjection,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        when (value) {
            is KaStarTypeProjection -> printer.append("*")
            is KaTypeArgumentWithVariance -> {
                if (value.variance != Variance.INVARIANT) {
                    printer.append("${value.variance.label} ")
                }
                renderType(value.type, printer, currentSymbolStack)
            }
        }
    }

    private fun KaSession.renderTypeQualifier(
        value: KaClassTypeQualifier,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        with(printer) {
            appendLine("qualifier:")
            withIndent {
                renderByPropertyNames(value, printer, currentSymbolStack)
            }
        }
    }

    private fun KaSession.renderContextReceiver(
        receiver: KaContextReceiver,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        with(printer) {
            append("KtContextReceiver:")
            withIndent {
                appendLine()
                append("label: ")
                renderValue(receiver.label, printer, renderSymbolsFully = false, currentSymbolStack)
                appendLine()
                append("type: ")
                renderType(receiver.type, printer, currentSymbolStack)
            }
        }
    }

    @OptIn(KaExperimentalApi::class)
    private fun renderModule(module: KaModule, printer: PrettyPrinter) {
        val apiClass = when (val moduleClass = module::class) {
            in kaModuleApiSubclasses -> moduleClass
            else -> moduleClass.allSuperclasses.first { it in kaModuleApiSubclasses }
        }
        printer.append(apiClass.simpleName + " \"" + module.moduleDescription + "\"")
    }

    private fun KClass<*>.allSealedSubClasses(): List> = buildList {
        add(this@allSealedSubClasses)
        sealedSubclasses.flatMapTo(this) { it.allSealedSubClasses() }
    }

    /**
     * All [KaModule] classes which are part of the API (defined in `KaModule.kt`) and should be printed in test data.
     */
    @OptIn(KaPlatformInterface::class, KaExperimentalApi::class)
    private val kaModuleApiSubclasses =
        listOf(
            KaModule::class,
            KaSourceModule::class,
            KaLibraryModule::class,
            KaLibrarySourceModule::class,
            KaBuiltinsModule::class,
            KaScriptModule::class,
            KaScriptDependencyModule::class,
            KaDanglingFileModule::class,
            KaNotUnderContentRootModule::class,
        ).sortedWith { a, b ->
            when {
                a == b -> 0
                a.isSubclassOf(b) -> -1
                b.isSubclassOf(a) -> 1
                else -> 0
            }
        }

    private fun renderKtInitializerValue(value: KaInitializerValue, printer: PrettyPrinter) {
        with(printer) {
            when (value) {
                is KaConstantInitializerValue -> {
                    append("KtConstantInitializerValue(")
                    append(value.constant.render())
                    append(")")
                }

                is KaNonConstantInitializerValue -> {
                    append("KtNonConstantInitializerValue(")
                    append(value.initializerPsi?.firstLineOfPsi() ?: "NO_PSI")
                    append(")")
                }

                is KaConstantValueForAnnotation -> {
                    append("KtConstantValueForAnnotation(")
                    append(value.annotationValue.renderAsSourceCode())
                    append(")")
                }
            }
        }
    }

    private fun KaSession.renderAnnotationsList(
        value: KaAnnotationList,
        printer: PrettyPrinter,
        currentSymbolStack: LinkedHashSet,
    ) {
        renderList(value, printer, renderSymbolsFully = false, currentSymbolStack)
    }

    private fun PsiElement.firstLineOfPsi(): String {
        val text = text
        val lines = text.lines()
        return if (lines.count() <= 1) text
        else lines.first() + " ..."
    }

    @KaNonPublicApi
    public companion object {
        private val ignoredPropertyNames = setOf(
            "psi",
            "token",
            "builder",
            "coneType",
            "analysisContext",
            "fe10Type",

            // These properties are made obsolete by their counterparts without `*IfNonLocal` (e.g. `classId`), which contain the same
            // values.
            "classIdIfNonLocal",
            "containingClassIdIfNonLocal",
            "callableIdIfNonLocal",
        )
    }
}

private val PrettyPrinter.printer: PrettyPrinter
    get() = this




© 2015 - 2025 Weber Informatics LLC | Privacy Policy