[IR] Enhance error reporting for IR linking issues

^KT-44626

Typical use case:
- There are two KLIB libraries: A and B.
- Library A has two versions: A.v1 (older) and A.v2 (newer).
- A.v2 is ABI-incompatible with A.v1.
- B depends on A and was compiled against A.v1.
- An attempt to build the application with A.v2 and B fails with weird error message. It's unclear for end user what's wrong and what needs to be done to fix the issue.

The fix improves error reporting for the following particular cases:
- A symbol that is gone (KT-41378)
- A class that became a typealias (KT-47285, KT-46697)
- A typealias that became a class (KT-46340)
This commit is contained in:
Dmitriy Dolovov
2021-06-10 11:40:16 +03:00
parent 672b972b38
commit c1fb40a436
15 changed files with 1046 additions and 81 deletions

View File

@@ -5,4 +5,5 @@
package org.jetbrains.kotlin.ir.linkage
object KotlinIrLinkerInternalException : Exception("Kotlin IR Linker exception")
// Used to terminate linking process. Detailed linkage errors are reported separately to IrMessageLogger.
class KotlinIrLinkerInternalException : Exception("Kotlin IR Linker exception")

View File

@@ -115,8 +115,7 @@ abstract class BasicIrModuleDeserializer(
strategy.needBodies,
allowErrorNodes,
strategy.inlineBodies,
moduleDeserializer,
linker::handleNoModuleDeserializerFound,
moduleDeserializer
)
fileToDeserializerMap[file] = fileDeserializationState.fileDeserializer

View File

