diff --git a/compiler/tests-common/org/jetbrains/kotlin/test/KotlinTestUtils.java b/compiler/tests-common/org/jetbrains/kotlin/test/KotlinTestUtils.java index 98ccf71e346..37bd31ea8da 100644 --- a/compiler/tests-common/org/jetbrains/kotlin/test/KotlinTestUtils.java +++ b/compiler/tests-common/org/jetbrains/kotlin/test/KotlinTestUtils.java @@ -124,6 +124,7 @@ public class KotlinTestUtils { "(?://\\s*MODULE:\\s*([^()\\n]+)(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*)?" + "//\\s*FILE:\\s*(.*)$", Pattern.MULTILINE); private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("^//\\s*!([\\w_]+)(:\\s*(.*)$)?", Pattern.MULTILINE); + private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\r\\n|\\r|\\n"); public static final BindingTrace DUMMY_TRACE = new BindingTrace() { @NotNull @@ -648,6 +649,12 @@ public class KotlinTestUtils { @NotNull public static List createTestFiles(String testFileName, String expectedText, TestFileFactory factory) { + return createTestFiles(testFileName, expectedText, factory, false); + } + + @NotNull + public static List createTestFiles(String testFileName, String expectedText, TestFileFactory factory, + boolean preserveLocations) { Map directives = parseDirectives(expectedText); List testFiles = Lists.newArrayList(); @@ -681,7 +688,9 @@ public class KotlinTestUtils { else { end = expectedText.length(); } - String fileText = expectedText.substring(start, end); + String fileText = preserveLocations ? + substringKeepingLocations(expectedText, start, end) : + expectedText.substring(start,end); processedChars = end; testFiles.add(factory.createFile(module, fileName, fileText, directives)); @@ -730,6 +739,26 @@ public class KotlinTestUtils { return testFiles; } + private static String substringKeepingLocations(String string, int start, int end) { + Matcher matcher = LINE_SEPARATOR_PATTERN.matcher(string); + StringBuilder prefix = new StringBuilder(); + int lastLineOffset = 0; + while (matcher.find()) { + if (matcher.end() > start) { + break; + } + + lastLineOffset = matcher.end(); + prefix.append('\n'); + } + + while (lastLineOffset++ < start) { + prefix.append(' '); + } + + return prefix + string.substring(start, end); + } + private static List parseModuleList(@Nullable String dependencies) { if (dependencies == null) return Collections.emptyList(); return StringsKt.split(dependencies, Pattern.compile(MODULE_DELIMITER), 0); diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/JsLibraryUtils.kt b/compiler/util/src/org/jetbrains/kotlin/utils/JsLibraryUtils.kt index dcc79b9140b..7caad458fee 100644 --- a/compiler/util/src/org/jetbrains/kotlin/utils/JsLibraryUtils.kt +++ b/compiler/util/src/org/jetbrains/kotlin/utils/JsLibraryUtils.kt @@ -22,6 +22,7 @@ import com.intellij.util.Processor import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull import java.io.File import java.io.IOException +import java.util.zip.ZipEntry import java.util.zip.ZipFile object JsLibraryUtils { @@ -43,49 +44,53 @@ object JsLibraryUtils { } } - @JvmStatic fun traverseJsLibraries(libs: List, action: (content: String, path: String) -> Unit) { + @JvmStatic fun traverseJsLibraries(libs: List, action: (JsLibrary) -> Unit) { libs.forEach { traverseJsLibrary(it, action) } } - @JvmStatic fun traverseJsLibrary(lib: File, action: (content: String, path: String) -> Unit) { + @JvmStatic fun traverseJsLibrary(lib: File, action: (JsLibrary) -> Unit) { when { lib.isDirectory -> traverseDirectory(lib, action) FileUtil.isJarOrZip(lib) -> traverseArchive(lib, action) lib.name.endsWith(KotlinJavascriptMetadataUtils.JS_EXT) -> { - lib.runIfFileExists(action) + lib.runIfFileExists(lib.path, action) val jsFile = lib.withReplacedExtensionOrNull( KotlinJavascriptMetadataUtils.META_JS_SUFFIX, KotlinJavascriptMetadataUtils.JS_EXT ) - jsFile?.runIfFileExists(action) + jsFile?.runIfFileExists(jsFile.path, action) } } } - private fun File.runIfFileExists(action: (content: String, path: String) -> Unit) { + private fun File.runIfFileExists(relativePath: String, action: (JsLibrary) -> Unit) { if (isFile) { - action(FileUtil.loadFile(this), "") + action(JsLibrary(readText(), relativePath, correspondingSourceMapFile().contentIfExists())) } } private fun copyJsFilesFromDirectory(dir: File, outputLibraryJsPath: String) { - traverseDirectory(dir) { content, relativePath -> - FileUtil.writeToFile(File(outputLibraryJsPath, relativePath), content) + traverseDirectory(dir) { (content, path) -> + FileUtil.writeToFile(File(outputLibraryJsPath, path), content) } } - private fun processDirectory(dir: File, action: (content: String, relativePath: String) -> Unit) { + private fun File.contentIfExists(): String? = if (exists()) readText() else null + + private fun File.correspondingSourceMapFile(): File = File(parentFile, name + ".map") + + private fun processDirectory(dir: File, action: (JsLibrary) -> Unit) { FileUtil.processFilesRecursively(dir, Processor { file -> val relativePath = FileUtil.getRelativePath(dir, file) ?: throw IllegalArgumentException("relativePath should not be null $dir $file") - if (file.isFile && relativePath.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) { + if (relativePath.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) { val suggestedRelativePath = getSuggestedPath(relativePath) ?: return@Processor true - action(FileUtil.loadFile(file), suggestedRelativePath) + file.runIfFileExists(suggestedRelativePath, action) } true }) } - private fun traverseDirectory(dir: File, action: (content: String, relativePath: String) -> Unit) { + private fun traverseDirectory(dir: File, action: (JsLibrary) -> Unit) { try { processDirectory(dir, action) } @@ -95,26 +100,48 @@ object JsLibraryUtils { } private fun copyJsFilesFromZip(file: File, outputLibraryJsPath: String) { - traverseArchive(file) { content, relativePath -> - FileUtil.writeToFile(File(outputLibraryJsPath, relativePath), content) + traverseArchive(file) { library -> + FileUtil.writeToFile(File(outputLibraryJsPath, library.path), library.content) } } - private fun traverseArchive(file: File, action: (content: String, relativePath: String) -> Unit) { + private fun traverseArchive(file: File, action: (JsLibrary) -> Unit) { val zipFile = ZipFile(file.path) try { val zipEntries = zipFile.entries() + val librariesWithoutSourceMaps = mutableListOf() + val possibleMapFiles = mutableMapOf() + while (zipEntries.hasMoreElements()) { val entry = zipEntries.nextElement() val entryName = entry.name - if (!entry.isDirectory && entryName.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) { - val relativePath = getSuggestedPath(entryName) ?: continue + if (!entry.isDirectory) { + if (entryName.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) { + val relativePath = getSuggestedPath(entryName) ?: continue - val stream = zipFile.getInputStream(entry) - val content = FileUtil.loadTextAndClose(stream) - action(content, relativePath) + val stream = zipFile.getInputStream(entry) + val content = FileUtil.loadTextAndClose(stream) + librariesWithoutSourceMaps += JsLibrary(content, relativePath, null) + } + else if (entryName.endsWith(KotlinJavascriptMetadataUtils.JS_MAP_EXT)) { + possibleMapFiles[entryName.removeSuffix(KotlinJavascriptMetadataUtils.JS_MAP_EXT)] = entry + } } } + + librariesWithoutSourceMaps + .map { + val zipEntry = possibleMapFiles[it.path] + if (zipEntry != null) { + val stream = zipFile.getInputStream(zipEntry) + val content = FileUtil.loadTextAndClose(stream) + it.copy(sourceMapContent = content) + } + else { + it + } + } + .forEach(action) } catch (ex: IOException) { LOG.error("Could not extract files from archive ${file.name}: ${ex.message}") @@ -136,3 +163,5 @@ object JsLibraryUtils { return path } } + +data class JsLibrary(val content: String, val path: String, val sourceMapContent: String?) diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/KotlinJavascriptMetadataUtils.kt b/compiler/util/src/org/jetbrains/kotlin/utils/KotlinJavascriptMetadataUtils.kt index 2bccd6baf94..38cc186a5eb 100644 --- a/compiler/util/src/org/jetbrains/kotlin/utils/KotlinJavascriptMetadataUtils.kt +++ b/compiler/util/src/org/jetbrains/kotlin/utils/KotlinJavascriptMetadataUtils.kt @@ -57,6 +57,7 @@ class JsMetadataVersion(vararg numbers: Int) : BinaryVersion(*numbers) { object KotlinJavascriptMetadataUtils { const val JS_EXT: String = ".js" const val META_JS_SUFFIX: String = ".meta.js" + const val JS_MAP_EXT: String = ".js.map" private val KOTLIN_JAVASCRIPT_METHOD_NAME = "kotlin_module_metadata" private val KOTLIN_JAVASCRIPT_METHOD_NAME_PATTERN = "\\.kotlin_module_metadata\\(".toPattern() @@ -78,8 +79,8 @@ object KotlinJavascriptMetadataUtils { fun loadMetadata(file: File): List { assert(file.exists()) { "Library $file not found" } val metadataList = arrayListOf() - JsLibraryUtils.traverseJsLibrary(file) { content, _ -> - parseMetadata(content, metadataList) + JsLibraryUtils.traverseJsLibrary(file) { library -> + parseMetadata(library.content, metadataList) } return metadataList diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt index f062b012c74..eb45c2ee154 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt @@ -26,7 +26,9 @@ import org.jetbrains.kotlin.js.backend.ast.metadata.inlineStrategy import org.jetbrains.kotlin.js.config.JsConfig import org.jetbrains.kotlin.js.inline.util.IdentitySet import org.jetbrains.kotlin.js.inline.util.isCallInvocation +import org.jetbrains.kotlin.js.parser.OffsetToSourceMapping import org.jetbrains.kotlin.js.parser.parseFunction +import org.jetbrains.kotlin.js.parser.sourcemaps.* import org.jetbrains.kotlin.js.translate.context.Namer import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getModuleName import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension @@ -34,6 +36,7 @@ import org.jetbrains.kotlin.resolve.inline.InlineStrategy import org.jetbrains.kotlin.utils.JsLibraryUtils import org.jetbrains.kotlin.utils.sure import java.io.File +import java.io.StringReader // TODO: add hash checksum to defineModule? /** @@ -57,7 +60,14 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName * kotlinVariable: kotlin object variable. * The default variable is Kotlin, but it can be renamed by minifier. */ - data class ModuleInfo(val filePath: String, val fileContent: String, val moduleVariable: String, val kotlinVariable: String) + class ModuleInfo( + val filePath: String, + val fileContent: String, + val moduleVariable: String, + val kotlinVariable: String, + val offsetToSourceMapping: OffsetToSourceMapping, + val sourceMap: SourceMap? + ) private val moduleNameToInfo = HashMultimap.create() @@ -68,22 +78,40 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName moduleNameMap = buildModuleNameMap(fragments) - JsLibraryUtils.traverseJsLibraries(libs) { fileContent, filePath -> + JsLibraryUtils.traverseJsLibraries(libs) { (content, path, sourceMapContent) -> var current = 0 while (true) { - var index = fileContent.indexOf(DEFINE_MODULE_FIND_PATTERN, current) + var index = content.indexOf(DEFINE_MODULE_FIND_PATTERN, current) if (index < 0) break current = index + 1 - index = rewindToIdentifierStart(fileContent, index) - val preciseMatcher = DEFINE_MODULE_PATTERN.matcher(offset(fileContent, index)) + index = rewindToIdentifierStart(content, index) + val preciseMatcher = DEFINE_MODULE_PATTERN.matcher(offset(content, index)) if (!preciseMatcher.lookingAt()) continue val moduleName = preciseMatcher.group(3) val moduleVariable = preciseMatcher.group(4) val kotlinVariable = preciseMatcher.group(1) - moduleNameToInfo.put(moduleName, ModuleInfo(filePath, fileContent, moduleVariable, kotlinVariable)) + + val sourceMap = sourceMapContent?.let { + val result = SourceMapParser.parse(StringReader(it)) + when (result) { + is SourceMapSuccess -> result.value + is SourceMapError -> throw RuntimeException("Error parsing source map file: ${result.message}\n$it") + } + } + + val moduleInfo = ModuleInfo( + filePath = path, + fileContent = content, + moduleVariable = moduleVariable, + kotlinVariable = kotlinVariable, + offsetToSourceMapping = OffsetToSourceMapping(content), + sourceMap = sourceMap + ) + + moduleNameToInfo.put(moduleName, moduleInfo) } } } @@ -152,9 +180,16 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName offset++ } - val function = parseFunction(source, info.filePath, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram())) + val position = info.offsetToSourceMapping[offset] + val function = parseFunction(source, info.filePath, position, offset, ThrowExceptionOnErrorReporter, JsRootScope(JsProgram())) val moduleReference = moduleNameMap[tag] ?: currentModuleName.makeRef() + val sourceMap = info.sourceMap + if (sourceMap != null) { + val remapper = SourceMapLocationRemapper(mapOf(info.filePath to sourceMap)) + remapper.remap(function) + } + val replacements = hashMapOf(info.moduleVariable to moduleReference, info.kotlinVariable to Namer.kotlinObject()) replaceExternalNames(function, replacements) diff --git a/js/js.parser/src/com/google/gwt/dev/js/rhino/LineBuffer.java b/js/js.parser/src/com/google/gwt/dev/js/rhino/LineBuffer.java index 7576d2a34e0..5769eebf34e 100644 --- a/js/js.parser/src/com/google/gwt/dev/js/rhino/LineBuffer.java +++ b/js/js.parser/src/com/google/gwt/dev/js/rhino/LineBuffer.java @@ -64,9 +64,10 @@ final class LineBuffer { */ static final int BUFLEN = 256; - LineBuffer(Reader in, int lineno) { + LineBuffer(Reader in, CodePosition position) { this.in = in; - this.lineno = lineno; + this.lineno = position.getLine(); + this.lineStart = -position.getOffset(); } int read() throws IOException { diff --git a/js/js.parser/src/com/google/gwt/dev/js/rhino/TokenStream.java b/js/js.parser/src/com/google/gwt/dev/js/rhino/TokenStream.java index 52c49a10300..4d74230ed71 100644 --- a/js/js.parser/src/com/google/gwt/dev/js/rhino/TokenStream.java +++ b/js/js.parser/src/com/google/gwt/dev/js/rhino/TokenStream.java @@ -495,14 +495,15 @@ public class TokenStream { } public TokenStream(Reader in, - String sourceName, int lineno) + String sourceName, CodePosition position) { - this.in = new LineBuffer(in, lineno); + this.in = new LineBuffer(in, position); this.pushbackToken = EOF; this.sourceName = sourceName; flags = 0; - secondToLastPosition = new CodePosition(lineno, 0); - lastPosition = new CodePosition(lineno, 0); + secondToLastPosition = position; + lastPosition = position; + lastTokenPosition = position; } /* return and pop the token from the stream if it matches... diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/OffsetToSourceMapping.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/OffsetToSourceMapping.kt new file mode 100644 index 00000000000..f1807e5f88a --- /dev/null +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/OffsetToSourceMapping.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.parser + +import com.google.gwt.dev.js.rhino.CodePosition + +class OffsetToSourceMapping(text: String) { + private val data: IntArray + + init { + val lineSeparators = LINE_SEPARATOR.findAll(text).map { it.range.endInclusive + 1 } + data = (sequenceOf(0) + lineSeparators).toList().toIntArray() + } + + operator fun get(offset: Int): CodePosition { + val lineNumber = data.binarySearch(offset).let { if (it >= 0) it else -it - 2 } + return CodePosition(lineNumber, offset - data[lineNumber]) + } + + private companion object { + private val LINE_SEPARATOR = Regex("\\r\\n|\\r|\\n") + } +} \ No newline at end of file diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt index d5f8177197d..aa0ba95c5fb 100644 --- a/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/parserUtils.kt @@ -16,29 +16,26 @@ package org.jetbrains.kotlin.js.parser -import com.google.gwt.dev.js.* +import com.google.gwt.dev.js.JsAstMapper import com.google.gwt.dev.js.rhino.* - import org.jetbrains.kotlin.js.backend.ast.JsFunction import org.jetbrains.kotlin.js.backend.ast.JsFunctionScope import org.jetbrains.kotlin.js.backend.ast.JsScope import org.jetbrains.kotlin.js.backend.ast.JsStatement -import org.jetbrains.kotlin.js.common.SourceInfoImpl -import java.io.* +import java.io.Reader +import java.io.StringReader import java.util.* -private val FAKE_SOURCE_INFO = SourceInfoImpl(null, 0, 0, 0, 0) - fun parse(code: String, reporter: ErrorReporter, scope: JsScope, fileName: String): List { val insideFunction = scope is JsFunctionScope - val node = parse(code, 0, reporter, insideFunction, Parser::parse) + val node = parse(code, CodePosition(0, 0), 0, reporter, insideFunction, Parser::parse) return node.toJsAst(scope, fileName) { mapStatements(it) } } -fun parseFunction(code: String, fileName: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction = - parse(code, offset, reporter, insideFunction = false) { +fun parseFunction(code: String, fileName: String, position: CodePosition, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction = + parse(code, position, offset, reporter, insideFunction = false) { addObserver(FunctionParsingObserver()) primaryExpr(it) }.toJsAst(scope, fileName, JsAstMapper::mapFunction) @@ -65,6 +62,7 @@ private class FunctionParsingObserver : Observer { inline private fun parse( code: String, + startPosition: CodePosition, offset: Int, reporter: ErrorReporter, insideFunction: Boolean, @@ -73,7 +71,7 @@ private fun parse( Context.enter().errorReporter = reporter try { - val ts = TokenStream(StringReader(code, offset), "", FAKE_SOURCE_INFO.line) + val ts = TokenStream(StringReader(code, offset), "", startPosition) val parser = Parser(IRFactory(ts), insideFunction) return parser.parseAction(ts) as Node } finally { diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsLineNumberTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsLineNumberTest.kt index b8d37d55815..7b26d075469 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsLineNumberTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractJsLineNumberTest.kt @@ -39,61 +39,104 @@ import org.jetbrains.kotlin.js.test.utils.LineCollector import org.jetbrains.kotlin.js.test.utils.LineOutputToStringVisitor import org.jetbrains.kotlin.js.util.TextOutputImpl import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.serialization.js.JsModuleDescriptor +import org.jetbrains.kotlin.serialization.js.KotlinJavascriptSerializationUtil import org.jetbrains.kotlin.serialization.js.ModuleKind +import org.jetbrains.kotlin.test.KotlinTestUtils import org.jetbrains.kotlin.test.KotlinTestWithEnvironment +import org.jetbrains.kotlin.utils.DFS import java.io.ByteArrayOutputStream +import java.io.Closeable import java.io.File import java.io.PrintStream import java.nio.charset.Charset abstract class AbstractJsLineNumberTest : KotlinTestWithEnvironment() { fun doTest(filePath: String) { - val translator = K2JSTranslator(createConfig()) - val unit = TranslationUnit.SourceFile(createPsiFile(filePath)) - val translationResult = translator.translateUnits(listOf(unit), MainCallParameters.noCall()) + val file = File(filePath) + val sourceCode = file.readText() - if (translationResult !is TranslationResult.Success) { - val outputStream = ByteArrayOutputStream() - val collector = PrintingMessageCollector(PrintStream(outputStream), MessageRenderer.PLAIN_FULL_PATHS, true) - AnalyzerWithCompilerReport.reportDiagnostics(translationResult.diagnostics, collector) - val messages = outputStream.toByteArray().toString(Charset.forName("UTF-8")) - throw AssertionError("The following errors occurred compiling test:\n" + messages) + TestFileFactoryImpl().use { testFactory -> + val inputFiles = KotlinTestUtils.createTestFiles(file.name, sourceCode, testFactory, true) + val modules = inputFiles + .map { it.module }.distinct() + .associateBy { it.name } + + val orderedModules = DFS.topologicalOrder(modules.values) { module -> module.dependencies.mapNotNull { modules[it] } } + + orderedModules.asReversed().forEach { module -> + val baseOutputPath = module.outputFileName(file) + + val translator = K2JSTranslator(createConfig(module, file, modules)) + val units = module.files.map { TranslationUnit.SourceFile(createPsiFile(it.fileName)) } + val translationResult = translator.translateUnits(units, MainCallParameters.noCall()) + + if (translationResult !is TranslationResult.Success) { + val outputStream = ByteArrayOutputStream() + val collector = PrintingMessageCollector(PrintStream(outputStream), MessageRenderer.PLAIN_FULL_PATHS, true) + AnalyzerWithCompilerReport.reportDiagnostics(translationResult.diagnostics, collector) + val messages = outputStream.toByteArray().toString(Charset.forName("UTF-8")) + throw AssertionError("The following errors occurred compiling test:\n" + messages) + } + + val lineCollector = LineCollector() + lineCollector.accept(translationResult.program) + + val programOutput = TextOutputImpl() + translationResult.program.globalBlock.accept(LineOutputToStringVisitor(programOutput, lineCollector)) + val generatedCode = programOutput.toString() + with(File(baseOutputPath + "-lines.js")) { + parentFile.mkdirs() + writeText(generatedCode) + } + + File(baseOutputPath + ".js").writeText(translationResult.program.globalBlock.toString()) + + val moduleDescription = JsModuleDescriptor( + name = module.name, + data = translationResult.moduleDescriptor, + kind = ModuleKind.PLAIN, + imported = emptyList() + ) + val metaFileContent = KotlinJavascriptSerializationUtil.metadataAsString( + translationResult.bindingContext, moduleDescription) + File(baseOutputPath + ".meta.js").writeText(metaFileContent) + + val linesMatcher = module.files + .mapNotNull { LINES_PATTERN.find(File(it.fileName).readText()) } + .firstOrNull() + ?: error("'// LINES: ' comment was not found in source file. Generated code is:\n$generatedCode") + + val expectedLines = linesMatcher.groups[1]!!.value + val actualLines = lineCollector.lines + .dropLastWhile { it == null } + .joinToString(" ") { if (it == null) "*" else (it + 1).toString() } + + TestCase.assertEquals(generatedCode, expectedLines, actualLines) + } } - - val lineCollector = LineCollector() - lineCollector.accept(translationResult.program) - - val programOutput = TextOutputImpl() - translationResult.program.globalBlock.accept(LineOutputToStringVisitor(programOutput, lineCollector)) - val generatedCode = programOutput.toString() - val relativePath = File(filePath).relativeTo(File(BASE_PATH)).path.removeSuffix(".kt") - with(File(File(OUT_PATH), relativePath + ".js")) { - parentFile.mkdirs() - writeText(generatedCode) - } - - val sourceCode = FileUtil.loadFile(File(filePath)) - val linesMatcher = LINES_PATTERN.find(sourceCode) ?: - error("'// LINES: ' comment was not found in source file. Generated code is:\n$generatedCode") - - val expectedLines = linesMatcher.groups[1]!!.value - val actualLines = lineCollector.lines - .dropLastWhile { it == null } - .joinToString(" ") { if (it == null) "*" else (it + 1).toString() } - - TestCase.assertEquals(generatedCode, expectedLines, actualLines) } + private fun TestModule.outputFileName(file: File): String { + return outputPath(file) + "-" + name + } + + private fun outputPath(file: File) = File(OUT_PATH, file.relativeTo(File(BASE_PATH)).path.removeSuffix(".kt")).path + override fun createEnvironment(): KotlinCoreEnvironment { return KotlinCoreEnvironment.createForTests(testRootDisposable, CompilerConfiguration(), EnvironmentConfigFiles.JS_CONFIG_FILES) } - private fun createConfig(): JsConfig { + private fun createConfig(module: TestModule, inputFile: File, modules: Map): JsConfig { + val dependencies = module.dependencies + .mapNotNull { modules[it]?.outputFileName(inputFile) } + .map { "$it.meta.js" } + val configuration = environment.configuration.copy() - configuration.put(JSConfigurationKeys.LIBRARIES, JsConfig.JS_STDLIB + JsConfig.JS_KOTLIN_TEST) + configuration.put(JSConfigurationKeys.LIBRARIES, JsConfig.JS_STDLIB + JsConfig.JS_KOTLIN_TEST + dependencies ) - configuration.put(CommonConfigurationKeys.MODULE_NAME, "test") + configuration.put(CommonConfigurationKeys.MODULE_NAME, module.name) configuration.put(JSConfigurationKeys.MODULE_KIND, ModuleKind.PLAIN) configuration.put(JSConfigurationKeys.TARGET, EcmaVersion.v5) @@ -109,6 +152,44 @@ abstract class AbstractJsLineNumberTest : KotlinTestWithEnvironment() { return psiManager.findFile(fileSystem.findFileByPath(fileName)!!) as KtFile } + private inner class TestFileFactoryImpl : KotlinTestUtils.TestFileFactory, Closeable { + val tmpDir = KotlinTestUtils.tmpDir("js-tests") + val defaultModule = TestModule(BasicBoxTest.TEST_MODULE, emptyList()) + + override fun createFile(module: TestModule?, fileName: String, text: String, directives: Map): TestFile? { + val currentModule = module ?: defaultModule + + val temporaryFile = File(tmpDir, "${currentModule.name}/$fileName") + KotlinTestUtils.mkdirs(temporaryFile.parentFile) + temporaryFile.writeText(text, Charsets.UTF_8) + + + return TestFile(temporaryFile.absolutePath, currentModule) + } + + override fun createModule(name: String, dependencies: List, friends: List): TestModule? { + return TestModule(name, dependencies) + } + + override fun close() { + FileUtil.delete(tmpDir) + } + } + + private class TestModule( + val name: String, + dependencies: List + ) { + val dependencies = dependencies.toMutableList() + val files = mutableListOf() + } + + private class TestFile(val fileName: String, val module: TestModule) { + init { + module.files += this + } + } + companion object { private val DIR_NAME = "lineNumbers" private val LINES_PATTERN = Regex("^ *// *LINES: *(.*)$", RegexOption.MULTILINE) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt index 576d13ed673..f3523cd1a3b 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt @@ -87,9 +87,7 @@ abstract class BasicBoxTest( val outputPostfixFile = getOutputPostfixFile(filePath) TestFileFactoryImpl().use { testFactory -> - testFactory.defaultModule.moduleKind - - val inputFiles = KotlinTestUtils.createTestFiles(file.name, fileContent, testFactory) + val inputFiles = KotlinTestUtils.createTestFiles(file.name, fileContent, testFactory, true) val modules = inputFiles .map { it.module }.distinct() .map { it.name to it }.toMap() @@ -627,7 +625,7 @@ abstract class BasicBoxTest( private val METADATA_EXTENSION = "jsmeta" private val HEADER_FILE = "header.$METADATA_EXTENSION" - private val TEST_MODULE = "JS_TESTS" + val TEST_MODULE = "JS_TESTS" private val DEFAULT_MODULE = "main" private val TEST_FUNCTION = "box" private val OLD_MODULE_SUFFIX = "-old" diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/JsLineNumberTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsLineNumberTestGenerated.java index 1de9aed424d..73a68c3ff4a 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/JsLineNumberTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsLineNumberTestGenerated.java @@ -269,4 +269,19 @@ public class JsLineNumberTestGenerated extends AbstractJsLineNumberTest { String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/lineNumbers/whileWithComplexCondition.kt"); doTest(fileName); } + + @TestMetadata("js/js.translator/testData/lineNumbers/inlineMultiModule") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class InlineMultiModule extends AbstractJsLineNumberTest { + public void testAllFilesPresentInInlineMultiModule() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("js/js.translator/testData/lineNumbers/inlineMultiModule"), Pattern.compile("^([^_](.+))\\.kt$"), TargetBackend.JS, true); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/lineNumbers/inlineMultiModule/simple.kt"); + doTest(fileName); + } + } } diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/facade/TranslationResult.kt b/js/js.translator/src/org/jetbrains/kotlin/js/facade/TranslationResult.kt index eeb39c7ea6f..b2803c3dc8b 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/facade/TranslationResult.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/facade/TranslationResult.kt @@ -47,8 +47,8 @@ abstract class TranslationResult protected constructor(val diagnostics: Diagnost val program: JsProgram, diagnostics: Diagnostics, private val importedModules: List, - private val moduleDescriptor: ModuleDescriptor, - private val bindingContext: BindingContext, + val moduleDescriptor: ModuleDescriptor, + val bindingContext: BindingContext, val metadataHeader: ByteArray?, val fileTranslationResults: Map ) : TranslationResult(diagnostics) { diff --git a/js/js.translator/testData/lineNumbers/delegateMemberVal.kt b/js/js.translator/testData/lineNumbers/delegateMemberVal.kt index 088f5fd1326..10cda707a92 100644 --- a/js/js.translator/testData/lineNumbers/delegateMemberVal.kt +++ b/js/js.translator/testData/lineNumbers/delegateMemberVal.kt @@ -1,10 +1,16 @@ class A { val z by - lazy { 23 } + Delegate { 23 } } fun foo() { println(A().z) } -// LINES: 2 3 * 2 * 3 * 7 +class Delegate(val f: () -> Int) { + operator fun getValue(thisRef: Any?, property: Any): Int { + return f() + } +} + +// LINES: 2 3 2 * 3 * 7 10 12 diff --git a/js/js.translator/testData/lineNumbers/inlineMultiModule/simple.kt b/js/js.translator/testData/lineNumbers/inlineMultiModule/simple.kt new file mode 100644 index 00000000000..540828e9abb --- /dev/null +++ b/js/js.translator/testData/lineNumbers/inlineMultiModule/simple.kt @@ -0,0 +1,24 @@ +// MODULE: lib +// FILE: lib.kt + +package pkg1 + +inline fun foo(x: String) { + println("foo1($x);") + println("foo2($x);") +} + +// LINES: 7 8 + +// MODULE: main(lib) +// FILE: main.kt + +package pkg2 +import pkg1.* + +fun box() { + foo("23") + foo("42") +} + +// LINES: 7 20 7 8 20 8 7 21 7 8 21 8 \ No newline at end of file