[go: nahoru, domu]

blob: 8ecce65efa32a2dd2e53d1220d450b646b0fe262 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* 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 androidx.compose.compiler.plugins.kotlin
import androidx.compose.compiler.plugins.kotlin.lower.dumpSrc
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContextImpl
import org.jetbrains.kotlin.backend.common.ir.createParameterDeclarations
import org.jetbrains.kotlin.backend.jvm.JvmGeneratorExtensions
import org.jetbrains.kotlin.backend.jvm.serialization.JvmIdSignatureDescriptor
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin
import org.jetbrains.kotlin.descriptors.konan.KlibModuleOrigin
import org.jetbrains.kotlin.ir.backend.jvm.serialization.EmptyLoggingContext
import org.jetbrains.kotlin.ir.backend.jvm.serialization.JvmIrLinker
import org.jetbrains.kotlin.ir.backend.jvm.serialization.JvmManglerDesc
import org.jetbrains.kotlin.ir.builders.declarations.buildClass
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.descriptors.IrFunctionFactory
import org.jetbrains.kotlin.ir.linkage.IrDeserializer
import org.jetbrains.kotlin.ir.util.DeclarationStubGenerator
import org.jetbrains.kotlin.ir.util.ExternalDependenciesGenerator
import org.jetbrains.kotlin.ir.util.dump
import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi2ir.Psi2IrConfiguration
import org.jetbrains.kotlin.psi2ir.Psi2IrTranslator
import org.jetbrains.kotlin.psi2ir.generators.GeneratorContext
import org.jetbrains.kotlin.resolve.AnalyzingUtils
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import java.io.File
@Suppress("LeakingThis")
abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
open val liveLiteralsEnabled get() = false
open val sourceInformationEnabled get() = true
private val extension = ComposeIrGenerationExtension(
liveLiteralsEnabled,
sourceInformationEnabled
)
override fun postProcessingStep(
module: IrModuleFragment,
generatorContext: GeneratorContext,
irLinker: IrDeserializer
) {
extension.generate(
module,
IrPluginContextImpl(
generatorContext.moduleDescriptor,
generatorContext.bindingContext,
generatorContext.languageVersionSettings,
generatorContext.symbolTable,
generatorContext.typeTranslator,
generatorContext.irBuiltIns,
irLinker
)
)
}
}
abstract class AbstractIrTransformTest : AbstractCompilerTest() {
protected fun sourceFile(name: String, source: String): KtFile {
val result = createFile(name, source, myEnvironment!!.project)
val ranges = AnalyzingUtils.getSyntaxErrorRanges(result)
assert(ranges.isEmpty()) { "Syntax errors found in $name: $ranges" }
return result
}
protected open val additionalPaths = emptyList<File>()
abstract fun postProcessingStep(
module: IrModuleFragment,
generatorContext: GeneratorContext,
irLinker: IrDeserializer
)
fun verifyComposeIrTransform(
source: String,
expectedTransformed: String,
extra: String = "",
dumpTree: Boolean = false
) {
val files = listOf(
sourceFile("Test.kt", source.replace('%', '$')),
sourceFile("Extra.kt", extra.replace('%', '$'))
)
val irModule = generateIrModuleWithJvmResolve(files)
val keySet = mutableListOf<Int>()
val actualTransformed = irModule
.files[0]
.dumpSrc()
.replace('$', '%')
// replace source keys for start group calls
.replace(
Regex(
"(%composer\\.start(Restart|Movable|Replaceable)Group\\()-?((0b)?[-\\d]+)"
)
) {
val stringKey = it.groupValues[3]
val key = if (stringKey.startsWith("0b"))
Integer.parseInt(stringKey.drop(2), 2)
else
stringKey.toInt()
if (key in keySet) {
"${it.groupValues[1]}<!DUPLICATE KEY: $key!>"
} else {
keySet.add(key)
"${it.groupValues[1]}<>"
}
}
// replace source information with source it references
.replace(
Regex("(%composer\\.start(Restart|Movable|Replaceable)Group\\" +
"([^\"\\n]*)\"(.*)\"\\)")
) {
"${it.groupValues[1]}\"${
generateSourceInfo(it.groupValues[3], source)
}\")"
}
.replace(
Regex("(composableLambda[N]?\\" +
"([^\"\\n]*)\"(.*)\"\\)")
) {
"${it.groupValues[1]}\"${
generateSourceInfo(it.groupValues[2], source)
}\")"
}
// replace source keys for joinKey calls
.replace(
Regex(
"(%composer\\.joinKey\\()([-\\d]+)"
)
) {
"${it.groupValues[1]}<>"
}
// composableLambdaInstance(<>, true)
.replace(
Regex(
"(composableLambdaInstance\\()([-\\d]+)"
)
) {
"${it.groupValues[1]}<>"
}
// composableLambda(%composer, <>, true)
.replace(
Regex(
"(composableLambda\\(%composer,\\s)([-\\d]+)"
)
) {
"${it.groupValues[1]}<>"
}
// %composer, -1234,
.replace(
Regex(
"%composer,\\s([-\\d]+),"
)
) {
"%composer, <>,"
}
.trimIndent()
.trimTrailingWhitespacesAndAddNewlineAtEOF()
if (dumpTree) {
println(irModule.dump())
}
assertEquals(
expectedTransformed
.trimIndent()
.trimTrailingWhitespacesAndAddNewlineAtEOF(),
actualTransformed
)
}
private fun MatchResult.isNumber() = groupValues[1].isNotEmpty()
private fun MatchResult.number() = groupValues[1].toInt()
private val MatchResult.text get() = groupValues[0]
private fun MatchResult.isChar(c: String) = text == c
private fun MatchResult.isFileName() = groups[4] != null
private fun generateSourceInfo(sourceInfo: String, source: String): String {
val r = Regex("(\\d+)|([,])|([*])|([:])|C(\\(.*\\))?|L|(P\\(*\\))|@")
var current = 0
var currentResult = r.find(sourceInfo, current)
var result = ""
fun next(): MatchResult? {
currentResult?.let {
current = it.range.last + 1
currentResult = it.next()
}
return currentResult
}
// A location has the format: [<line-number>]['@' <offset> ['L' <length>]]
// where the named productions are numbers
fun parseLocation(): String? {
var mr = currentResult
if (mr != null && mr.isNumber()) {
// line number, we ignore the value in during testing.
mr = next()
}
if (mr != null && mr.isChar("@")) {
// Offset
mr = next()
if (mr == null || !mr.isNumber()) {
return null
}
val offset = mr.number()
mr = next()
var ellipsis = ""
val maxFragment = 6
val rawLength = if (mr != null && mr.isChar("L")) {
mr = next()
if (mr == null || !mr.isNumber()) {
return null
}
mr.number().also { next() }
} else {
maxFragment
}
val eol = source.indexOf('\n', offset).let {
if (it < 0) source.length else it
}
val space = source.indexOf(' ', offset).let {
if (it < 0) source.length else it
}
val maxEnd = offset + maxFragment
if (eol > maxEnd && space > maxEnd) ellipsis = "..."
val length = minOf(maxEnd, minOf(offset + rawLength, space, eol)) - offset
return "<${source.substring(offset, offset + length)}$ellipsis>"
}
return null
}
while (currentResult != null) {
val mr = currentResult!!
if (mr.range.start != current) {
return "invalid source info at $current: '$sourceInfo'"
}
when {
mr.isNumber() || mr.isChar("@") -> {
val fragment = parseLocation()
?: return "invalid source info at $current: '$sourceInfo'"
result += fragment
}
mr.isFileName() -> {
return result + ":" + sourceInfo.substring(mr.range.last + 1)
}
else -> {
result += mr.text
next()
}
}
require(mr != currentResult) { "regex didn't advance" }
}
if (current != sourceInfo.length)
return "invalid source info at $current: '$sourceInfo'"
return result
}
protected fun generateIrModuleWithJvmResolve(files: List<KtFile>): IrModuleFragment {
val classPath = createClasspath() + additionalPaths
val configuration = newConfiguration()
configuration.addJvmClasspathRoots(classPath)
configuration.put(JVMConfigurationKeys.IR, true)
configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
val environment = KotlinCoreEnvironment.createForTests(
myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
).also { setupEnvironment(it) }
val mangler = JvmManglerDesc(null)
val signaturer = JvmIdSignatureDescriptor(mangler)
val psi2ir = Psi2IrTranslator(
environment.configuration.languageVersionSettings,
Psi2IrConfiguration(ignoreErrors = false),
signaturer
)
val analysisResult = JvmResolveUtil.analyze(files, environment)
if (!psi2ir.configuration.ignoreErrors) {
analysisResult.throwIfError()
AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
}
val extensions = JvmGeneratorExtensions()
val generatorContext = psi2ir.createGeneratorContext(
analysisResult.moduleDescriptor,
analysisResult.bindingContext,
extensions = extensions
)
val stubGenerator = DeclarationStubGenerator(
generatorContext.moduleDescriptor,
generatorContext.symbolTable,
generatorContext.irBuiltIns.languageVersionSettings,
extensions
)
val functionFactory = IrFunctionFactory(
generatorContext.irBuiltIns,
generatorContext.symbolTable
)
generatorContext.irBuiltIns.functionFactory = functionFactory
val irLinker = JvmIrLinker(
generatorContext.moduleDescriptor,
EmptyLoggingContext,
generatorContext.irBuiltIns,
generatorContext.symbolTable,
functionFactory,
stubGenerator,
mangler
)
generatorContext.moduleDescriptor.allDependencyModules.map {
val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
irLinker.deserializeIrModuleHeader(it, kotlinLibrary)
}
val irProviders = listOf(irLinker)
stubGenerator.setIrProviders(irProviders)
ExternalDependenciesGenerator(
generatorContext.symbolTable,
irProviders,
generatorContext.languageVersionSettings
).generateUnboundSymbolsAsDependencies()
psi2ir.addPostprocessingStep { module ->
val old = stubGenerator.unboundSymbolGeneration
try {
stubGenerator.unboundSymbolGeneration = true
postProcessingStep(module, generatorContext, irLinker)
} finally {
stubGenerator.unboundSymbolGeneration = old
}
}
val irModuleFragment = psi2ir.generateModuleFragment(
generatorContext,
files,
irProviders,
IrGenerationExtension.getInstances(myEnvironment!!.project),
expectDescriptorToSymbol = null
)
irLinker.postProcess()
return irModuleFragment
}
fun facadeClassGenerator(source: DeserializedContainerSource): IrClass? {
val jvmPackagePartSource = source.safeAs<JvmPackagePartSource>() ?: return null
val facadeName = jvmPackagePartSource.facadeClassName ?: jvmPackagePartSource.className
return buildClass {
origin = IrDeclarationOrigin.FILE_CLASS
name = facadeName.fqNameForTopLevelClassMaybeWithDollars.shortName()
}.also {
it.createParameterDeclarations()
}
}
}