@@ -8,6 +8,8 @@ package org.jetbrains.kotlin.backend.common.serialization
import org.jetbrains.kotlin.backend.common.overrides.FakeOverrideBuilder
import org.jetbrains.kotlin.backend.common.overrides.FakeOverrideClassFilter
import org.jetbrains.kotlin.backend.common.serialization.encodings.*
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.checkErrorNodesAllowed
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.checkSymbolType
import org.jetbrains.kotlin.backend.common.serialization.proto.IrDeclaration.DeclaratorCase.*
import org.jetbrains.kotlin.backend.common.serialization.proto.IrType.KindCase.*
import org.jetbrains.kotlin.descriptors.InlineClassRepresentation
@@ -124,13 +126,12 @@ class IrDeclarationDeserializer(
}
private fun deserializeSimpleType(proto: ProtoSimpleType): IrSimpleType {
val symbol = deserializeIrSymbolAndRemap(proto.classifier) as? IrClassifierSymbol
?: error("could not convert sym to ClassifierSymbol")
val symbol = checkSymbolType<IrClassifierSymbol>(deserializeIrSymbolAndRemap(proto.classifier))
val arguments = proto.argumentList.map { deserializeIrTypeArgument(it) }
val annotations = deserializeAnnotations(proto.annotationList)
val result: IrSimpleType = IrSimpleTypeImpl(
return IrSimpleTypeImpl(
null,
symbol,
proto.hasQuestionMark,
@@ -138,16 +139,11 @@ class IrDeclarationDeserializer(
annotations,
if (proto.hasAbbreviation()) deserializeTypeAbbreviation(proto.abbreviation) else null
)
return result
}
private fun deserializeTypeAbbreviation(proto: ProtoTypeAbbreviation): IrTypeAbbreviation =
IrTypeAbbreviationImpl(
deserializeIrSymbolAndRemap(proto.typeAlias).let {
it as? IrTypeAliasSymbol
?: error("IrTypeAliasSymbol expected: $it")
},
checkSymbolType(deserializeIrSymbolAndRemap(proto.typeAlias)),
proto.hasQuestionMark,
proto.argumentList.map { deserializeIrTypeArgument(it) },
deserializeAnnotations(proto.annotationList)
@@ -159,7 +155,7 @@ class IrDeclarationDeserializer(
}
private fun deserializeErrorType(proto: ProtoErrorType): IrErrorType {
require(allowErrorNodes) { "IrErrorType found but error code is not allowed" }
checkErrorNodesAllowed<IrErrorType>(allowErrorNodes)
val annotations = deserializeAnnotations(proto.annotationList)
return IrErrorTypeImpl(null, annotations, Variance.INVARIANT)
}
@@ -273,7 +269,7 @@ class IrDeclarationDeserializer(
val result = symbolTable.run {
if (isGlobal) {
val p = symbolDeserializer.deserializeIrSymbolToDeclare(proto.base.symbol)
val symbol = p.first as IrTypeParameterSymbol
val symbol = checkSymbolType<IrTypeParameterSymbol>(p.first)
sig = p.second
declareGlobalTypeParameter(sig, { symbol }, factory)
} else {
@@ -297,7 +293,7 @@ class IrDeclarationDeserializer(
val nameAndType = BinaryNameAndType.decode(proto.nameType)
irFactory.createValueParameter(
startOffset, endOffset, origin,
symbol as IrValueParameterSymbol,
checkSymbolType(symbol),
deserializeName(nameAndType.nameIndex),
index,
deserializeIrType(nameAndType.typeIndex),
@@ -314,11 +310,11 @@ class IrDeclarationDeserializer(
fun deserializeIrClass(proto: ProtoClass): IrClass =
withDeserializedIrDeclarationBase(proto.base) { symbol, signature, startOffset, endOffset, origin, fcode ->
checkSymbolType<IrClassSymbol>(symbol)
if (allowRedeclaration && symbol.isBound) return symbol.owner
val flags = ClassFlags.decode(fcode)
if (allowRedeclaration && symbol.isBound) return symbol.owner as IrClass
symbolTable.declareClass(signature, { symbol as IrClassSymbol }) {
symbolTable.declareClass(signature, { symbol }) {
irFactory.createClass(
startOffset, endOffset, origin,
it,
@@ -379,7 +375,7 @@ class IrDeclarationDeserializer(
private fun deserializeIrTypeAlias(proto: ProtoTypeAlias): IrTypeAlias =
withDeserializedIrDeclarationBase(proto.base) { symbol, uniqId, startOffset, endOffset, origin, fcode ->
require(symbol is IrTypeAliasSymbol)
checkSymbolType<IrTypeAliasSymbol>(symbol)
symbolTable.declareTypeAlias(uniqId, { symbol }) {
val flags = TypeAliasFlags.decode(fcode)
val nameType = BinaryNameAndType.decode(proto.nameType)
@@ -400,7 +396,7 @@ class IrDeclarationDeserializer(
}
private fun deserializeErrorDeclaration(proto: ProtoErrorDeclaration): IrErrorDeclaration {
require(allowErrorNodes) { "IrErrorDeclaration found but error code is not allowed" }
checkErrorNodesAllowed<IrErrorDeclaration>(allowErrorNodes)
val coordinates = BinaryCoordinates.decode(proto.coordinates)
return irFactory.createErrorDeclaration(coordinates.startOffset, coordinates.endOffset).also {
it.parent = currentParent
@@ -528,7 +524,7 @@ class IrDeclarationDeserializer(
block: (IrFunctionSymbol, IdSignature, Int, Int, IrDeclarationOrigin, Long) -> T
): T = withDeserializedIrDeclarationBase(proto.base) { symbol, idSig, startOffset, endOffset, origin, fcode ->
symbolTable.withScope(symbol) {
block(symbol as IrFunctionSymbol, idSig, startOffset, endOffset, origin, fcode).usingParent {
block(checkSymbolType(symbol), idSig, startOffset, endOffset, origin, fcode).usingParent {
if (!skipMutableState) {
typeParameters = deserializeTypeParameters(proto.typeParameterList, false)
val nameType = BinaryNameAndType.decode(proto.nameType)
@@ -553,9 +549,11 @@ class IrDeclarationDeserializer(
internal fun deserializeIrFunction(proto: ProtoFunction): IrSimpleFunction {
return withDeserializedIrFunctionBase(proto.base) { symbol, idSig, startOffset, endOffset, origin, fcode ->
checkSymbolType<IrSimpleFunctionSymbol>(symbol)
if (allowRedeclaration && symbol.isBound) return symbol.owner
val flags = FunctionFlags.decode(fcode)
if (allowRedeclaration && symbol.isBound) return symbol.owner as IrSimpleFunction
symbolTable.declareSimpleFunction(idSig, { symbol as IrSimpleFunctionSymbol }) {
symbolTable.declareSimpleFunction(idSig, { symbol }) {
val nameType = BinaryNameAndType.decode(proto.base.nameType)
irFactory.createFunction(
startOffset, endOffset, origin,
@@ -574,18 +572,21 @@ class IrDeclarationDeserializer(
flags.isFakeOverride
)
}.apply {
overriddenSymbols = proto.overriddenList.map { deserializeIrSymbolAndRemap(it) as IrSimpleFunctionSymbol }
overriddenSymbols = proto.overriddenList.map { checkSymbolType(deserializeIrSymbolAndRemap(it)) }
}
}
}
fun deserializeIrVariable(proto: ProtoVariable): IrVariable =
withDeserializedIrDeclarationBase(proto.base) { symbol, _, startOffset, endOffset, origin, fcode ->
checkSymbolType<IrVariableSymbol>(symbol)
val flags = LocalVariableFlags.decode(fcode)
val nameType = BinaryNameAndType.decode(proto.nameType)
(if (allowRedeclaration && symbol.isBound) symbol.owner as IrVariable else IrVariableImpl(
(if (allowRedeclaration && symbol.isBound) symbol.owner else IrVariableImpl(
startOffset, endOffset, origin,
symbol as IrVariableSymbol,
symbol,
deserializeName(nameType.nameIndex),
deserializeIrType(nameType.typeIndex),
flags.isVar,
@@ -599,7 +600,7 @@ class IrDeclarationDeserializer(
private fun deserializeIrEnumEntry(proto: ProtoEnumEntry): IrEnumEntry =
withDeserializedIrDeclarationBase(proto.base) { symbol, uniqId, startOffset, endOffset, origin, _ ->
symbolTable.declareEnumEntry(uniqId, { symbol as IrEnumEntrySymbol }) {
symbolTable.declareEnumEntry(uniqId, { checkSymbolType(symbol) }) {
irFactory.createEnumEntry(startOffset, endOffset, origin, it, deserializeName(proto.name))
}.apply {
if (!skipMutableState) {
@@ -613,14 +614,14 @@ class IrDeclarationDeserializer(
private fun deserializeIrAnonymousInit(proto: ProtoAnonymousInit): IrAnonymousInitializer =
withDeserializedIrDeclarationBase(proto.base) { symbol, _, startOffset, endOffset, origin, _ ->
irFactory.createAnonymousInitializer(startOffset, endOffset, origin, symbol as IrAnonymousInitializerSymbol).apply {
irFactory.createAnonymousInitializer(startOffset, endOffset, origin, checkSymbolType(symbol)).apply {
body = deserializeStatementBody(proto.body) as IrBlockBody
}
}
private fun deserializeIrConstructor(proto: ProtoConstructor): IrConstructor =
withDeserializedIrFunctionBase(proto.base) { symbol, idSig, startOffset, endOffset, origin, fcode ->
require(symbol is IrConstructorSymbol)
checkSymbolType<IrConstructorSymbol>(symbol)
val flags = FunctionFlags.decode(fcode)
val nameType = BinaryNameAndType.decode(proto.base.nameType)
symbolTable.declareConstructor(idSig, { symbol }) {
@@ -641,7 +642,7 @@ class IrDeclarationDeserializer(
private fun deserializeIrField(proto: ProtoField): IrField =
withDeserializedIrDeclarationBase(proto.base) { symbol, uniqId, startOffset, endOffset, origin, fcode ->
require(symbol is IrFieldSymbol)
checkSymbolType<IrFieldSymbol>(symbol)
val nameType = BinaryNameAndType.decode(proto.nameType)
val type = deserializeIrType(nameType.typeIndex)
val flags = FieldFlags.decode(fcode)
@@ -672,11 +673,14 @@ class IrDeclarationDeserializer(
private fun deserializeIrLocalDelegatedProperty(proto: ProtoLocalDelegatedProperty): IrLocalDelegatedProperty =
withDeserializedIrDeclarationBase(proto.base) { symbol, _, startOffset, endOffset, origin, fcode ->
checkSymbolType<IrLocalDelegatedPropertySymbol>(symbol)
val flags = LocalVariableFlags.decode(fcode)
val nameAndType = BinaryNameAndType.decode(proto.nameType)
val prop = if (allowRedeclaration && symbol.isBound) symbol.owner as IrLocalDelegatedProperty else irFactory.createLocalDelegatedProperty(
val prop = if (allowRedeclaration && symbol.isBound) symbol.owner else irFactory.createLocalDelegatedProperty(
startOffset, endOffset, origin,
symbol as IrLocalDelegatedPropertySymbol,
symbol,
deserializeName(nameAndType.nameIndex),
deserializeIrType(nameAndType.typeIndex),
flags.isVar
@@ -696,7 +700,7 @@ class IrDeclarationDeserializer(
private fun deserializeIrProperty(proto: ProtoProperty): IrProperty =
withDeserializedIrDeclarationBase(proto.base) { symbol, uniqId, startOffset, endOffset, origin, fcode ->
require(symbol is IrPropertySymbol)
checkSymbolType<IrPropertySymbol>(symbol)
val flags = PropertyFlags.decode(fcode)
val prop = if (allowRedeclaration && symbol.isBound) symbol.owner else symbolTable.declareProperty(uniqId, { symbol }) {
irFactory.createProperty(

View File

@@ -5,13 +5,11 @@
package org.jetbrains.kotlin.backend.common.serialization
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.impl.EmptyPackageFragmentDescriptor
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl
import org.jetbrains.kotlin.ir.declarations.path
import org.jetbrains.kotlin.ir.symbols.impl.IrFileSymbolImpl
import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.NaiveSourceBasedFileEntryImpl
@@ -65,8 +63,7 @@ class FileDeserializationState(
deserializeBodies: Boolean,
allowErrorNodes: Boolean,
deserializeInlineFunctions: Boolean,
moduleDeserializer: IrModuleDeserializer,
handleNoModuleDeserializerFound: (IdSignature, ModuleDescriptor, Collection<IrModuleDeserializer>) -> IrModuleDeserializer,
moduleDeserializer: IrModuleDeserializer
) {
val symbolDeserializer =
@@ -78,12 +75,13 @@ class FileDeserializationState(
) { idSig, symbolKind ->
val topLevelSig = idSig.topLevelSignature()
val actualModuleDeserializer =
moduleDeserializer.findModuleDeserializerForTopLevelId(topLevelSig) ?: handleNoModuleDeserializerFound(
idSig,
moduleDeserializer.moduleDescriptor,
moduleDeserializer.moduleDependencies
)
val actualModuleDeserializer = moduleDeserializer.findModuleDeserializerForTopLevelId(topLevelSig)
?: run {
// The symbol might be gone in newer version of dependency KLIB. Then the KLIB that was compiled against
// the older version of dependency KLIB will still have a reference to non-existing symbol. And the linker will have to
// handle such situation appropriately. See KT-41378.
linker.handleSignatureIdNotFoundInModuleWithDependencies(idSig, moduleDeserializer)
}
actualModuleDeserializer.deserializeIrSymbol(idSig, symbolKind)
}

View File

@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.common.serialization
import org.jetbrains.kotlin.backend.common.overrides.FakeOverrideBuilder
import org.jetbrains.kotlin.backend.common.overrides.FileLocalAwareLinker
import org.jetbrains.kotlin.backend.common.serialization.encodings.BinarySymbolData
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.*
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
@@ -16,7 +17,6 @@ import org.jetbrains.kotlin.ir.builders.TranslationPluginContext
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.linkage.IrDeserializer
import org.jetbrains.kotlin.ir.linkage.KotlinIrLinkerInternalException
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.IrMessageLogger
@@ -53,33 +53,23 @@ abstract class KotlinIrLinker(
private lateinit var linkerExtensions: Collection<IrDeserializer.IrLinkerExtension>
public open fun handleNoModuleDeserializerFound(idSignature: IdSignature, currentModule: ModuleDescriptor, dependencies: Collection<IrModuleDeserializer>): IrModuleDeserializer {
val message = buildString {
append("Module ${currentModule.name} has reference $idSignature, unfortunately neither itself nor its dependencies ")
dependencies.joinTo(this, "\n\t", "[\n\t", "\n]") { it.moduleDescriptor.name.asString() }
append(" contain this declaration")
append("\n")
append("Please check that project configuration is correct and has required dependencies.")
}
messageLogger.report(IrMessageLogger.Severity.ERROR, message, null)
protected open val userVisibleIrModulesSupport: UserVisibleIrModulesSupport = UserVisibleIrModulesSupport.DEFAULT
throw KotlinIrLinkerInternalException
fun handleSignatureIdNotFoundInModuleWithDependencies(
idSignature: IdSignature,
moduleDeserializer: IrModuleDeserializer
): IrModuleDeserializer {
throw SignatureIdNotFoundInModuleWithDependencies(
idSignature = idSignature,
problemModuleDeserializer = moduleDeserializer,
allModuleDeserializers = deserializersForModules.values,
userVisibleIrModulesSupport = userVisibleIrModulesSupport
).raiseIssue(messageLogger)
}
public open fun resolveModuleDeserializer(module: ModuleDescriptor, signature: IdSignature?): IrModuleDeserializer {
return deserializersForModules[module.name.asString()] ?: run {
val message = buildString {
append("Could not load module ")
append(module)
signature?.let {
append("; It was an attempt to find deserializer for ")
append(it)
}
}
messageLogger.report(IrMessageLogger.Severity.ERROR, message, null)
throw KotlinIrLinkerInternalException
}
fun resolveModuleDeserializer(module: ModuleDescriptor, idSignature: IdSignature?): IrModuleDeserializer {
return deserializersForModules[module.name.asString()]
?: throw NoDeserializerForModule(module.name, idSignature).raiseIssue(messageLogger)
}
protected abstract fun createModuleDeserializer(
@@ -153,7 +143,11 @@ abstract class KotlinIrLinker(
}
if (!symbol.isBound) {
findDeserializedDeclarationForSymbol(symbol) ?: tryResolveCustomDeclaration(symbol) ?: return null
try {
findDeserializedDeclarationForSymbol(symbol) ?: tryResolveCustomDeclaration(symbol) ?: return null
} catch (e: IrSymbolTypeMismatchException) {
throw SymbolTypeMismatch(e, deserializersForModules.values, userVisibleIrModulesSupport).raiseIssue(messageLogger)
}
}
// TODO: we do have serializations for those, but let's just create a stub for now.

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2010-2021 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.backend.common.serialization.linkerissues
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
sealed class IrDeserializationException(message: String) : Exception(message) {
override val message: String get() = super.message!!
}
class IrSymbolTypeMismatchException(
val expected: Class<out IrSymbol>,
val actual: IrSymbol
) : IrDeserializationException("The symbol of unexpected type encountered during IR deserialization: ${actual::class.java.simpleName}, ${actual.signature?.render() ?: actual.toString()}. ${expected.simpleName} is expected.")
class IrDisallowedErrorNode(
clazz: Class<out IrAnnotationContainer>
) : IrDeserializationException("${clazz::class.java.simpleName} found but error nodes are not allowed.")
@OptIn(ExperimentalContracts::class)
internal inline fun <reified T : IrSymbol> checkSymbolType(symbol: IrSymbol): T {
contract {
returns() implies (symbol is T)
}
if (symbol !is T) throw IrSymbolTypeMismatchException(T::class.java, symbol) else return symbol
}
@OptIn(ExperimentalContracts::class)
internal inline fun <reified T : IrAnnotationContainer> checkErrorNodesAllowed(errorNodesAllowed: Boolean) {
contract {
returns() implies errorNodesAllowed
}
if (!errorNodesAllowed) throw IrDisallowedErrorNode(T::class.java)
}

View File

@@ -0,0 +1,536 @@
/*
* Copyright 2010-2021 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.backend.common.serialization.linkerissues
import org.jetbrains.kotlin.backend.common.serialization.IrModuleDeserializer
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.PotentialConflictKind.*
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.PotentialConflictKind.Companion.mostSignificantConflictKind
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.PotentialConflictReason.Companion.mostSignificantConflictReasons
import org.jetbrains.kotlin.ir.linkage.KotlinIrLinkerInternalException
import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.IrMessageLogger
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.utils.ResolvedDependency
import org.jetbrains.kotlin.utils.ResolvedDependencyId
import org.jetbrains.kotlin.utils.ResolvedDependencyVersion
import kotlin.Comparator
abstract class KotlinIrLinkerIssue {
protected abstract val message: String
fun raiseIssue(messageLogger: IrMessageLogger): KotlinIrLinkerInternalException {
messageLogger.report(IrMessageLogger.Severity.ERROR, message, null)
throw KotlinIrLinkerInternalException()
}
}
class SignatureIdNotFoundInModuleWithDependencies(
idSignature: IdSignature,
problemModuleDeserializer: IrModuleDeserializer,
allModuleDeserializers: Collection<IrModuleDeserializer>,
userVisibleIrModulesSupport: UserVisibleIrModulesSupport
) : KotlinIrLinkerIssue() {
override val message = buildString {
val allModules = userVisibleIrModulesSupport.getUserVisibleModules(allModuleDeserializers)
val problemModuleId = userVisibleIrModulesSupport.getProblemModuleId(problemModuleDeserializer, allModules)
val problemModuleIdWithVersion = allModules.getValue(problemModuleId).moduleIdWithVersion
// cause:
append("Module \"$problemModuleId\" has a reference to symbol ${idSignature.render()}.")
append(" Neither the module itself nor its dependencies contain such declaration.")
// explanation:
append("\n\nThis could happen if the required dependency is missing in the project.")
append(" Or if there is a dependency of \"$problemModuleId\" that has a different version in the project")
append(" than the version that \"$problemModuleIdWithVersion\" was initially compiled with.")
// action items:
append(" Please check that the project configuration is correct and has consistent versions of all required dependencies.")
// potentially conflicting modules:
appendPotentiallyConflictingDependencies(
header = "The list of \"$problemModuleIdWithVersion\" dependencies that may lead to conflicts:",
allModules = allModules,
potentiallyConflictingDependencies = findPotentiallyConflictingOutgoingDependencies(
problemModuleId = problemModuleId,
allModules = allModules,
),
moduleIdComparator = userVisibleIrModulesSupport.moduleIdComparator
)
// the tree of dependencies:
appendProjectDependencies(
allModules = allModules,
problemModuleIds = setOf(problemModuleId),
problemCause = "This module requires symbol ${idSignature.render()}",
moduleIdComparator = userVisibleIrModulesSupport.moduleIdComparator
)
}
}
class NoDeserializerForModule(moduleName: Name, idSignature: IdSignature?) : KotlinIrLinkerIssue() {
override val message = buildString {
append("Could not load module ${moduleName.asString()}")
if (idSignature != null) append(" in an attempt to find deserializer for symbol ${idSignature.render()}.")
}
}
class SymbolTypeMismatch(
cause: IrSymbolTypeMismatchException,
allModuleDeserializers: Collection<IrModuleDeserializer>,
userVisibleIrModulesSupport: UserVisibleIrModulesSupport
) : KotlinIrLinkerIssue() {
override val message: String = buildString {
val allModules = userVisibleIrModulesSupport.getUserVisibleModules(allModuleDeserializers)
val idSignature = cause.actual.signature
// There might be multiple declaring modules. Which is also an error, and should re reported separately.
val declaringModuleIds: Set<ResolvedDependencyId> = if (idSignature != null) {
allModuleDeserializers.mapNotNullTo(mutableSetOf()) { deserializer ->
if (idSignature in deserializer) userVisibleIrModulesSupport.getProblemModuleId(deserializer, allModules) else null
}
} else emptySet()
// cause:
append(cause.message)
// explanation:
append("\n\nThis could happen if there are two libraries, where one library was compiled against the different version")
append(" of the other library than the one currently used in the project.")
// action items:
append(" Please check that the project configuration is correct and has consistent versions of dependencies.")
// potentially conflicting modules:
declaringModuleIds.forEach { declaringModuleId ->
appendPotentiallyConflictingDependencies(
header = "The list of libraries that depend on \"$declaringModuleId\" and may lead to conflicts:",
allModules = allModules,
potentiallyConflictingDependencies = findPotentiallyConflictingIncomingDependencies(
problemModuleId = declaringModuleId,
allModules = allModules,
),
moduleIdComparator = userVisibleIrModulesSupport.moduleIdComparator
)
}
// the tree of dependencies:
appendProjectDependencies(
allModules = allModules,
problemModuleIds = declaringModuleIds,
problemCause = "This module contains ${
idSignature?.render()?.let { "symbol $it" } ?: "a symbol"
} that is the cause of the conflict",
moduleIdComparator = userVisibleIrModulesSupport.moduleIdComparator
)
}
}
private fun UserVisibleIrModulesSupport.getProblemModuleId(
problemModuleDeserializer: IrModuleDeserializer,
allModules: Map<ResolvedDependencyId, ResolvedDependency>
): ResolvedDependencyId = allModules.findMatchingModule(getUserVisibleModuleId(problemModuleDeserializer)).id
/**
* Do the best effort to find a module that matches the given [moduleId]:
* - If there is a node in the map with such [ResolvedDependencyId] as [moduleId], then return the value from this node.
* - If not, then try to find a [ResolvedDependency] which contains all unique names from the given [moduleId]. This makes sense
* for such cases when the map contains a node for "org.jetbrains.kotlinx:kotlinx-coroutines-core (org.jetbrains.kotlinx:kotlinx-coroutines-core-macosx64)"
* but we are looking just for "org.jetbrains.kotlinx:kotlinx-coroutines-core".
*/
private fun Map<ResolvedDependencyId, ResolvedDependency>.findMatchingModule(moduleId: ResolvedDependencyId): ResolvedDependency {
this[moduleId]?.let { module ->
// Yes, there is a module with such ID. Just return it.
return module
}
// No, there is no module with such ID. => Find a module with a wider set of unique names.
// Typical case is when we are looking for "org.jetbrains.kotlinx:kotlinx-coroutines-core", but there is no such module.
// But there is "org.jetbrains.kotlinx:kotlinx-coroutines-core (org.jetbrains.kotlinx:kotlinx-coroutines-core-macosx64)"
// that should be used instead.
return values.first { moduleId in it.id }
}
private fun StringBuilder.appendProjectDependencies(
allModules: Map<ResolvedDependencyId, ResolvedDependency>,
problemModuleIds: Set<ResolvedDependencyId>,
problemCause: String,
moduleIdComparator: Comparator<ResolvedDependencyId>
) {
append("\n\nProject dependencies:")
if (allModules.isEmpty()) {
append(" <empty>")
return
}
val incomingDependencyIdToDependencies: MutableMap<ResolvedDependencyId, MutableCollection<ResolvedDependency>> = mutableMapOf()
allModules.values.forEach { module ->
module.requestedVersionsByIncomingDependencies.keys.forEach { incomingDependencyId ->
incomingDependencyIdToDependencies.getOrPut(incomingDependencyId) { mutableListOf() } += module
}
}
val renderedModules: MutableSet<ResolvedDependencyId> = mutableSetOf()
var everDependenciesOmitted = false
fun renderModules(modules: Collection<ResolvedDependency>, parentData: Data?) {
val filteredModules: Collection<ResolvedDependency> = if (parentData == null)
modules.filter { it.visibleAsFirstLevelDependency }
else
modules
val sortedModules: List<ResolvedDependency> = filteredModules.sortedWith { a, b -> moduleIdComparator.compare(a.id, b.id) }
sortedModules.forEachIndexed { index, module ->
val data = Data(
parent = parentData,
incomingDependencyId = module.id, // For children.
isLast = index + 1 == sortedModules.size
)
append('\n').append(data.regularLinePrefix)
append(module.id)
val incomingDependencyId: ResolvedDependencyId = parentData?.incomingDependencyId
?: ResolvedDependencyId.SOURCE_CODE_MODULE_ID
val requestedVersion: ResolvedDependencyVersion = module.requestedVersionsByIncomingDependencies.getValue(incomingDependencyId)
if (!requestedVersion.isEmpty() || !module.selectedVersion.isEmpty()) {
append(": ")
append(requestedVersion.version.ifEmpty { UNKNOWN_VERSION })
if (requestedVersion != module.selectedVersion) {
append(" -> ")
append(module.selectedVersion.version.ifEmpty { UNKNOWN_VERSION })
}
}
val renderedFirstTime = renderedModules.add(module.id)
val dependencies: Collection<ResolvedDependency>? = incomingDependencyIdToDependencies[module.id]
val needToRenderDependencies = when {
renderedFirstTime -> {
// Rendered for the first time => also render dependencies, if any.
true
}
!dependencies.isNullOrEmpty() -> {
// Already rendered at least once. Do not render dependencies, but add a mark that dependencies are omitted.
everDependenciesOmitted = true
append(" (*)")
false
}
else -> {
// Already rendered at least once. No dependencies.
false
}
}
if (module.id in problemModuleIds) {
append('\n').append(data.errorLinePrefix)
append("^^^ $problemCause.")
}
if (needToRenderDependencies && !dependencies.isNullOrEmpty()) {
renderModules(dependencies, data)
}
}
}
// Find first-level dependencies. I.e. the modules that the source code module directly depends on.
val firstLevelDependencies: Collection<ResolvedDependency> =
incomingDependencyIdToDependencies.getValue(ResolvedDependencyId.SOURCE_CODE_MODULE_ID)
renderModules(firstLevelDependencies, parentData = null)
if (everDependenciesOmitted) {
append("\n\n(*) - dependencies omitted (listed previously)")
}
}
private const val UNKNOWN_VERSION = "unknown"
private class Data(val parent: Data?, val incomingDependencyId: ResolvedDependencyId, val isLast: Boolean) {
val regularLinePrefix: String
get() {
return generateSequence(this) { it.parent }.map {
if (it === this) {
if (it.isLast) "\u2514\u2500\u2500\u2500 " /* └─── */ else "\u251C\u2500\u2500\u2500 " /* ├─── */
} else {
if (it.isLast) " " else "\u2502 " /* │ */
}
}.toList().asReversed().joinToString(separator = "")
}
val errorLinePrefix: String
get() {
return generateSequence(this) { it.parent }.map {
if (it.isLast) " " else "\u2502 " /* │ */
}.toList().asReversed().joinToString(separator = "")
}
}
/**
* Find all outgoing dependencies of [problemModuleId] that might conflict with [problemModuleId] because they have
* different (overridden) selected version then the version that [problemModuleId] was initially compiled with.
*/
private fun findPotentiallyConflictingOutgoingDependencies(
problemModuleId: ResolvedDependencyId,
allModules: Map<ResolvedDependencyId, ResolvedDependency>
): Map<ResolvedDependencyId, PotentialConflictDescription> {
data class OutgoingDependency(
val id: ResolvedDependencyId,
val requestedVersion: ResolvedDependencyVersion,
val selectedVersion: ResolvedDependencyVersion
)
// Reverse dependency index.
val outgoingDependenciesIndex: MutableMap<ResolvedDependencyId, MutableList<OutgoingDependency>> = mutableMapOf()
allModules.values.forEach { module ->
module.requestedVersionsByIncomingDependencies.forEach { (incomingDependencyId, requestedVersion) ->
outgoingDependenciesIndex.getOrPut(incomingDependencyId) { mutableListOf() } += OutgoingDependency(
id = module.id,
requestedVersion = requestedVersion,
selectedVersion = module.selectedVersion
)
}
}
val dependencyStatesMap: MutableMap<ResolvedDependencyId, MutableSet<DependencyState>> = mutableMapOf()
fun recurse(moduleId: ResolvedDependencyId, underConflictingDependency: Boolean) {
val outgoingDependencies: List<OutgoingDependency> = outgoingDependenciesIndex[moduleId].orEmpty()
outgoingDependencies.forEach { outgoingDependency ->
val dependencyState: DependencyState = when {
underConflictingDependency -> {
// Can't guarantee that any library that is a dependency of a potentially conflicting dependency
// is not a potentially conflicting dependency itself.
DependencyState(
conflictReason = PotentialConflictReason(
kind = BEHIND_CONFLICTING_DEPENDENCY,
conflictingModuleId = outgoingDependency.id
)
)
}
outgoingDependency.selectedVersion.isEmpty() -> {
// Selected version is empty (unknown). Can't guarantee that this is the same library that was requested even if
// the requested version is also empty (unknown).
DependencyState(
conflictReason = PotentialConflictReason(
kind = UNKNOWN_SELECTED_VERSION,
conflictingModuleId = outgoingDependency.id,
requestedVersion = outgoingDependency.requestedVersion
)
)
}
outgoingDependency.requestedVersion != outgoingDependency.selectedVersion -> {
// The requested and selected versions don't match.
DependencyState(
conflictReason = PotentialConflictReason(
kind = REQUESTED_SELECTED_VERSIONS_MISMATCH,
conflictingModuleId = outgoingDependency.id,
requestedVersion = outgoingDependency.requestedVersion,
selectedVersion = outgoingDependency.selectedVersion
)
)
}
else -> DependencyState.SUCCESS
}
val dependencyStates: MutableSet<DependencyState> = dependencyStatesMap.getOrPut(outgoingDependency.id) { mutableSetOf() }
val notBeenHereYet = dependencyStates.add(dependencyState)
if (notBeenHereYet) {
// Don't visit the same dependency twice.
recurse(moduleId = outgoingDependency.id, underConflictingDependency = dependencyState.conflictReason != null)
}
}
}
recurse(moduleId = problemModuleId, underConflictingDependency = false)
return dependencyStatesMap.describeDependencyStates { potentialConflictReason ->
when (potentialConflictReason.kind) {
UNKNOWN_SELECTED_VERSION -> {
"a library with unknown version"
}
REQUESTED_SELECTED_VERSIONS_MISMATCH -> {
val requested = potentialConflictReason.conflictingModuleId.withVersion(potentialConflictReason.requestedVersion)
"was initially compiled with \"$requested\""
}
BEHIND_CONFLICTING_DEPENDENCY -> {
"a dependency of the library with unknown version or versions mismatch: \"${potentialConflictReason.conflictingModuleId}\""
}
}
}
}
/**
* Find all incoming dependencies of [problemModuleId] that might conflict with [problemModuleId] because they were
* initially compiled with the different version of [problemModuleId] than the one used in the project.
*/
private fun findPotentiallyConflictingIncomingDependencies(
problemModuleId: ResolvedDependencyId,
allModules: Map<ResolvedDependencyId, ResolvedDependency>
): Map<ResolvedDependencyId, PotentialConflictDescription> {
val dependencyStatesMap: MutableMap<ResolvedDependencyId, MutableSet<DependencyState>> = mutableMapOf()
fun recurse(moduleId: ResolvedDependencyId, aboveConflictingDependency: Boolean) {
val module = allModules.findMatchingModule(moduleId)
module.requestedVersionsByIncomingDependencies.forEach { (incomingDependencyId, requestedVersion) ->
if (incomingDependencyId == ResolvedDependencyId.SOURCE_CODE_MODULE_ID) return@forEach
val dependencyState: DependencyState = when {
aboveConflictingDependency -> {
// Can't guarantee that any library that is an incoming dependency of a potentially conflicting dependency
// is not a potentially conflicting dependency itself.
DependencyState(
conflictReason = PotentialConflictReason(
kind = BEHIND_CONFLICTING_DEPENDENCY,
conflictingModuleId = module.id
)
)
}
module.selectedVersion.isEmpty() -> {
// Selected version is empty (unknown). Can't guarantee that this is the same library that was requested even if
// the requested version is also empty (unknown).
DependencyState(
conflictReason = PotentialConflictReason(
kind = UNKNOWN_SELECTED_VERSION,
conflictingModuleId = module.id,
requestedVersion = requestedVersion
)
)
}
requestedVersion != module.selectedVersion -> {
// The requested and selected versions don't match.
DependencyState(
conflictReason = PotentialConflictReason(
kind = REQUESTED_SELECTED_VERSIONS_MISMATCH,
conflictingModuleId = module.id,
requestedVersion = requestedVersion,
selectedVersion = module.selectedVersion
)
)
}
else -> DependencyState.SUCCESS
}
val dependencyStates: MutableSet<DependencyState> = dependencyStatesMap.getOrPut(incomingDependencyId) { mutableSetOf() }
val notBeenHereYet = dependencyStates.add(dependencyState)
if (notBeenHereYet) {
// Don't visit the same dependency twice.
recurse(moduleId = incomingDependencyId, aboveConflictingDependency = dependencyState.isConflicting)
}
}
}
recurse(moduleId = problemModuleId, aboveConflictingDependency = false)
return dependencyStatesMap.describeDependencyStates { potentialConflictReason ->
when (potentialConflictReason.kind) {
UNKNOWN_SELECTED_VERSION -> {
"depends on the library with unknown version: \"${potentialConflictReason.conflictingModuleId}\""
}
REQUESTED_SELECTED_VERSIONS_MISMATCH -> {
val requested = potentialConflictReason.conflictingModuleId.withVersion(potentialConflictReason.requestedVersion)
val selected = potentialConflictReason.conflictingModuleId.withVersion(potentialConflictReason.selectedVersion)
"was compiled against \"$requested\" but \"$selected\" is used in the project"
}
BEHIND_CONFLICTING_DEPENDENCY -> {
"depends on the library with unknown version or versions mismatch: \"${potentialConflictReason.conflictingModuleId}\""
}
}
}
}
private typealias PotentialConflictDescription = String
private class PotentialConflictReason(
val kind: PotentialConflictKind,
val conflictingModuleId: ResolvedDependencyId,
val requestedVersion: ResolvedDependencyVersion = ResolvedDependencyVersion.EMPTY,
val selectedVersion: ResolvedDependencyVersion = ResolvedDependencyVersion.EMPTY
) {
override fun equals(other: Any?) =
other is PotentialConflictReason && other.kind == kind && other.conflictingModuleId == conflictingModuleId
override fun hashCode() = kind.hashCode() + 31 * conflictingModuleId.hashCode()
companion object {
val Collection<PotentialConflictReason>.mostSignificantConflictReasons: Collection<PotentialConflictReason>
get() {
val mapping: Map<PotentialConflictKind, List<PotentialConflictReason>> = groupBy { it.kind }
val mostSignificantConflictKind: PotentialConflictKind = mapping.keys.mostSignificantConflictKind ?: return emptyList()
return mapping.getValue(mostSignificantConflictKind)
}
}
}
// Important: Enum entries must be declared in the order of most-to-least significant reasons.
private enum class PotentialConflictKind {
UNKNOWN_SELECTED_VERSION,
REQUESTED_SELECTED_VERSIONS_MISMATCH,
BEHIND_CONFLICTING_DEPENDENCY;
companion object {
val Collection<PotentialConflictKind>.mostSignificantConflictKind: PotentialConflictKind?
get() = minOrNull()
}
}
private data class DependencyState(val conflictReason: PotentialConflictReason? /* Null assumes success. */) {
val isConflicting: Boolean get() = conflictReason != null
companion object {
val SUCCESS = DependencyState(conflictReason = null)
}
}
private fun Map<ResolvedDependencyId, MutableSet<DependencyState>>.describeDependencyStates(
getDescription: (PotentialConflictReason) -> PotentialConflictDescription
): Map<ResolvedDependencyId, PotentialConflictDescription> = mapNotNull { (dependencyId, dependencyStates) ->
val mostSignificantConflictReasons = dependencyStates.mapNotNull { it.conflictReason }.mostSignificantConflictReasons
when {
mostSignificantConflictReasons.isEmpty() -> {
// No conflicts.
return@mapNotNull null
}
mostSignificantConflictReasons.first().kind == BEHIND_CONFLICTING_DEPENDENCY -> {
// Ignore dependency of conflicting dependency if this library has no conflicts when referred from
// non-conflicting dependencies.
val hasNoConflictsBehindNonConflictingDependencies = dependencyStates.any { !it.isConflicting }
if (hasNoConflictsBehindNonConflictingDependencies) return@mapNotNull null
}
else -> Unit
}
dependencyId to mostSignificantConflictReasons.joinToString(separator = "; ") { getDescription(it) }
}.toMap()
private fun StringBuilder.appendPotentiallyConflictingDependencies(
header: String,
allModules: Map<ResolvedDependencyId, ResolvedDependency>,
potentiallyConflictingDependencies: Map<ResolvedDependencyId, PotentialConflictDescription>,
moduleIdComparator: Comparator<ResolvedDependencyId>
) {
if (potentiallyConflictingDependencies.isEmpty()) return
append("\n\n$header")
val paddingSize = (potentiallyConflictingDependencies.size + 1).toString().length
potentiallyConflictingDependencies.toSortedMap(moduleIdComparator).entries.forEachIndexed { index, (moduleId, potentialConflictReason) ->
val padding = (index + 1).toString().padStart(paddingSize, ' ')
val moduleIdWithVersion = allModules.getValue(moduleId).moduleIdWithVersion
append("\n$padding. \"$moduleIdWithVersion\" ($potentialConflictReason)")
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2010-2021 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.backend.common.serialization.linkerissues
import org.jetbrains.kotlin.backend.common.serialization.IrModuleDeserializer
import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin
import org.jetbrains.kotlin.descriptors.konan.KlibModuleOrigin
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.library.KotlinLibrary
import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.utils.*
open class UserVisibleIrModulesSupport(protected val externalDependenciesLoader: ExternalDependenciesLoader) {
/**
* Load external [ResolvedDependency]s provided by the build system. These dependencies:
* - all have [ResolvedDependency.selectedVersion] specified
* - keep the information about which modules are first-level dependencies (i.e. the modules that the source code module
* directly depends on) and indirect (transitive) dependencies
* - miss modules provided by Kotlin/Native distribution (stdlib, endorsed and platform libraries), as they are
* not visible to the build system
*/
interface ExternalDependenciesLoader {
fun load(): Collection<ResolvedDependency>
companion object {
val EMPTY = object : ExternalDependenciesLoader {
override fun load(): Collection<ResolvedDependency> = emptyList()
}
fun from(externalDependenciesFile: File?, onMalformedExternalDependencies: (String) -> Unit): ExternalDependenciesLoader =
if (externalDependenciesFile != null)
object : ExternalDependenciesLoader {
override fun load(): Collection<ResolvedDependency> {
return if (externalDependenciesFile.exists) {
// Deserialize external dependencies from the [externalDependenciesFile].
val externalDependenciesText = String(externalDependenciesFile.readBytes())
ResolvedDependenciesSupport.deserialize(externalDependenciesText) { lineNo, line ->
onMalformedExternalDependencies("Malformed external dependencies at $externalDependenciesFile:$lineNo: $line")
}
} else emptyList()
}
}
else
EMPTY
}
}
fun getUserVisibleModuleId(deserializer: IrModuleDeserializer): ResolvedDependencyId {
val nameFromMetadataModuleHeader: String = deserializer.moduleFragment.name.asStringStripSpecialMarkers()
val nameFromKlibManifest: String? = deserializer.kotlinLibrary?.uniqueName
return ResolvedDependencyId(listOfNotNull(nameFromMetadataModuleHeader, nameFromKlibManifest))
}
open fun getUserVisibleModules(deserializers: Collection<IrModuleDeserializer>): Map<ResolvedDependencyId, ResolvedDependency> {
return mergedModules(deserializers)
}
/**
* Load [ResolvedDependency]s that represent all libraries participating in the compilation. Includes external dependencies,
* but without version and hierarchy information. Also includes the libraries that are not visible to the build system
* (and therefore are missing in [ExternalDependenciesLoader.load]) but are provided by the compiler. For Kotlin/Native such
* libraries are stdlib, endorsed and platform libraries.
*/
protected open fun modulesFromDeserializers(deserializers: Collection<IrModuleDeserializer>): Map<ResolvedDependencyId, ResolvedDependency> {
val modules: Map<ResolvedDependencyId, ModuleWithUninitializedDependencies> = deserializers.associate { deserializer ->
val moduleId = getUserVisibleModuleId(deserializer)
val module = ResolvedDependency(
id = moduleId,
// TODO: support extracting all the necessary details for non-Native libs: selectedVersion, requestedVersions, artifacts
selectedVersion = ResolvedDependencyVersion.EMPTY,
// Assumption: As we don't know for sure which modules the source code module depends on directly and which modules
// it depends on transitively, so let's assume it depends on all modules directly.
requestedVersionsByIncomingDependencies = mutableMapOf(ResolvedDependencyId.SOURCE_CODE_MODULE_ID to ResolvedDependencyVersion.EMPTY),
artifactPaths = mutableSetOf()
)
val outgoingDependencyIds = deserializer.moduleDependencies.map { getUserVisibleModuleId(it) }
moduleId to ModuleWithUninitializedDependencies(module, outgoingDependencyIds)
}
// Stamp dependencies.
return modules.stampDependenciesWithRequestedVersionEqualToSelectedVersion()
}
/**
* The result of the merge of [ExternalDependenciesLoader.load] and [modulesFromDeserializers].
*/
protected fun mergedModules(deserializers: Collection<IrModuleDeserializer>): MutableMap<ResolvedDependencyId, ResolvedDependency> {
// First, load external dependencies.
val externalDependencyModules: Collection<ResolvedDependency> = externalDependenciesLoader.load()
val externalDependencyModulesByNames: Map</* unique name */ String, ResolvedDependency> =
mutableMapOf<String, ResolvedDependency>().apply {
externalDependencyModules.forEach { externalDependency ->
externalDependency.id.uniqueNames.forEach { uniqueName ->
this[uniqueName] = externalDependency
}
}
}
fun findMatchingExternalDependencyModule(moduleId: ResolvedDependencyId): ResolvedDependency? =
moduleId.uniqueNames.firstNotNullOfOrNull { uniqueName -> externalDependencyModulesByNames[uniqueName] }
// The build system may express a group of modules where one module is a library KLIB and one or more modules
// are just C-interop KLIBs as a single module with multiple artifacts. We need to expand them so that every particular
// module/artifact will be represented as an individual [ResolvedDependency] instance.
val artifactPathsToOriginModules: MutableMap<ResolvedDependencyArtifactPath, ResolvedDependency> = mutableMapOf()
externalDependencyModules.forEach { originModule ->
originModule.artifactPaths.forEach { artifactPath -> artifactPathsToOriginModules[artifactPath] = originModule }
}
// Collect "provided" modules, i.e. modules that are missing in "external dependencies".
val providedModules = mutableListOf<ResolvedDependency>()
// Next, merge external dependencies with dependencies from deserializers.
modulesFromDeserializers(deserializers).forEach { (moduleId, module) ->
val externalDependencyModule = findMatchingExternalDependencyModule(moduleId)
if (externalDependencyModule != null) {
// Just add missing dependencies to the same module in [mergedModules].
module.requestedVersionsByIncomingDependencies.forEach { (incomingDependencyId, requestedVersion) ->
val adjustedIncomingDependencyId = findMatchingExternalDependencyModule(incomingDependencyId)?.id
?: incomingDependencyId
if (adjustedIncomingDependencyId !in externalDependencyModule.requestedVersionsByIncomingDependencies) {
externalDependencyModule.requestedVersionsByIncomingDependencies[adjustedIncomingDependencyId] = requestedVersion
}
}
} else {
val originModuleVersion = module.artifactPaths.firstNotNullOfOrNull { artifactPathsToOriginModules[it] }?.selectedVersion
if (originModuleVersion != null) {
// Handle artifacts that needs to be represented as individual [ResolvedDependency] objects.
module.selectedVersion = originModuleVersion
val incomingDependencyIdsToStampRequestedVersion = module.requestedVersionsByIncomingDependencies
.mapNotNull { (incomingDependencyId, requestedVersion) ->
if (requestedVersion.isEmpty()) incomingDependencyId else null
}
incomingDependencyIdsToStampRequestedVersion.forEach { incomingDependencyId ->
module.requestedVersionsByIncomingDependencies[incomingDependencyId] = originModuleVersion
}
} else {
// Just keep the module as is. If it has no incoming dependencies, then treat it as
// the first-level dependency module (i.e. the module that only the source code module depends on).
if (module.requestedVersionsByIncomingDependencies.isEmpty()) {
module.requestedVersionsByIncomingDependencies[ResolvedDependencyId.SOURCE_CODE_MODULE_ID] = module.selectedVersion
}
}
// Patch incoming dependencies.
module.requestedVersionsByIncomingDependencies.mapNotNull { (incomingDependencyId, requestedVersion) ->
val adjustedIncomingDependencyId = findMatchingExternalDependencyModule(incomingDependencyId)?.id
?: return@mapNotNull null
Triple(incomingDependencyId, adjustedIncomingDependencyId, requestedVersion)
}.forEach { (incomingDependencyId, adjustedIncomingDependencyId, requestedVersion) ->
module.requestedVersionsByIncomingDependencies.remove(incomingDependencyId)
module.requestedVersionsByIncomingDependencies[adjustedIncomingDependencyId] = requestedVersion
}
providedModules += module
}
}
return (externalDependencyModules + providedModules).associateByTo(mutableMapOf()) { it.id }
}
protected data class ModuleWithUninitializedDependencies(
val module: ResolvedDependency,
val outgoingDependencyIds: List<ResolvedDependencyId>
)
private fun Map<ResolvedDependencyId, ModuleWithUninitializedDependencies>.stampDependenciesWithRequestedVersionEqualToSelectedVersion(): Map<ResolvedDependencyId, ResolvedDependency> {
return mapValues { (moduleId, moduleWithUninitializedDependencies) ->
val (module, outgoingDependencyIds) = moduleWithUninitializedDependencies
outgoingDependencyIds.forEach { outgoingDependencyId ->
val dependencyModule = getValue(outgoingDependencyId).module
dependencyModule.requestedVersionsByIncomingDependencies[moduleId] = dependencyModule.selectedVersion
}
module
}
}
// TODO: find a way to access KotlinLibrary without descriptors
protected val IrModuleDeserializer.kotlinLibrary: KotlinLibrary?
get() = (moduleDescriptor.getCapability(KlibModuleOrigin.CAPABILITY) as? DeserializedKlibModuleOrigin)?.library
val moduleIdComparator: Comparator<ResolvedDependencyId> = Comparator { a, b ->
when {
a == b -> 0
// Kotlin libs go lower.
a.isKotlinLibrary && !b.isKotlinLibrary -> 1
!a.isKotlinLibrary && b.isKotlinLibrary -> -1
// Modules with simple names go upper as they are most likely user-made libs.
a.hasSimpleName && !b.hasSimpleName -> -1
!a.hasSimpleName && b.hasSimpleName -> 1
// Else: just compare by names.
else -> {
val aUniqueNames = a.uniqueNames.iterator()
val bUniqueNames = b.uniqueNames.iterator()
while (aUniqueNames.hasNext() && bUniqueNames.hasNext()) {
val diff = aUniqueNames.next().compareTo(bUniqueNames.next())
if (diff != 0) return@Comparator diff
}
when {
aUniqueNames.hasNext() -> 1
bUniqueNames.hasNext() -> -1
else -> 0
}
}
}
}
protected open val ResolvedDependencyId.isKotlinLibrary: Boolean
get() = uniqueNames.any { uniqueName -> uniqueName.startsWith(KOTLIN_LIBRARY_PREFIX) }
protected open val ResolvedDependencyId.hasSimpleName: Boolean
get() = uniqueNames.all { uniqueName -> uniqueName.none { it == '.' || it == ':' } }
companion object {
const val KOTLIN_LIBRARY_PREFIX = "org.jetbrains.kotlin"
val DEFAULT = UserVisibleIrModulesSupport(ExternalDependenciesLoader.EMPTY)
}
}

View File

@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.ir.backend.js.ic
import org.jetbrains.kotlin.backend.common.overrides.DefaultFakeOverrideClassFilter
import org.jetbrains.kotlin.backend.common.serialization.*
import org.jetbrains.kotlin.backend.common.serialization.encodings.BinarySymbolData
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.ir.backend.js.JsMappingState
import org.jetbrains.kotlin.ir.backend.js.JsStatementOrigins
import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsIrLinker
@@ -36,7 +35,6 @@ class IcFileDeserializer(
allowErrorNodes: Boolean,
deserializeInlineFunctions: Boolean,
val moduleDeserializer: IrModuleDeserializer,
val handleNoModuleDeserializerFound: (IdSignature, ModuleDescriptor, Collection<IrModuleDeserializer>) -> IrModuleDeserializer,
val originalEnqueue: IdSignature.(IcFileDeserializer) -> Unit,
val icFileData: SerializedIcDataForFile,
val mappingState: JsMappingState,
@@ -82,9 +80,8 @@ class IcFileDeserializer(
// TODO: reference lowered declarations cross-module
if (kind == BinarySymbolData.SymbolKind.FILE_SYMBOL) return file.symbol
val actualModuleDeserializer =
moduleDeserializer.findModuleDeserializerForTopLevelId(idSig)
?: handleNoModuleDeserializerFound(idSig, moduleDeserializer.moduleDescriptor, moduleDeserializer.moduleDependencies)
val actualModuleDeserializer = moduleDeserializer.findModuleDeserializerForTopLevelId(idSig)
?: linker.handleSignatureIdNotFoundInModuleWithDependencies(idSig, moduleDeserializer)
return actualModuleDeserializer.deserializeIrSymbol(idSig, kind)
}

View File

@@ -141,7 +141,6 @@ class IcModuleDeserializer(
allowErrorNodes,
strategy.inlineBodies,
moduleDeserializer,
linker::handleNoModuleDeserializerFound,
{ fileDeserializer -> originalEnqueue(fileDeserializer) },
icFileData,
mapping.state,

View File

@@ -58,6 +58,7 @@ data class File constructor(internal val javaPath: Path) {
get() = if (exists) listFiles else emptyList()
fun child(name: String) = File(this, name)
fun startsWith(another: File) = javaPath.startsWith(another.javaPath)
fun copyTo(destination: File) {
Files.copy(javaPath, destination.javaPath, StandardCopyOption.REPLACE_EXISTING)

View File

@@ -6,6 +6,8 @@
package org.jetbrains.kotlin.backend.konan
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.UserVisibleIrModulesSupport
import org.jetbrains.kotlin.backend.konan.serialization.KonanUserVisibleIrModulesSupport
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.config.kotlinSourceRoots
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
@@ -91,6 +93,15 @@ class KonanConfig(val project: Project, val configuration: CompilerConfiguration
val resolvedLibraries get() = resolve.resolvedLibraries
internal val userVisibleIrModulesSupport = KonanUserVisibleIrModulesSupport(
externalDependenciesLoader = UserVisibleIrModulesSupport.ExternalDependenciesLoader.from(
externalDependenciesFile = configuration.get(KonanConfigKeys.EXTERNAL_DEPENDENCIES)?.let(::File),
onMalformedExternalDependencies = { warningMessage ->
configuration.report(CompilerMessageSeverity.STRONG_WARNING, warningMessage)
}),
konanKlibDir = File(distribution.klib)
)
internal val cacheSupport = CacheSupport(configuration, resolvedLibraries, target, produce)
internal val cachedLibraries: CachedLibraries
@@ -206,10 +217,10 @@ class KonanConfig(val project: Project, val configuration: CompilerConfiguration
File(distribution.defaultNatives(target)).child("exceptionsSupport.bc").absolutePath
internal val nativeLibraries: List<String> =
configuration.getList(KonanConfigKeys.NATIVE_LIBRARY_FILES)
configuration.getList(KonanConfigKeys.NATIVE_LIBRARY_FILES)
internal val includeBinaries: List<String> =
configuration.getList(KonanConfigKeys.INCLUDED_BINARY_FILES)
configuration.getList(KonanConfigKeys.INCLUDED_BINARY_FILES)
internal val languageVersionSettings =
configuration.get(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS)!!

View File

@@ -101,7 +101,8 @@ internal fun Context.psiToIr(
stubGenerator,
irProviderForCEnumsAndCStructs,
exportedDependencies,
config.cachedLibraries
config.cachedLibraries,
config.userVisibleIrModulesSupport
).also { linker ->
// context.config.librariesWithDependencies could change at each iteration.

View File

@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.backend.common.overrides.FakeOverrideBuilder
import org.jetbrains.kotlin.backend.common.overrides.FakeOverrideClassFilter
import org.jetbrains.kotlin.backend.common.serialization.*
import org.jetbrains.kotlin.backend.common.serialization.encodings.BinarySymbolData
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.UserVisibleIrModulesSupport
import org.jetbrains.kotlin.backend.konan.CachedLibraries
import org.jetbrains.kotlin.backend.konan.descriptors.isInteropLibrary
import org.jetbrains.kotlin.backend.konan.ir.interop.IrProviderForCEnumAndCStructStubs
@@ -72,7 +73,8 @@ internal class KonanIrLinker(
private val stubGenerator: DeclarationStubGenerator,
private val cenumsProvider: IrProviderForCEnumAndCStructStubs,
exportedDependencies: List<ModuleDescriptor>,
private val cachedLibraries: CachedLibraries
private val cachedLibraries: CachedLibraries,
override val userVisibleIrModulesSupport: UserVisibleIrModulesSupport
) : KotlinIrLinker(currentModule, messageLogger, builtIns, symbolTable, exportedDependencies) {
companion object {
@@ -204,7 +206,7 @@ internal class KonanIrLinker(
val descriptor = resolveDescriptor(idSig)
val actualModule = descriptor.module
if (actualModule !== moduleDescriptor) {
val moduleDeserializer = deserializersForModules[actualModule.name.asString()] ?: error("No module deserializer for $actualModule")
val moduleDeserializer = resolveModuleDeserializer(actualModule, idSig)
moduleDeserializer.addModuleReachableTopLevel(idSig)
return symbolTable.referenceClassFromLinker(idSig)
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2010-2021 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.backend.konan.serialization
import org.jetbrains.kotlin.backend.common.serialization.IrModuleDeserializer
import org.jetbrains.kotlin.backend.common.serialization.linkerissues.UserVisibleIrModulesSupport
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.konan.library.KONAN_PLATFORM_LIBS_NAME_PREFIX
import org.jetbrains.kotlin.konan.library.KONAN_STDLIB_NAME
import org.jetbrains.kotlin.library.KotlinLibrary
import org.jetbrains.kotlin.library.RequiredUnresolvedLibrary
import org.jetbrains.kotlin.library.unresolvedDependencies
import org.jetbrains.kotlin.utils.ResolvedDependency
import org.jetbrains.kotlin.utils.ResolvedDependencyArtifactPath
import org.jetbrains.kotlin.utils.ResolvedDependencyId
import org.jetbrains.kotlin.utils.ResolvedDependencyVersion
class KonanUserVisibleIrModulesSupport(
externalDependenciesLoader: ExternalDependenciesLoader,
private val konanKlibDir: File
) : UserVisibleIrModulesSupport(externalDependenciesLoader) {
override fun getUserVisibleModules(deserializers: Collection<IrModuleDeserializer>): Map<ResolvedDependencyId, ResolvedDependency> {
return compressedModules(deserializers)
}
override fun modulesFromDeserializers(deserializers: Collection<IrModuleDeserializer>): Map<ResolvedDependencyId, ResolvedDependency> {
val moduleToCompilerVersion: MutableMap<ResolvedDependencyId, ResolvedDependencyVersion> = mutableMapOf()
val kotlinNativeBundledLibraries: MutableSet<ResolvedDependencyId> = mutableSetOf()
// Transform deserializers to [ModuleWithUninitializedDependencies]s.
val modules: Map<ResolvedDependencyId, ModuleWithUninitializedDependencies> = deserializers.mapNotNull { deserializer ->
val library: KotlinLibrary = deserializer.kotlinLibrary ?: return@mapNotNull null
val compilerVersion: ResolvedDependencyVersion = library.compilerVersion
val isDefaultLibrary = library.isDefaultLibrary
// For default libraries the version is the same as the version of the compiler.
// Note: Empty string means missing (unknown) version.
val libraryVersion: ResolvedDependencyVersion = if (isDefaultLibrary) compilerVersion else ResolvedDependencyVersion.EMPTY
val moduleId = getUserVisibleModuleId(deserializer)
val module = ResolvedDependency(
id = moduleId,
selectedVersion = libraryVersion,
requestedVersionsByIncomingDependencies = mutableMapOf(), // To be initialized in a separate pass below.
artifactPaths = mutableSetOf(ResolvedDependencyArtifactPath(library.libraryFile.absolutePath))
)
moduleToCompilerVersion[moduleId] = compilerVersion
if (isDefaultLibrary) kotlinNativeBundledLibraries += moduleId
// Don't rely on dependencies in IrModuleDeserializer. In Kotlin/Native each module depends on all other modules,
// and this contradicts with the real module dependencies as written in KLIB manifest files.
val outgoingDependencyIds = library.unresolvedDependencies.map { it.moduleId }
moduleId to ModuleWithUninitializedDependencies(module, outgoingDependencyIds)
}.toMap()
// Stamp dependencies.
return modules.mapValues { (moduleId, moduleWithUninitializedDependencies) ->
val (module, outgoingDependencyIds) = moduleWithUninitializedDependencies
outgoingDependencyIds.forEach { outgoingDependencyId ->
val outgoingDependencyModule = modules.getValue(outgoingDependencyId).module
if (outgoingDependencyId in kotlinNativeBundledLibraries) {
outgoingDependencyModule.requestedVersionsByIncomingDependencies[moduleId] = moduleToCompilerVersion.getValue(moduleId)
} else {
outgoingDependencyModule.requestedVersionsByIncomingDependencies[moduleId] = outgoingDependencyModule.selectedVersion
}
}
module
}
}
/**
* This is an optimization to avoid displaying 100+ Kotlin/Native platform libraries to the user.
* Instead, lets compress them into a single row and avoid excessive output.
*/
private fun compressedModules(deserializers: Collection<IrModuleDeserializer>): Map<ResolvedDependencyId, ResolvedDependency> {
val compressedModules: MutableMap<ResolvedDependencyId, ResolvedDependency> = mergedModules(deserializers)
var platformLibrariesVersion: ResolvedDependencyVersion? = null // Must be the same version to succeed.
val platformLibraries: MutableList<ResolvedDependency> = mutableListOf() // All platform libraries to be patched.
val outgoingDependencyIds: MutableSet<ResolvedDependencyId> = mutableSetOf() // All outgoing dependencies from platform libraries.
for ((moduleId, module) in compressedModules) {
if (moduleId.isKonanPlatformLibrary) {
if (ResolvedDependencyId.SOURCE_CODE_MODULE_ID !in module.requestedVersionsByIncomingDependencies) {
continue
}
platformLibrariesVersion = when (platformLibrariesVersion) {
null, module.selectedVersion -> module.selectedVersion
else -> {
// Multiple versions of platform libs. Give up.
return compressedModules
}
}
platformLibraries += module
} else {
module.requestedVersionsByIncomingDependencies.keys.forEach { incomingDependencyId ->
if (incomingDependencyId.isKonanPlatformLibrary) {
outgoingDependencyIds += moduleId
}
}
}
}
if (platformLibraries.isNotEmpty()) {
platformLibraries.forEach { it.visibleAsFirstLevelDependency = false }
val compressedModuleId = ResolvedDependencyId("$KONAN_PLATFORM_LIBS_NAME_PREFIX* (${platformLibraries.size} libraries)")
val compressedModule = ResolvedDependency(
id = compressedModuleId,
selectedVersion = platformLibrariesVersion!!,
requestedVersionsByIncomingDependencies = mutableMapOf(ResolvedDependencyId.SOURCE_CODE_MODULE_ID to platformLibrariesVersion),
artifactPaths = mutableSetOf()
)
outgoingDependencyIds.forEach { outgoingDependencyId ->
val outgoingDependency = compressedModules.getValue(outgoingDependencyId)
outgoingDependency.requestedVersionsByIncomingDependencies[compressedModuleId] = compressedModule.selectedVersion
}
compressedModules[compressedModuleId] = compressedModule
}
return compressedModules
}
private val KotlinLibrary.compilerVersion: ResolvedDependencyVersion
get() = ResolvedDependencyVersion(versions.compilerVersion.orEmpty())
// This is much safer check then KotlinLibrary.isDefault, which may return false even for "stdlib" when
// Kotlin/Native compiler is running with "-nostdlib", "-no-endorsed-libs", "-no-default-libs" arguments.
private val KotlinLibrary.isDefaultLibrary: Boolean
get() = libraryFile.startsWith(konanKlibDir)
override val ResolvedDependencyId.isKotlinLibrary: Boolean
get() = uniqueNames.any { uniqueName -> uniqueName == KONAN_STDLIB_NAME || uniqueName.startsWith(KOTLIN_LIBRARY_PREFIX) }
companion object {
private val RequiredUnresolvedLibrary.moduleId: ResolvedDependencyId
get() = ResolvedDependencyId(path) // Yep, it's named "path" but in fact holds unique name of the library.
private val ResolvedDependencyId.isKonanPlatformLibrary: Boolean
get() = uniqueNames.any { it.startsWith(KONAN_PLATFORM_LIBS_NAME_PREFIX) }
}
}