[K/JS] Add warning for ES-modules on the klibgen stage on the uniqueness of the exported names from the module

This commit is contained in:
Artem Kobzar
2023-08-08 15:45:02 +00:00
committed by Space Team
parent 734a3e5716
commit a29fa428b3
15 changed files with 199 additions and 29 deletions
@@ -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<IrTransla
output.writeArray(value.declarations)
output.writeArray(value.bodies)
output.writeArray(value.fqn)
output.writeArray(value.fileMetadata)
value.debugInfo?.let { output.writeArray(it) }
}
@@ -302,9 +303,10 @@ private object IrTranslationResultValueExternalizer : DataExternalizer<IrTransla
val declarations = input.readArray()
val bodies = input.readArray()
val fqn = input.readArray()
val fileMetadata = input.readArray()
val debugInfos = input.readArrayOrNull()
return IrTranslationResultValue(fileData, types, signatures, strings, declarations, bodies, fqn, debugInfos)
return IrTranslationResultValue(fileData, types, signatures, strings, declarations, bodies, fqn, fileMetadata, debugInfos)
}
}
@@ -330,10 +332,11 @@ private class IrTranslationResultMap(
newDeclarations: ByteArray,
newBodies: ByteArray,
fqn: ByteArray,
debugInfos: ByteArray?
newFileMetadata: ByteArray,
debugInfos: ByteArray?,
) {
storage[pathConverter.toPath(sourceFile)] =
IrTranslationResultValue(newFiledata, newTypes, newSignatures, newStrings, newDeclarations, newBodies, fqn, debugInfos)
IrTranslationResultValue(newFiledata, newTypes, newSignatures, newStrings, newDeclarations, newBodies, fqn, newFileMetadata, debugInfos)
}
operator fun get(sourceFile: File): IrTranslationResultValue? =
@@ -26,7 +26,8 @@ class RemoteIncrementalResultsConsumer(
declarations: ByteArray,
bodies: ByteArray,
fqn: ByteArray,
debugInfo: ByteArray?
fileMetadata: ByteArray,
debugInfo: ByteArray?,
) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
@@ -147,6 +147,10 @@ open class IrFileSerializer(
protected val protoDebugInfoArray = arrayListOf<String>()
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(),
)
}
@@ -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<SerializedIrFileFin
return split(FILE_FINGERPRINTS_SEPARATOR).mapNotNull(SerializedIrFileFingerprint::fromString)
}
private fun CompilerConfiguration.assertNoExportedNamesClashes(moduleName: String, files: List<KotlinFileSerializedData>) {
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<KtSourceFile>): L
strings,
bodies,
declarations,
debugInfo
debugInfo,
fileMetadata,
)
}
storage.add(KotlinFileSerializedData(metaFile.metadata, irFile))
@@ -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<String>) : 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)
)
}
}
}
@@ -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<IrDeclarationWithName>()
.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<String>).value
}
@@ -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<SerializedIrFile>)
@@ -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)
}
}
@@ -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?,
)
@@ -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) {
@@ -0,0 +1,16 @@
plugins {
kotlin("js")
}
repositories {
mavenLocal()
mavenCentral()
}
kotlin {
js(IR) {
useEsModules()
binaries.executable()
nodejs()
}
}
@@ -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
@@ -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
}
@@ -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