[LL API] Support code fragment compilation

This commit is contained in:
Yan Zhulanow
2023-06-20 01:46:50 +09:00
committed by Space Team
parent 9446118d7f
commit e1db3c88cf
126 changed files with 2439 additions and 14 deletions
@@ -0,0 +1,208 @@
/*
* Copyright 2010-2023 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.impl.base.test.cases.components.compilerFacility
import com.intellij.openapi.extensions.LoadingOrder
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.components.KtCompilationResult
import org.jetbrains.kotlin.analysis.api.components.KtCompiledFile
import org.jetbrains.kotlin.analysis.api.components.KtCompilerFacility
import org.jetbrains.kotlin.analysis.api.components.KtCompilerTarget
import org.jetbrains.kotlin.analysis.api.diagnostics.KtDiagnostic
import org.jetbrains.kotlin.analysis.api.diagnostics.KtDiagnosticWithPsi
import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedSingleModuleTest
import org.jetbrains.kotlin.analysis.test.framework.services.expressionMarkerProvider
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.codegen.BytecodeListingTextCollectingVisitor
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.util.DumpIrTreeOptions
import org.jetbrains.kotlin.ir.util.dump
import org.jetbrains.kotlin.psi.KtCodeFragment
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import java.io.File
import kotlin.test.assertFalse
abstract class AbstractCompilerFacilityTest : AbstractAnalysisApiBasedSingleModuleTest() {
private companion object {
private val ALLOWED_ERRORS = listOf(
FirErrors.INVISIBLE_REFERENCE,
FirErrors.INVISIBLE_SETTER,
FirErrors.DEPRECATION_ERROR,
FirErrors.DIVISION_BY_ZERO,
FirErrors.OPT_IN_USAGE_ERROR,
FirErrors.OPT_IN_OVERRIDE_ERROR,
FirErrors.UNSAFE_CALL,
FirErrors.UNSAFE_IMPLICIT_INVOKE_CALL,
FirErrors.UNSAFE_INFIX_CALL,
FirErrors.UNSAFE_OPERATOR_CALL,
FirErrors.ITERATOR_ON_NULLABLE,
FirErrors.UNEXPECTED_SAFE_CALL,
FirErrors.DSL_SCOPE_VIOLATION,
).map { it.name }
}
override fun doTestByFileStructure(ktFiles: List<KtFile>, module: TestModule, testServices: TestServices) {
val ktFile = ktFiles.singleOrNull() ?: ktFiles.first { it.name == "main.kt" }
val irCollector = CollectingIrGenerationExtension()
val project = ktFile.project
project.extensionArea.getExtensionPoint(IrGenerationExtension.extensionPointName)
.registerExtension(irCollector, LoadingOrder.LAST, project)
val ktCodeFragment = createCodeFragment(ktFile, module, testServices)
if (ktCodeFragment != null) {
for (importNameString in module.directives[Directives.CODE_FRAGMENT_IMPORT]) {
ktCodeFragment.addImport("import $importNameString")
}
}
val compilerConfiguration = CompilerConfiguration().apply {
put(CommonConfigurationKeys.MODULE_NAME, module.name)
put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, module.languageVersionSettings)
put(JVMConfigurationKeys.IR, true)
module.directives[Directives.CODE_FRAGMENT_CLASS_NAME].singleOrNull()
?.let { put(KtCompilerFacility.CODE_FRAGMENT_CLASS_NAME, it) }
module.directives[Directives.CODE_FRAGMENT_METHOD_NAME].singleOrNull()
?.let { put(KtCompilerFacility.CODE_FRAGMENT_METHOD_NAME, it) }
}
val ktTargetFile = ktCodeFragment ?: ktFile
analyze(ktTargetFile) {
val target = KtCompilerTarget.Jvm(ClassBuilderFactories.TEST)
val allowedErrorFilter: (KtDiagnostic) -> Boolean = { it.factoryName in ALLOWED_ERRORS }
val actualText = when (val result = compile(ktTargetFile, compilerConfiguration, target, allowedErrorFilter)) {
is KtCompilationResult.Failure -> result.errors.joinToString("\n") { dumpDiagnostic(it) }
is KtCompilationResult.Success -> dumpClassFiles(result.output)
}
testServices.assertions.assertEqualsToTestDataFileSibling(actualText)
}
testServices.assertions.assertEqualsToTestDataFileSibling(irCollector.result, extension = ".ir.txt")
}
override fun configureTest(builder: TestConfigurationBuilder) {
super.configureTest(builder)
with(builder) {
useDirectives(Directives)
useConfigurators()
defaultDirectives {
+ConfigurationDirectives.WITH_STDLIB
+JvmEnvironmentConfigurationDirectives.FULL_JDK
}
}
}
private fun dumpDiagnostic(diagnostic: KtDiagnostic): String {
val textRanges = when (diagnostic) {
is KtDiagnosticWithPsi<*> -> {
diagnostic.textRanges.singleOrNull()?.toString()
?: diagnostic.textRanges.joinToString(prefix = "[", postfix = "]")
}
else -> null
}
return buildString {
if (textRanges != null) {
append(textRanges)
append(" ")
}
append(diagnostic.factoryName)
append(" ")
append(diagnostic.defaultMessage)
}
}
private fun dumpClassFiles(outputFiles: List<KtCompiledFile>): String {
val classes = outputFiles
.filter { it.path.endsWith(".class", ignoreCase = true) }
.also { check(it.isNotEmpty()) }
.sortedBy { it.path }
.map { outputFile ->
val classReader = ClassReader(outputFile.content)
ClassNode(Opcodes.API_VERSION).also { classReader.accept(it, ClassReader.SKIP_CODE) }
}
val allClasses = classes.associateBy { Type.getObjectType(it.name) }
return classes.joinToString("\n\n") { node ->
val visitor = BytecodeListingTextCollectingVisitor(
BytecodeListingTextCollectingVisitor.Filter.EMPTY,
allClasses,
withSignatures = false,
withAnnotations = false,
sortDeclarations = true
)
node.accept(visitor)
visitor.text
}
}
object Directives : SimpleDirectivesContainer() {
val CODE_FRAGMENT_IMPORT by stringDirective("Import directive for a code fragment")
val CODE_FRAGMENT_CLASS_NAME by stringDirective("Short name of a code fragment class")
val CODE_FRAGMENT_METHOD_NAME by stringDirective("Name of a code fragment facade method")
}
}
internal fun createCodeFragment(ktFile: KtFile, module: TestModule, testServices: TestServices): KtCodeFragment? {
val ioFile = module.files.single { it.name == ktFile.name }.originalFile
val ioFragmentFile = File(ioFile.parent, "${ioFile.nameWithoutExtension}.fragment.${ioFile.extension}")
if (!ioFragmentFile.exists()) {
return null
}
val contextElement = testServices.expressionMarkerProvider.getElementOfTypeAtCaret<KtDeclaration>(ktFile)
val fragmentText = ioFragmentFile.readText()
val isBlockFragment = fragmentText.any { it == '\n' }
val project = ktFile.project
val factory = KtPsiFactory(project, markGenerated = false)
return when {
isBlockFragment -> factory.createBlockCodeFragment(fragmentText, contextElement)
else -> factory.createExpressionCodeFragment(fragmentText, contextElement)
}
}
private class CollectingIrGenerationExtension : IrGenerationExtension {
lateinit var result: String
private set
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
assertFalse { ::result.isInitialized }
val dumpOptions = DumpIrTreeOptions(normalizeNames = true, stableOrder = true)
result = moduleFragment.dump(dumpOptions)
}
}