[K/JS] Move ES modules logic to a new transformer with IC

This commit is contained in:
Artem Kobzar
2022-10-13 07:32:44 +00:00
committed by Space Team
parent 54deba63a1
commit de880ce9aa
88 changed files with 2476 additions and 1134 deletions
@@ -49,7 +49,7 @@ public class AntTaskJsTest extends AbstractAntTaskTest {
List<String> filePaths = CollectionsKt.map(fileNames, s -> getOutputFileByName(s).getAbsolutePath());
(useNashorn ? NashornJsTestChecker.INSTANCE : V8JsTestChecker.INSTANCE).check(filePaths, "out", "foo", "box", "OK", withModuleSystem);
(useNashorn ? NashornJsTestChecker.INSTANCE : V8JsTestChecker.INSTANCE).check(filePaths, "out", "foo", "box", "OK", withModuleSystem, null);
}
private void doJsAntTestForPostfixPrefix(@Nullable String prefix, @Nullable String postfix) throws Exception {
@@ -51,9 +51,7 @@ class ClassicJsBackendFacade(
"$KOTLIN_TEST_INTERNAL.setModuleId(\"$escapedModuleId\"); }\n" +
"$content\n"
ModuleKind.PLAIN -> content
ModuleKind.ES -> error("Module emulation markers are not supported for ES modules")
ModuleKind.PLAIN, ModuleKind.ES -> content
}
}
}
@@ -12,36 +12,29 @@ import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureDe
import org.jetbrains.kotlin.cli.common.isWindows
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.codegen.CompilerOutputSink
import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity
import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationOptions
import org.jetbrains.kotlin.ir.backend.js.codegen.generateEsModules
import org.jetbrains.kotlin.ir.backend.js.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.ic.JsExecutableProducer
import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerDesc
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformerTmp
import org.jetbrains.kotlin.ir.backend.js.SourceMapsInfo
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImplForJsIC
import org.jetbrains.kotlin.ir.util.SymbolTable
import org.jetbrains.kotlin.ir.util.irMessageLogger
import org.jetbrains.kotlin.js.config.ErrorTolerancePolicy
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.test.handlers.JsBoxRunner.Companion.TEST_FUNCTION
import org.jetbrains.kotlin.js.test.utils.esModulesSubDir
import org.jetbrains.kotlin.js.test.utils.extractTestPackage
import org.jetbrains.kotlin.js.test.utils.jsIrIncrementalDataProvider
import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi2ir.Psi2IrConfiguration
import org.jetbrains.kotlin.psi2ir.Psi2IrTranslator
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.test.DebugMode
import org.jetbrains.kotlin.test.directives.JsEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendOutputArtifact
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.frontend.classic.moduleDescriptorProvider
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.services.*
@@ -49,6 +42,9 @@ import org.jetbrains.kotlin.test.services.configuration.JsEnvironmentConfigurato
import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull
import java.io.File
const val REGULAR_EXTENSION = ".js"
const val ESM_EXTENSION = ".mjs"
class JsIrBackendFacade(
val testServices: TestServices,
private val firstTimeCompilation: Boolean
@@ -91,16 +87,16 @@ class JsIrBackendFacade(
else -> JsGenerationGranularity.WHOLE_PROGRAM
}
val testPackage = extractTestPackage(testServices)
val testPackage = extractTestPackage(testServices, ignoreEsModules = false)
val skipRegularMode = JsEnvironmentConfigurationDirectives.SKIP_REGULAR_MODE in module.directives
if (skipRegularMode) return null
if (JsEnvironmentConfigurator.incrementalEnabled(testServices)) {
val outputFile = if (firstTimeCompilation) {
File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name) + ".js")
File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name) + module.kind.extension)
} else {
File(JsEnvironmentConfigurator.getRecompiledJsModuleArtifactPath(testServices, module.name) + ".js")
File(JsEnvironmentConfigurator.getRecompiledJsModuleArtifactPath(testServices, module.name) + module.kind.extension)
}
val compiledModule = CompilerResult(
@@ -136,11 +132,12 @@ class JsIrBackendFacade(
PhaseConfig(jsPhases)
}
val loweredIr = compileIr(
irModuleFragment,
irModuleFragment.apply { resolveTestPathes() },
MainModule.Klib(inputArtifact.outputFile.absolutePath),
configuration,
dependencyModules,
dependencyModules.apply { forEach { it.resolveTestPathes() } },
emptyMap(),
irModuleFragment.irBuiltins,
symbolTable,
@@ -164,22 +161,27 @@ class JsIrBackendFacade(
module: TestModule,
loweredIr: LoweredIr,
granularity: JsGenerationGranularity,
): BinaryArtifacts.Js? {
val generateDts = JsEnvironmentConfigurationDirectives.GENERATE_DTS in module.directives
): BinaryArtifacts.Js {
val mainArguments = JsEnvironmentConfigurator.getMainCallParametersForModule(module)
.run { if (shouldBeGenerated()) arguments() else null }
val runIrDce = JsEnvironmentConfigurationDirectives.RUN_IR_DCE in module.directives
val onlyIrDce = JsEnvironmentConfigurationDirectives.ONLY_IR_DCE in module.directives
val esModules = JsEnvironmentConfigurationDirectives.ES_MODULES in module.directives
val runNewIr2Js = JsEnvironmentConfigurationDirectives.RUN_NEW_IR_2_JS in module.directives
val perModuleOnly = JsEnvironmentConfigurationDirectives.SPLIT_PER_MODULE in module.directives
val isEsModules = JsEnvironmentConfigurationDirectives.ES_MODULES in module.directives ||
module.directives[JsEnvironmentConfigurationDirectives.MODULE_KIND].contains(ModuleKind.ES)
val outputFile = File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, TranslationMode.FULL) + ".js")
val dceOutputFile = File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, TranslationMode.FULL_DCE_MINIMIZED_NAMES) + ".js")
if (!esModules) {
if (runNewIr2Js) {
val transformer = IrModuleToJsTransformerTmp(loweredIr.context, mainArguments)
val outputFile = File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, TranslationMode.FULL) + module.kind.extension)
if (runNewIr2Js) {
val transformer = IrModuleToJsTransformerTmp(
loweredIr.context,
mainArguments,
moduleToName = JsIrModuleToPath(
testServices,
isEsModules && granularity != JsGenerationGranularity.WHOLE_PROGRAM
)
)
// If runIrDce then include DCE results
// If perModuleOnly then skip whole program
// (it.dce => runIrDce) && (perModuleOnly => it.perModule)
@@ -199,19 +201,14 @@ class JsIrBackendFacade(
relativeRequirePath = false
)
return BinaryArtifacts.Js.JsIrArtifact(outputFile, transformer.generateModule(loweredIr.allModules)).dump(module)
}
return BinaryArtifacts.Js.JsIrArtifact(outputFile, transformer.generateModule(loweredIr.allModules)).dump(module)
}
}
val options = JsGenerationOptions(generatePackageJson = true, generateTypeScriptDefinitions = generateDts)
generateEsModules(loweredIr, jsOutputSink(outputFile.parentFile.esModulesSubDir), mainArguments, granularity, options)
if (runIrDce) {
eliminateDeadDeclarations(loweredIr.allModules, loweredIr.context)
generateEsModules(loweredIr, jsOutputSink(dceOutputFile.parentFile.esModulesSubDir), mainArguments, granularity, options)
return BinaryArtifacts.Js.JsEsArtifact(outputFile, dceOutputFile).dump(module)
private fun IrModuleFragment.resolveTestPathes() {
JsIrPathReplacer(testServices).let {
files.forEach(it::lower)
}
return BinaryArtifacts.Js.JsEsArtifact(outputFile, null).dump(module)
}
private fun loadIrFromKlib(module: TestModule, configuration: CompilerConfiguration): IrModuleInfo {
@@ -238,55 +235,10 @@ class JsIrBackendFacade(
) { if (it == mainModuleLib) moduleDescriptor else testServices.jsLibraryProvider.getDescriptorByCompiledLibrary(it) }
}
private fun loadIrFromSources(
private fun BinaryArtifacts.Js.JsIrArtifact.dump(
module: TestModule,
configuration: CompilerConfiguration,
inputArtifact: ClassicFrontendOutputArtifact
): IrModuleInfo {
val errorPolicy = configuration.get(JSConfigurationKeys.ERROR_TOLERANCE_POLICY) ?: ErrorTolerancePolicy.DEFAULT
val messageLogger = configuration.irMessageLogger
val symbolTable = SymbolTable(IdSignatureDescriptor(JsManglerDesc), IrFactoryImplForJsIC(WholeWorldStageController()),)
val verifySignatures = JsEnvironmentConfigurationDirectives.SKIP_MANGLE_VERIFICATION !in module.directives
val psi2Ir = Psi2IrTranslator(
configuration.languageVersionSettings,
Psi2IrConfiguration(errorPolicy.allowErrors),
messageLogger::checkNoUnboundSymbols
)
val psi2IrContext = psi2Ir.createGeneratorContext(
inputArtifact.analysisResult.moduleDescriptor,
inputArtifact.analysisResult.bindingContext,
symbolTable
)
return getIrModuleInfoForSourceFiles(
psi2IrContext,
inputArtifact.project,
configuration,
inputArtifact.allKtFiles.values.toList(),
sortDependencies(JsEnvironmentConfigurator.getAllRecursiveLibrariesFor(module, testServices)),
emptyMap(),
symbolTable,
messageLogger,
loadFunctionInterfacesIntoStdlib = true,
verifySignatures,
) { testServices.jsLibraryProvider.getDescriptorByCompiledLibrary(it) }
}
private fun jsOutputSink(perFileOutputDir: File): CompilerOutputSink {
perFileOutputDir.deleteRecursively()
perFileOutputDir.mkdirs()
return object : CompilerOutputSink {
override fun write(module: String, path: String, content: String) {
val file = File(File(perFileOutputDir, module), path)
file.parentFile.mkdirs()
file.writeText(content)
}
}
}
private fun BinaryArtifacts.Js.JsIrArtifact.dump(module: TestModule, firstTimeCompilation: Boolean = true): BinaryArtifacts.Js.JsIrArtifact {
firstTimeCompilation: Boolean = true
): BinaryArtifacts.Js.JsIrArtifact {
val configuration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
val moduleId = configuration.getNotNull(CommonConfigurationKeys.MODULE_NAME)
val moduleKind = configuration.get(JSConfigurationKeys.MODULE_KIND, ModuleKind.PLAIN)
@@ -297,9 +249,15 @@ class JsIrBackendFacade(
if (dontSkipRegularMode) {
for ((mode, output) in compilerResult.outputs.entries) {
val outputFile = if (firstTimeCompilation) {
File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, mode) + ".js")
File(JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, mode) + moduleKind.extension)
} else {
File(JsEnvironmentConfigurator.getRecompiledJsModuleArtifactPath(testServices, module.name, mode) + ".js")
File(
JsEnvironmentConfigurator.getRecompiledJsModuleArtifactPath(
testServices,
module.name,
mode
) + moduleKind.extension
)
}
output.writeTo(outputFile, moduleId, moduleKind)
}
@@ -307,24 +265,13 @@ class JsIrBackendFacade(
if (generateDts) {
outputFile
.withReplacedExtensionOrNull("_v5.js", ".d.ts")!!
.withReplacedExtensionOrNull("_v5${moduleKind.extension}", ".d.ts")!!
.write(compilerResult.tsDefinitions ?: error("No ts definitions"))
}
return this
}
private fun BinaryArtifacts.Js.JsEsArtifact.dump(module: TestModule): BinaryArtifacts.Js.JsEsArtifact {
val configuration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
val moduleName = configuration.getNotNull(CommonConfigurationKeys.MODULE_NAME)
val esmTestFile = outputFile.parentFile.esModulesSubDir.resolve("test.mjs")
createEsTestFile(esmTestFile, moduleName)
val dceEsmTestFile = outputDceFile?.parentFile?.esModulesSubDir?.resolve("test.mjs") ?: return this
createEsTestFile(dceEsmTestFile, moduleName)
return this
}
private fun CompilationOutputs.writeTo(outputFile: File, moduleId: String, moduleKind: ModuleKind) {
val wrappedCode = ClassicJsBackendFacade.wrapWithModuleEmulationMarkers(jsCode, moduleId = moduleId, moduleKind = moduleKind)
outputFile.write(wrappedCode)
@@ -344,32 +291,37 @@ class JsIrBackendFacade(
writeText(text)
}
private fun createEsTestFile(file: File, moduleName: String) {
val customTestModule = testServices.moduleStructure.modules
.flatMap { it.files }
.singleOrNull { JsEnvironmentConfigurationDirectives.ENTRY_ES_MODULE in it.directives }
val customTestModuleText = customTestModule?.let { testServices.sourceFileProvider.getContentOfSourceFile(it) }
val defaultTestModule =
"""
import { box } from './${moduleName}/index.js';
let res = box();
if (res !== "OK") {
throw "Wrong result: " + String(res);
}
""".trimIndent()
file.writeText(customTestModuleText ?: defaultTestModule)
}
override fun shouldRunAnalysis(module: TestModule): Boolean {
return JsEnvironmentConfigurator.isMainModule(module, testServices)
}
}
val ModuleKind.extension: String
get() = when (this) {
ModuleKind.ES -> ESM_EXTENSION
else -> REGULAR_EXTENSION
}
val RegisteredDirectives.moduleKind: ModuleKind
get() = get(JsEnvironmentConfigurationDirectives.MODULE_KIND).singleOrNull()
?: if (contains(JsEnvironmentConfigurationDirectives.ES_MODULES)) ModuleKind.ES else ModuleKind.PLAIN
val TestModule.kind: ModuleKind
get() = directives.moduleKind
fun String.augmentWithModuleName(moduleName: String): String {
check(endsWith("_v5.js"))
val normalizedName = moduleName.run { if (isWindows) minify() else this }
return removeSuffix("_v5.js") + "-${normalizedName}_v5.js"
return if (moduleName.isPath()) {
replaceAfterLast(File.separator, moduleName.replace("./", ""))
} else {
val suffix = when {
endsWith(ESM_EXTENSION) -> ESM_EXTENSION
endsWith(REGULAR_EXTENSION) -> REGULAR_EXTENSION
else -> error("Unexpected file '$this' extension")
}
val normalizedName = moduleName.run { if (isWindows) minify() else this }
return removeSuffix("_v5$suffix") + "-${normalizedName}_v5$suffix"
}
}
// D8 ignores Windows settings related to extending of maximum path symbols count
@@ -380,4 +332,6 @@ fun String.minify(): String {
.replace("_minimal_for_test", "_min")
}
private fun String.isPath(): Boolean = contains("/")
fun File.augmentWithModuleName(moduleName: String): File = File(absolutePath.augmentWithModuleName(moduleName))
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2022 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.js.test.converters
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.safeName
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.configuration.JsEnvironmentConfigurator.Companion.getJsArtifactSimpleName
import org.jetbrains.kotlin.utils.addToStdlib.runIf
private typealias K = IrModuleFragment
private typealias V = String
class JsIrModuleToPath(val testServices: TestServices, shouldProvidePaths: Boolean) : Map<K, V> {
override val size = if (!shouldProvidePaths) 0 else 1
override val entries = emptySet<Map.Entry<K, V>>()
override val keys = emptySet<K>()
override val values = emptyList<V>()
override fun isEmpty() = size == 0
override fun containsKey(key: K): Boolean = !isEmpty()
override fun containsValue(value: V): Boolean = !isEmpty()
override operator fun get(key: K): V? {
return runIf(!isEmpty()) {
"./${getJsArtifactSimpleName(testServices, key.safeName)}_v5.mjs"
}
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2022 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.js.test.converters
import org.jetbrains.kotlin.backend.common.DeclarationTransformer
import org.jetbrains.kotlin.ir.backend.js.utils.JsAnnotations
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.js.test.utils.*
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.isJsFile
import org.jetbrains.kotlin.test.services.isMjsFile
import org.jetbrains.kotlin.test.services.moduleStructure
class JsIrPathReplacer(testServices: TestServices) : DeclarationTransformer {
private val replacements = testServices.collectReplacementsMap()
override fun lower(irFile: IrFile) {
super.lower(irFile)
irFile.replaceJsModulePath()
}
override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
return null.also {
declaration.replaceJsModulePath()
}
}
private fun IrAnnotationContainer.replaceJsModulePath() {
val jsModuleAnnotation = getAnnotation(JsAnnotations.jsModuleFqn) ?: return
@Suppress("UNCHECKED_CAST")
val stringLiteral = jsModuleAnnotation.getValueArgument(0) as IrConst<String>
val pathReplacement = stringLiteral.getReplacement() ?: return
jsModuleAnnotation.putValueArgument(0, pathReplacement)
}
private fun IrConst<String>.getReplacement(): IrConst<String>? {
val replacement = replacements[value] ?: replacements[value.replace("./", "")] ?: return null
return IrConstImpl.string(startOffset, endOffset, type, "./" + replacement.replace("./", ""))
}
private fun TestServices.collectReplacementsMap(): Map<String, String> {
return moduleStructure.modules.asSequence()
.map { module -> module to module.files.filter { it.isJsFile || it.isMjsFile } }
.filter { (_, files) -> files.isNotEmpty() }
.flatMap { (module, files) -> files.map { it.relativePath to module.getNameFor(it, this) } }
.plus(getAdditionalFiles(this).map { it.name to it.name })
.plus(getAdditionalMainFiles(this).map { it.name to it.name })
.toMap()
}
}
@@ -5,25 +5,15 @@
package org.jetbrains.kotlin.js.test.handlers
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode
import org.jetbrains.kotlin.js.testOld.engines.ExternalTool
import org.jetbrains.kotlin.js.test.utils.*
import org.jetbrains.kotlin.test.directives.JsEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.configuration.JsEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.defaultsProvider
import org.jetbrains.kotlin.test.services.moduleStructure
import java.io.File
private val v8tool by lazy { ExternalTool(System.getProperty("javascript.engine.path.V8")) }
class JsBoxRunner(testServices: TestServices) : AbstractJsArtifactsCollector(testServices) {
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
if (someAssertionWasFailed) return
if (JsEnvironmentConfigurationDirectives.ES_MODULES in testServices.moduleStructure.allDirectives) {
runEsCode()
} else {
if (!someAssertionWasFailed) {
runJsCode()
}
}
@@ -43,37 +33,30 @@ class JsBoxRunner(testServices: TestServices) : AbstractJsArtifactsCollector(tes
val dontSkipRegularMode = JsEnvironmentConfigurationDirectives.SKIP_REGULAR_MODE !in globalDirectives
if (dontSkipRegularMode) {
for (jsFiles in allJsFiles.values) {
runGeneratedCode(jsFiles, testModuleName, testPackage, withModuleSystem)
for ((mode, jsFiles) in allJsFiles) {
val entryModulePath = extractEntryModulePath(mode, testServices)
runGeneratedCode(entryModulePath, jsFiles, testModuleName, testPackage, withModuleSystem)
}
}
}
private fun runEsCode() {
val globalDirectives = testServices.moduleStructure.allDirectives
val esmOutputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices).esModulesSubDir
val esmDceOutputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, TranslationMode.FULL_DCE_MINIMIZED_NAMES).esModulesSubDir
val dontSkipRegularMode = JsEnvironmentConfigurationDirectives.SKIP_REGULAR_MODE !in globalDirectives
val runIrDce = JsEnvironmentConfigurationDirectives.RUN_IR_DCE in globalDirectives
if (dontSkipRegularMode) {
singleRunEsCode(esmOutputDir)
if (runIrDce) {
singleRunEsCode(esmDceOutputDir)
}
}
}
private fun singleRunEsCode(esmOutputDir: File) {
val perFileEsModuleFile = "$esmOutputDir/test.mjs"
val (allNonEsModuleFiles, inputJsFilesAfter) = extractAllFilesForEsRunner(testServices, esmOutputDir)
v8tool.run(*allNonEsModuleFiles.toTypedArray(), perFileEsModuleFile, *inputJsFilesAfter.toTypedArray())
}
private fun runGeneratedCode(jsFiles: List<String>, testModuleName: String?, testPackage: String?, withModuleSystem: Boolean) {
private fun runGeneratedCode(
entryModulePath: String?,
jsFiles: List<String>,
testModuleName: String?,
testPackage: String?,
withModuleSystem: Boolean
) {
getTestChecker(testServices)
.check(jsFiles, testModuleName, testPackage, TEST_FUNCTION, DEFAULT_EXPECTED_RESULT, withModuleSystem)
.check(
jsFiles,
testModuleName,
testPackage,
TEST_FUNCTION,
DEFAULT_EXPECTED_RESULT,
withModuleSystem,
entryModulePath,
)
}
companion object {
@@ -24,6 +24,7 @@ class JsDtsHandler(testServices: TestServices) : JsBinaryArtifactHandler(testSer
val referenceDtsFile = module.files.first().originalFile.withReplacedExtensionOrNull(".kt", ".d.ts")
?: error("Can't find reference .d.ts file")
val generatedDtsFile = info.outputFile.withReplacedExtensionOrNull("_v5.js", ".d.ts")
?: info.outputFile.withReplacedExtensionOrNull("_v5.mjs", ".d.ts")
?: error("Can't find generated .d.ts file")
val generatedDts = generatedDtsFile.readText()
@@ -10,9 +10,10 @@ import org.jetbrains.kotlin.js.JavaScript
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.test.JsAdditionalSourceProvider
import org.jetbrains.kotlin.js.test.converters.augmentWithModuleName
import org.jetbrains.kotlin.js.test.converters.extension
import org.jetbrains.kotlin.js.test.converters.kind
import org.jetbrains.kotlin.js.test.handlers.JsBoxRunner.Companion.TEST_FUNCTION
import org.jetbrains.kotlin.js.testOld.*
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.test.TargetBackend
@@ -30,14 +31,23 @@ import java.io.File
private const val MODULE_EMULATION_FILE = "${JsEnvironmentConfigurator.TEST_DATA_DIR_PATH}/moduleEmulation.js"
val File.esModulesSubDir: File
get() = File(absolutePath + "_esm")
fun TestModule.getNameFor(filePath: String, testServices: TestServices): String {
return JsEnvironmentConfigurator.getJsArtifactSimpleName(testServices, name) + "-js-" + filePath
}
private fun extractJsFiles(testServices: TestServices, modules: List<TestModule>): Pair<List<String>, List<String>> {
val outputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices)
fun TestModule.getNameFor(file: TestFile, testServices: TestServices): String {
return getNameFor(file.name, testServices)
}
private fun extractJsFiles(
testServices: TestServices,
modules: List<TestModule>,
mode: TranslationMode = TranslationMode.FULL,
): Pair<List<String>, List<String>> {
val outputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, mode)
fun copyInputJsFile(module: TestModule, inputJsFile: TestFile): String {
val newName = JsEnvironmentConfigurator.getJsArtifactSimpleName(testServices, module.name) + "-js-" + inputJsFile.name
val newName = module.getNameFor(inputJsFile, testServices)
val targetFile = File(outputDir, newName)
targetFile.writeText(inputJsFile.originalContent)
return targetFile.absolutePath
@@ -45,49 +55,74 @@ private fun extractJsFiles(testServices: TestServices, modules: List<TestModule>
val inputJsFiles = modules
.flatMap { module -> module.files.map { module to it } }
.filter { it.second.isJsFile }
.filter { it.second.isJsFile || it.second.isMjsFile }
val after = inputJsFiles
.filter { (_, inputJsFile) -> inputJsFile.name.endsWith("__after.js") }
.filter { (module, inputJsFile) -> inputJsFile.name.endsWith("__after${module.kind.extension}") }
.map { (module, inputJsFile) -> copyInputJsFile(module, inputJsFile) }
val before = inputJsFiles
.filterNot { (_, inputJsFile) -> inputJsFile.name.endsWith("__after.js") }
.filterNot { (module, inputJsFile) -> inputJsFile.name.endsWith("__after${module.kind.extension}") }
.map { (module, inputJsFile) -> copyInputJsFile(module, inputJsFile) }
return before to after
}
private fun getAdditionalFiles(testServices: TestServices): List<String> {
fun getAdditionalFilePathes(testServices: TestServices, mode: TranslationMode = TranslationMode.FULL): List<String> {
return getAdditionalFiles(testServices, mode, true).map { it.absolutePath }
}
fun getAdditionalFiles(
testServices: TestServices,
mode: TranslationMode = TranslationMode.FULL,
shouldCopyFiles: Boolean = false
): List<File> {
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
val withModuleSystem = testWithModuleSystem(testServices)
val additionalFiles = mutableListOf<String>()
if (withModuleSystem) additionalFiles += File(MODULE_EMULATION_FILE).absolutePath
val additionalFiles = mutableListOf<File>()
if (withModuleSystem) additionalFiles += File(MODULE_EMULATION_FILE)
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + JavaScript.DOT_EXTENSION)
.takeIf { it.exists() }
?.let { additionalFiles += it.absolutePath }
?.let { additionalFiles += it }
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + JavaScript.DOT_MODULE_EXTENSION)
.takeIf { it.exists() }
?.let {
File(JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, mode), it.name).apply {
if (shouldCopyFiles) it.copyTo(this, true)
}
}
?.let { additionalFiles += it }
return additionalFiles
}
private fun getAdditionalMjsFiles(testServices: TestServices): List<String> {
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
return originalFile.parentFile.resolve(originalFile.nameWithoutExtension + ".mjs")
.takeIf { it.exists() }
?.let { listOf(it.absolutePath) } ?: emptyList()
fun getAdditionalMainFilePathes(testServices: TestServices, mode: TranslationMode = TranslationMode.FULL): List<String> {
return getAdditionalMainFiles(testServices, mode, shouldCopyFiles = true).map { it.absolutePath }
}
private fun getAdditionalMainFiles(testServices: TestServices): List<String> {
fun getAdditionalMainFiles(
testServices: TestServices,
mode: TranslationMode = TranslationMode.FULL,
shouldCopyFiles: Boolean = false
): List<File> {
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
val additionalFiles = mutableListOf<String>()
val additionalFiles = mutableListOf<File>()
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + "__main.js")
.takeIf { it.exists() }
?.let { additionalFiles += it.absolutePath }
?.let { additionalFiles += it }
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + "__main.mjs")
.takeIf { it.exists() }
?.let {
File(JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, mode), it.name).apply {
if (shouldCopyFiles) it.copyTo(this, true)
}
}
?.let { additionalFiles += it }
return additionalFiles
}
@@ -99,26 +134,31 @@ fun testWithModuleSystem(testServices: TestServices): Boolean {
return mainModuleKind != ModuleKind.PLAIN && NO_JS_MODULE_SYSTEM !in globalDirectives
}
fun getModeOutputFilePath(testServices: TestServices, module: TestModule, mode: TranslationMode): String {
return JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, mode) + module.kind.extension
}
fun getAllFilesForRunner(
testServices: TestServices, modulesToArtifact: Map<TestModule, BinaryArtifacts.Js>
): Map<TranslationMode, List<String>> {
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
val commonFiles = JsAdditionalSourceProvider.getAdditionalJsFiles(originalFile.parent).map { it.absolutePath }
val (inputJsFilesBefore, inputJsFilesAfter) = extractJsFiles(testServices, testServices.moduleStructure.modules)
val additionalFiles = getAdditionalFiles(testServices)
val additionalMainFiles = getAdditionalMainFiles(testServices)
if (modulesToArtifact.values.any { it is BinaryArtifacts.Js.JsIrArtifact }) {
// JS IR
val (module, compilerResult) = modulesToArtifact.entries.mapNotNull { (m, c) -> (c as? BinaryArtifacts.Js.JsIrArtifact)?.let { m to c.compilerResult } }.single()
val (module, compilerResult) = modulesToArtifact.entries.mapNotNull { (m, c) -> (c as? BinaryArtifacts.Js.JsIrArtifact)?.let { m to c.compilerResult } }
.single()
val result = mutableMapOf<TranslationMode, List<String>>()
compilerResult.outputs.entries.forEach { (mode, outputs) ->
val paths = mutableListOf<String>()
val outputFile = JsEnvironmentConfigurator.getJsModuleArtifactPath(testServices, module.name, mode) + ".js"
val outputFile = getModeOutputFilePath(testServices, module, mode)
val (inputJsFilesBefore, inputJsFilesAfter) = extractJsFiles(testServices, testServices.moduleStructure.modules, mode)
val additionalFiles = getAdditionalFilePathes(testServices, mode)
val additionalMainFiles = getAdditionalMainFilePathes(testServices, mode)
outputs.dependencies.forEach { (moduleId, _) ->
paths += outputFile.augmentWithModuleName(moduleId)
}
@@ -129,12 +169,15 @@ fun getAllFilesForRunner(
return result
} else {
// Old BE and ES modules
val (inputJsFilesBefore, inputJsFilesAfter) = extractJsFiles(testServices, testServices.moduleStructure.modules)
val additionalFiles = getAdditionalFilePathes(testServices)
val additionalMainFiles = getAdditionalMainFilePathes(testServices)
// Old BE
val outputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices)
val dceOutputDir = JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, TranslationMode.FULL_DCE_MINIMIZED_NAMES)
val artifactsPaths = modulesToArtifact.values.map { it.outputFile.absolutePath }.filter { !File(it).isDirectory }
val allJsFiles = additionalFiles + inputJsFilesBefore +artifactsPaths + commonFiles + additionalMainFiles + inputJsFilesAfter
val allJsFiles = additionalFiles + inputJsFilesBefore + artifactsPaths + commonFiles + additionalMainFiles + inputJsFilesAfter
val result = mutableMapOf<TranslationMode, List<String>>()
@@ -154,38 +197,6 @@ fun getAllFilesForRunner(
}
}
fun extractAllFilesForEsRunner(testServices: TestServices, esmOutputDir: File): Pair<List<String>, List<String>> {
val modules = testServices.moduleStructure.modules
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
val commonFiles = JsAdditionalSourceProvider.getAdditionalJsFiles(originalFile.parent).map { it.absolutePath }
val (inputJsFilesBefore, inputJsFilesAfter) = extractJsFiles(testServices, modules)
val additionalFiles = getAdditionalFiles(testServices)
val additionalMjsFiles = getAdditionalMjsFiles(testServices)
val additionalMainFiles = getAdditionalMainFiles(testServices)
val allNonEsModuleFiles = additionalFiles + inputJsFilesBefore + commonFiles
// Copy __main file if present
if (additionalMainFiles.isNotEmpty()) {
val newFileName = File(esmOutputDir, "test.mjs")
newFileName.delete()
File(additionalMainFiles.first()).copyTo(newFileName)
}
// Copy all .mjs files into generated directory
modules.flatMap { it.files }
.filter { it.isMjsFile }
.map { File(esmOutputDir, it.name).writeText(it.originalContent) }
additionalMjsFiles.forEach { mjsFile ->
val outFile = File(esmOutputDir, File(mjsFile).name)
File(mjsFile).copyTo(outFile, overwrite = true)
}
return Pair(allNonEsModuleFiles, inputJsFilesAfter)
}
fun getOnlyJsFilesForRunner(testServices: TestServices, modulesToArtifact: Map<TestModule, BinaryArtifacts.Js>): List<String> {
return getAllFilesForRunner(testServices, modulesToArtifact).let {
it[TranslationMode.FULL] ?: it[TranslationMode.PER_MODULE]!!
@@ -215,8 +226,49 @@ fun getBoxFunction(testServices: TestServices): KtNamedFunction? {
}.singleOrNull()
}
fun extractTestPackage(testServices: TestServices): String? =
getBoxFunction(testServices)?.containingKtFile?.packageFqName?.asString()?.takeIf { it.isNotEmpty() }
fun extractTestPackage(testServices: TestServices, ignoreEsModules: Boolean = true): String? {
val runPlainBoxFunction = RUN_PLAIN_BOX_FUNCTION in testServices.moduleStructure.allDirectives
if (runPlainBoxFunction) return null
val ktFiles = testServices.moduleStructure.modules.flatMap { module ->
module.files
.filter { it.isKtFile }
.map {
val project = testServices.compilerConfigurationProvider.getProject(module)
module to testServices.sourceFileProvider.getKtFileForSourceFile(it, project)
}
}
val fileWithBoxFunction = ktFiles.find { (module, ktFile) ->
(!ignoreEsModules || module.kind != ModuleKind.ES) &&
ktFile.declarations.find { it is KtNamedFunction && it.name == TEST_FUNCTION } != null
} ?: return null
return fileWithBoxFunction.second.packageFqName.asString().takeIf { it.isNotEmpty() }
}
fun extractEntryModulePath(
mode: TranslationMode,
testServices: TestServices,
): String? =
if (getBoxFunction(testServices) == null) {
testServices.moduleStructure.modules
.find { JsEnvironmentConfigurator.isMainModule(it, testServices) }
?.run {
files
.find { it.isMjsFile && JsEnvironmentConfigurationDirectives.ENTRY_ES_MODULE in it.directives }
?.let {
JsEnvironmentConfigurator.getJsArtifactsOutputDir(testServices, mode).absolutePath +
File.separator + getNameFor(it, testServices)
}
}
} else {
testServices.moduleStructure.modules
.find { JsEnvironmentConfigurator.isMainModule(it, testServices) }
?.let { getModeOutputFilePath(testServices, it, mode) }
}
fun getTestChecker(testServices: TestServices): AbstractJsTestChecker {
val runTestInNashorn = java.lang.Boolean.getBoolean("kotlin.js.useNashorn")
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.js.engine.loadFiles
import org.junit.Assert
private const val DIST_DIR_JS_PATH = "dist/js/"
private const val ESM_EXTENSION = ".mjs"
fun createScriptEngine(): ScriptEngine {
return if (java.lang.Boolean.getBoolean("kotlin.js.useNashorn")) ScriptEngineNashorn() else ScriptEngineV8()
@@ -22,14 +23,26 @@ fun ScriptEngine.overrideAsserter() {
eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(this['kotlin-test'].kotlin.test.DefaultAsserter);")
}
private fun String.escapePath(): String {
return replace("\\", "/")
}
@Suppress("UNUSED_PARAMETER")
fun ScriptEngine.runTestFunction(
testModuleName: String?,
testPackageName: String?,
testFunctionName: String,
withModuleSystem: Boolean,
testFunctionArgs: String = "",
entryModulePath: String? = null,
): String {
if (withModuleSystem && testModuleName == null && entryModulePath == null) {
error("Entry point was not found. Please specify ENTRY_ES_MODULE directive near js file, if this is ES Modules test.")
}
var script = when {
entryModulePath != null && entryModulePath.endsWith(ESM_EXTENSION) -> "globalThis".also {
eval("import('${entryModulePath.escapePath()}').then(module => Object.assign(globalThis, module)).catch(console.error)")
}
withModuleSystem -> "\$kotlin_test_internal\$.require('" + testModuleName!! + "')"
testModuleName === null -> "this"
else -> testModuleName
@@ -51,9 +64,10 @@ abstract class AbstractJsTestChecker {
testPackageName: String?,
testFunctionName: String,
expectedResult: String,
withModuleSystem: Boolean
withModuleSystem: Boolean,
entryModulePath: String? = null,
) {
val actualResult = run(files, testModuleName, testPackageName, testFunctionName, "", withModuleSystem)
val actualResult = run(files, testModuleName, testPackageName, testFunctionName, "", withModuleSystem, entryModulePath)
Assert.assertEquals(expectedResult, actualResult.normalize())
}
@@ -76,9 +90,10 @@ abstract class AbstractJsTestChecker {
testPackageName: String?,
testFunctionName: String,
testFunctionArgs: String,
withModuleSystem: Boolean
withModuleSystem: Boolean,
entryModulePath: String? = null,
) = run(files) {
runTestFunction(testModuleName, testPackageName, testFunctionName, withModuleSystem, testFunctionArgs)
runTestFunction(testModuleName, testPackageName, testFunctionName, withModuleSystem, testFunctionArgs, entryModulePath)
}