diff --git a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJsCache.kt b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJsCache.kt index b9de3fb8d0d..70198c56b75 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJsCache.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJsCache.kt @@ -136,8 +136,8 @@ open class IncrementalJsCache( } for ((srcFile, irData) in incrementalResults.irFileData) { - val (fileData, types, signatures, strings, declarations, bodies, fqn, debugInfos) = irData - irTranslationResults.put(srcFile, fileData, types, signatures, strings, declarations, bodies, fqn, debugInfos) + val (fileData, types, signatures, strings, declarations, bodies, fqn, fileMetadata, debugInfos) = irData + irTranslationResults.put(srcFile, fileData, types, signatures, strings, declarations, bodies, fqn, fileMetadata, debugInfos) } } @@ -268,6 +268,7 @@ private object IrTranslationResultValueExternalizer : DataExternalizer() + interface FileBackendSpecificMetadata { + fun toByteArray(): ByteArray + } + sealed class XStatementOrExpression { abstract fun toByteArray(): ByteArray @@ -1353,6 +1357,7 @@ open class IrFileSerializer( open fun backendSpecificExplicitRootExclusion(node: IrAnnotationContainer): Boolean = false open fun keepOrderOfProperties(property: IrProperty): Boolean = !property.isConst open fun backendSpecificSerializeAllMembers(irClass: IrClass) = false + open fun backendSpecificMetadata(irFile: IrFile): FileBackendSpecificMetadata? = null open fun memberNeedsSerialization(member: IrDeclaration): Boolean { val parent = member.parent @@ -1454,15 +1459,16 @@ open class IrFileSerializer( serializeExpectActualSubstitutionTable(proto) return SerializedIrFile( - proto.build().toByteArray(), - file.packageFqName.asString(), - file.path, - IrMemoryArrayWriter(protoTypeArray.map { it.toByteArray() }).writeIntoMemory(), - IrMemoryArrayWriter(protoIdSignatureArray.map { it.toByteArray() }).writeIntoMemory(), - IrMemoryStringWriter(protoStringArray).writeIntoMemory(), - IrMemoryArrayWriter(protoBodyArray.map { it.toByteArray() }).writeIntoMemory(), - IrMemoryDeclarationWriter(topLevelDeclarations).writeIntoMemory(), - IrMemoryStringWriter(protoDebugInfoArray).writeIntoMemory() + fileData = proto.build().toByteArray(), + fqName = file.packageFqName.asString(), + path = file.path, + types = IrMemoryArrayWriter(protoTypeArray.map { it.toByteArray() }).writeIntoMemory(), + signatures = IrMemoryArrayWriter(protoIdSignatureArray.map { it.toByteArray() }).writeIntoMemory(), + strings = IrMemoryStringWriter(protoStringArray).writeIntoMemory(), + bodies = IrMemoryArrayWriter(protoBodyArray.map { it.toByteArray() }).writeIntoMemory(), + declarations = IrMemoryDeclarationWriter(topLevelDeclarations).writeIntoMemory(), + debugInfo = IrMemoryStringWriter(protoDebugInfoArray).writeIntoMemory(), + backendSpecificMetadata = backendSpecificMetadata(file)?.toByteArray(), ) } diff --git a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/klib.kt b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/klib.kt index 2175ca63fe1..85b8aeef1b2 100644 --- a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/klib.kt +++ b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/klib.kt @@ -72,6 +72,7 @@ import org.jetbrains.kotlin.storage.StorageManager import org.jetbrains.kotlin.util.DummyLogger import org.jetbrains.kotlin.util.Logger import org.jetbrains.kotlin.utils.DFS +import org.jetbrains.kotlin.utils.addToStdlib.flatGroupBy import org.jetbrains.kotlin.utils.addToStdlib.ifTrue import org.jetbrains.kotlin.utils.memoryOptimizedFilter import java.io.File @@ -101,6 +102,9 @@ private val CompilerConfiguration.metadataVersion private val CompilerConfiguration.expectActualLinker: Boolean get() = get(CommonConfigurationKeys.EXPECT_ACTUAL_LINKER) ?: false +private val SerializedIrFile.fileMetadata: ByteArray + get() = backendSpecificMetadata ?: error("Expect file caches to have backendSpecificMetadata, but '$path' doesn't") + val CompilerConfiguration.resolverLogger: Logger get() = when (val messageLogger = this[IrMessageLogger.IR_MESSAGE_LOGGER]) { null -> DummyLogger @@ -627,6 +631,29 @@ private fun String.parseSerializedIrFileFingerprints(): List) { + val allExportedNameClashes = files + .flatGroupBy { JsIrFileMetadata.fromByteArray(it.irData.fileMetadata).exportedNames } + .filterValues { it.size > 1 } + + if (allExportedNameClashes.isEmpty()) return + + val nameClashesString = buildString { + allExportedNameClashes.forEach { (name, files) -> + appendLine(" * Next files contain declarations with @JsExport and name '$name'") + files.forEach { appendLine(" - ${it.irData.path}") } + } + } + + val message = """ + |There are clashes of declaration names that annotated with @JsExport in module '$moduleName'. + |${nameClashesString} + |Note, that this clash could affect the generated JS code in case of ES module kind usage + """.trimMargin() + + irMessageLogger.report(IrMessageLogger.Severity.WARNING, message, null) +} + fun serializeModuleIntoKlib( moduleName: String, configuration: CompilerConfiguration, @@ -674,7 +701,18 @@ fun serializeModuleIntoKlib( incrementalResultsConsumer?.run { processPackagePart(ioFile, compiledFile.metadata, empty, empty) with(compiledFile.irData) { - processIrFile(ioFile, fileData, types, signatures, strings, declarations, bodies, fqName.toByteArray(), debugInfo) + processIrFile( + ioFile, + fileData, + types, + signatures, + strings, + declarations, + bodies, + fqName.toByteArray(), + fileMetadata, + debugInfo, + ) } } } @@ -699,7 +737,11 @@ fun serializeModuleIntoKlib( processCompiledFileData(ioFile!!, compiledKotlinFile) } - val compiledKotlinFiles = (cleanFiles + additionalFiles) + val compiledKotlinFiles = (cleanFiles + additionalFiles).also { + if (builtInsPlatform == BuiltInsPlatform.JS) { + configuration.assertNoExportedNamesClashes(moduleName, it) + } + } val header = serializeKlibHeader( configuration.languageVersionSettings, moduleDescriptor, @@ -845,7 +887,8 @@ fun IncrementalDataProvider.getSerializedData(newSources: List): L strings, bodies, declarations, - debugInfo + debugInfo, + fileMetadata, ) } storage.add(KotlinFileSerializedData(metaFile.metadata, irFile)) diff --git a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileMetadata.kt b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileMetadata.kt new file mode 100644 index 00000000000..627ebd4bda6 --- /dev/null +++ b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileMetadata.kt @@ -0,0 +1,27 @@ +/* + * 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.ir.backend.js.lower.serialization.ir + +import org.jetbrains.kotlin.backend.common.serialization.IrFileSerializer +import org.jetbrains.kotlin.library.impl.toArray +import org.jetbrains.kotlin.library.encodings.WobblyTF8 +import org.jetbrains.kotlin.library.impl.IrMemoryArrayWriter +import org.jetbrains.kotlin.library.impl.IrMemoryStringWriter +import org.jetbrains.kotlin.library.impl.IrArrayMemoryReader + +class JsIrFileMetadata(val exportedNames: List) : IrFileSerializer.FileBackendSpecificMetadata { + override fun toByteArray(): ByteArray { + return IrMemoryStringWriter(exportedNames).writeIntoMemory() + } + + companion object { + fun fromByteArray(data: ByteArray): JsIrFileMetadata { + return JsIrFileMetadata( + exportedNames = IrArrayMemoryReader(data).toArray().map(WobblyTF8::decode) + ) + } + } +} \ No newline at end of file diff --git a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileSerializer.kt b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileSerializer.kt index 60da7e520ec..8835d888ed2 100644 --- a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileSerializer.kt +++ b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/lower/serialization/ir/JsIrFileSerializer.kt @@ -5,15 +5,22 @@ package org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir +import org.jetbrains.kotlin.backend.common.ir.isExpect import org.jetbrains.kotlin.backend.common.serialization.CompatibilityMode import org.jetbrains.kotlin.backend.common.serialization.DeclarationTable import org.jetbrains.kotlin.backend.common.serialization.IrFileSerializer import org.jetbrains.kotlin.config.LanguageVersionSettings import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer +import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall import org.jetbrains.kotlin.ir.symbols.IrSymbol import org.jetbrains.kotlin.ir.util.IrMessageLogger +import org.jetbrains.kotlin.ir.util.getAnnotation import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.isEffectivelyExternal import org.jetbrains.kotlin.name.FqName class JsIrFileSerializer( @@ -38,15 +45,38 @@ class JsIrFileSerializer( sourceBaseDirs = sourceBaseDirs ) { companion object { + private val JS_NAME_FQN = FqName("kotlin.js.JsName") private val JS_EXPORT_FQN = FqName("kotlin.js.JsExport") private val JS_EXPORT_IGNORE_FQN = FqName("kotlin.js.JsExport.Ignore") } - override fun backendSpecificExplicitRoot(node: IrAnnotationContainer): Boolean { - return node.annotations.hasAnnotation(JS_EXPORT_FQN) && !backendSpecificExplicitRootExclusion(node) + private fun IrAnnotationContainer.isExportedDeclaration(): Boolean { + return annotations.hasAnnotation(JS_EXPORT_FQN) && !isExportIgnoreDeclaration() } - override fun backendSpecificExplicitRootExclusion(node: IrAnnotationContainer): Boolean { - return node.annotations.hasAnnotation(JS_EXPORT_IGNORE_FQN) + private fun IrAnnotationContainer.isExportIgnoreDeclaration(): Boolean { + return annotations.hasAnnotation(JS_EXPORT_IGNORE_FQN) } + + private val IrDeclarationWithName.exportedName: String + get() = getAnnotation(JS_NAME_FQN)?.getSingleConstStringArgument() ?: name.toString() + + override fun backendSpecificExplicitRoot(node: IrAnnotationContainer) = node.isExportedDeclaration() + override fun backendSpecificExplicitRootExclusion(node: IrAnnotationContainer) = node.isExportIgnoreDeclaration() + override fun backendSpecificMetadata(irFile: IrFile): FileBackendSpecificMetadata { + val isFileExported = irFile.annotations.hasAnnotation(JS_EXPORT_FQN) + + val exportedNames = irFile.declarations.asSequence() + .filterIsInstance() + .filter { if (isFileExported) !it.isExportIgnoreDeclaration() else it.isExportedDeclaration() } + .filter { !it.isEffectivelyExternal() && !it.isExpect } + .map { it.exportedName } + .toList() + + return JsIrFileMetadata(exportedNames) + } + + @Suppress("UNCHECKED_CAST") + private fun IrConstructorCall.getSingleConstStringArgument() = + (getValueArgument(0) as IrConst).value } diff --git a/compiler/util-klib/src/org/jetbrains/kotlin/library/KotlinLibraryWriter.kt b/compiler/util-klib/src/org/jetbrains/kotlin/library/KotlinLibraryWriter.kt index 69be5f488ea..27a4e2b29a8 100644 --- a/compiler/util-klib/src/org/jetbrains/kotlin/library/KotlinLibraryWriter.kt +++ b/compiler/util-klib/src/org/jetbrains/kotlin/library/KotlinLibraryWriter.kt @@ -45,7 +45,8 @@ class SerializedIrFile( val strings: ByteArray, val bodies: ByteArray, val declarations: ByteArray, - val debugInfo: ByteArray? + val debugInfo: ByteArray?, + val backendSpecificMetadata: ByteArray?, ) class SerializedIrModule(val files: Collection) \ No newline at end of file diff --git a/js/js.config/src/org/jetbrains/kotlin/incremental/js/IncrementalResultsConsumer.kt b/js/js.config/src/org/jetbrains/kotlin/incremental/js/IncrementalResultsConsumer.kt index 0736959f38b..26c7db90337 100644 --- a/js/js.config/src/org/jetbrains/kotlin/incremental/js/IncrementalResultsConsumer.kt +++ b/js/js.config/src/org/jetbrains/kotlin/incremental/js/IncrementalResultsConsumer.kt @@ -50,7 +50,8 @@ interface IncrementalResultsConsumer { declarations: ByteArray, bodies: ByteArray, fqn: ByteArray, - debugInfo: ByteArray? + fileMetadata: ByteArray, + debugInfo: ByteArray?, ) } @@ -136,9 +137,10 @@ open class IncrementalResultsConsumerImpl : IncrementalResultsConsumer { declarations: ByteArray, bodies: ByteArray, fqn: ByteArray, - debugInfo: ByteArray? + fileMetadata: ByteArray, + debugInfo: ByteArray?, ) { - _irFileData[sourceFile] = IrTranslationResultValue(fileData, types, signatures, strings, declarations, bodies, fqn, debugInfo) + _irFileData[sourceFile] = IrTranslationResultValue(fileData, types, signatures, strings, declarations, bodies, fqn, fileMetadata, debugInfo) } } diff --git a/js/js.config/src/org/jetbrains/kotlin/incremental/js/TranslationResultValue.kt b/js/js.config/src/org/jetbrains/kotlin/incremental/js/TranslationResultValue.kt index ebf6d75b0f5..5dcba55574b 100644 --- a/js/js.config/src/org/jetbrains/kotlin/incremental/js/TranslationResultValue.kt +++ b/js/js.config/src/org/jetbrains/kotlin/incremental/js/TranslationResultValue.kt @@ -18,7 +18,6 @@ package org.jetbrains.kotlin.incremental.js data class TranslationResultValue(val metadata: ByteArray, val binaryAst: ByteArray, val inlineData: ByteArray) - data class IrTranslationResultValue( val fileData: ByteArray, val types: ByteArray, @@ -27,5 +26,6 @@ data class IrTranslationResultValue( val declarations: ByteArray, val bodies: ByteArray, val fqn: ByteArray, - val debugInfo: ByteArray? -) + val fileMetadata: ByteArray, + val debugInfo: ByteArray?, +) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt index db374187235..c4cdfe18d69 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/Kotlin2JsGradlePluginIT.kt @@ -356,6 +356,19 @@ class Kotlin2JsIrGradlePluginIT : KGPBaseTest() { } } + @DisplayName("klib compilation with the declarations name clash") + @GradleTest + fun testProjectWithExportedNamesClash(gradleVersion: GradleVersion) { + project("kotlin-js-invalid-project-with-exported-clash", gradleVersion) { + build("compileKotlinJs") { + assertOutputContains(""" + |There are clashes of declaration names that annotated with @JsExport in module 'kotlin-js-invalid-project-with-exported-clash'. + | * Next files contain declarations with @JsExport and name 'best' + """.trimMargin()) + } + } + } + @DisplayName("per-file with the declarations validation") @GradleTest fun testPerFileProjectWithResultFilesClash(gradleVersion: GradleVersion) { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/build.gradle.kts new file mode 100644 index 00000000000..26eb11c7cee --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("js") +} + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + js(IR) { + useEsModules() + binaries.executable() + nodejs() + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/custom/main.txt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/custom/main.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/gradle.properties b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/gradle.properties new file mode 100644 index 00000000000..c1009d30bf2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/gradle.properties @@ -0,0 +1,7 @@ +kotlin.tests.individualTaskReports=true +kotlin.incremental=false +kotlin.incremental.js=false +kotlin.incremental.js.ir=false +kotlin.incremental.js.klib=false +kotlin.incremental.multiplatform=false +kotlin.incremental.useClasspathSnapshot=false \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base1/Base.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base1/Base.kt new file mode 100644 index 00000000000..95cb6ea693a --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base1/Base.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2019 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 first + +@JsExport +fun best(): Int { + return 42 +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base2/base.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base2/base.kt new file mode 100644 index 00000000000..c3d04b8428d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/kotlin-js-invalid-project-with-exported-clash/src/main/kotlin/base2/base.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2010-2019 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 second + +@JsExport +@JsName("best") +public val foo = 44