Use source map remapper in JS inliner

This commit is contained in:
Alexey Andreev
2017-05-23 19:50:38 +03:00
parent 9c4ec902b0
commit bf21cfd6e0
14 changed files with 344 additions and 89 deletions
@@ -124,6 +124,7 @@ public class KotlinTestUtils {
"(?://\\s*MODULE:\\s*([^()\\n]+)(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*)?" + "(?://\\s*MODULE:\\s*([^()\\n]+)(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*)?" +
"//\\s*FILE:\\s*(.*)$", Pattern.MULTILINE); "//\\s*FILE:\\s*(.*)$", Pattern.MULTILINE);
private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("^//\\s*!([\\w_]+)(:\\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() { public static final BindingTrace DUMMY_TRACE = new BindingTrace() {
@NotNull @NotNull
@@ -648,6 +649,12 @@ public class KotlinTestUtils {
@NotNull @NotNull
public static <M, F> List<F> createTestFiles(String testFileName, String expectedText, TestFileFactory<M, F> factory) { public static <M, F> List<F> createTestFiles(String testFileName, String expectedText, TestFileFactory<M, F> factory) {
return createTestFiles(testFileName, expectedText, factory, false);
}
@NotNull
public static <M, F> List<F> createTestFiles(String testFileName, String expectedText, TestFileFactory<M, F> factory,
boolean preserveLocations) {
Map<String, String> directives = parseDirectives(expectedText); Map<String, String> directives = parseDirectives(expectedText);
List<F> testFiles = Lists.newArrayList(); List<F> testFiles = Lists.newArrayList();
@@ -681,7 +688,9 @@ public class KotlinTestUtils {
else { else {
end = expectedText.length(); end = expectedText.length();
} }
String fileText = expectedText.substring(start, end); String fileText = preserveLocations ?
substringKeepingLocations(expectedText, start, end) :
expectedText.substring(start,end);
processedChars = end; processedChars = end;
testFiles.add(factory.createFile(module, fileName, fileText, directives)); testFiles.add(factory.createFile(module, fileName, fileText, directives));
@@ -730,6 +739,26 @@ public class KotlinTestUtils {
return testFiles; 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<String> parseModuleList(@Nullable String dependencies) { private static List<String> parseModuleList(@Nullable String dependencies) {
if (dependencies == null) return Collections.emptyList(); if (dependencies == null) return Collections.emptyList();
return StringsKt.split(dependencies, Pattern.compile(MODULE_DELIMITER), 0); return StringsKt.split(dependencies, Pattern.compile(MODULE_DELIMITER), 0);
@@ -22,6 +22,7 @@ import com.intellij.util.Processor
import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
object JsLibraryUtils { object JsLibraryUtils {
@@ -43,49 +44,53 @@ object JsLibraryUtils {
} }
} }
@JvmStatic fun traverseJsLibraries(libs: List<File>, action: (content: String, path: String) -> Unit) { @JvmStatic fun traverseJsLibraries(libs: List<File>, action: (JsLibrary) -> Unit) {
libs.forEach { traverseJsLibrary(it, action) } 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 { when {
lib.isDirectory -> traverseDirectory(lib, action) lib.isDirectory -> traverseDirectory(lib, action)
FileUtil.isJarOrZip(lib) -> traverseArchive(lib, action) FileUtil.isJarOrZip(lib) -> traverseArchive(lib, action)
lib.name.endsWith(KotlinJavascriptMetadataUtils.JS_EXT) -> { lib.name.endsWith(KotlinJavascriptMetadataUtils.JS_EXT) -> {
lib.runIfFileExists(action) lib.runIfFileExists(lib.path, action)
val jsFile = lib.withReplacedExtensionOrNull( val jsFile = lib.withReplacedExtensionOrNull(
KotlinJavascriptMetadataUtils.META_JS_SUFFIX, KotlinJavascriptMetadataUtils.JS_EXT 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) { if (isFile) {
action(FileUtil.loadFile(this), "") action(JsLibrary(readText(), relativePath, correspondingSourceMapFile().contentIfExists()))
} }
} }
private fun copyJsFilesFromDirectory(dir: File, outputLibraryJsPath: String) { private fun copyJsFilesFromDirectory(dir: File, outputLibraryJsPath: String) {
traverseDirectory(dir) { content, relativePath -> traverseDirectory(dir) { (content, path) ->
FileUtil.writeToFile(File(outputLibraryJsPath, relativePath), content) 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> { file -> FileUtil.processFilesRecursively(dir, Processor<File> { file ->
val relativePath = FileUtil.getRelativePath(dir, file) val relativePath = FileUtil.getRelativePath(dir, file)
?: throw IllegalArgumentException("relativePath should not be null $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 val suggestedRelativePath = getSuggestedPath(relativePath) ?: return@Processor true
action(FileUtil.loadFile(file), suggestedRelativePath) file.runIfFileExists(suggestedRelativePath, action)
} }
true true
}) })
} }
private fun traverseDirectory(dir: File, action: (content: String, relativePath: String) -> Unit) { private fun traverseDirectory(dir: File, action: (JsLibrary) -> Unit) {
try { try {
processDirectory(dir, action) processDirectory(dir, action)
} }
@@ -95,26 +100,48 @@ object JsLibraryUtils {
} }
private fun copyJsFilesFromZip(file: File, outputLibraryJsPath: String) { private fun copyJsFilesFromZip(file: File, outputLibraryJsPath: String) {
traverseArchive(file) { content, relativePath -> traverseArchive(file) { library ->
FileUtil.writeToFile(File(outputLibraryJsPath, relativePath), content) 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) val zipFile = ZipFile(file.path)
try { try {
val zipEntries = zipFile.entries() val zipEntries = zipFile.entries()
val librariesWithoutSourceMaps = mutableListOf<JsLibrary>()
val possibleMapFiles = mutableMapOf<String, ZipEntry>()
while (zipEntries.hasMoreElements()) { while (zipEntries.hasMoreElements()) {
val entry = zipEntries.nextElement() val entry = zipEntries.nextElement()
val entryName = entry.name val entryName = entry.name
if (!entry.isDirectory && entryName.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) { if (!entry.isDirectory) {
val relativePath = getSuggestedPath(entryName) ?: continue if (entryName.endsWith(KotlinJavascriptMetadataUtils.JS_EXT)) {
val relativePath = getSuggestedPath(entryName) ?: continue
val stream = zipFile.getInputStream(entry) val stream = zipFile.getInputStream(entry)
val content = FileUtil.loadTextAndClose(stream) val content = FileUtil.loadTextAndClose(stream)
action(content, relativePath) 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) { catch (ex: IOException) {
LOG.error("Could not extract files from archive ${file.name}: ${ex.message}") LOG.error("Could not extract files from archive ${file.name}: ${ex.message}")
@@ -136,3 +163,5 @@ object JsLibraryUtils {
return path return path
} }
} }
data class JsLibrary(val content: String, val path: String, val sourceMapContent: String?)
@@ -57,6 +57,7 @@ class JsMetadataVersion(vararg numbers: Int) : BinaryVersion(*numbers) {
object KotlinJavascriptMetadataUtils { object KotlinJavascriptMetadataUtils {
const val JS_EXT: String = ".js" const val JS_EXT: String = ".js"
const val META_JS_SUFFIX: String = ".meta.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 = "kotlin_module_metadata"
private val KOTLIN_JAVASCRIPT_METHOD_NAME_PATTERN = "\\.kotlin_module_metadata\\(".toPattern() private val KOTLIN_JAVASCRIPT_METHOD_NAME_PATTERN = "\\.kotlin_module_metadata\\(".toPattern()
@@ -78,8 +79,8 @@ object KotlinJavascriptMetadataUtils {
fun loadMetadata(file: File): List<KotlinJavascriptMetadata> { fun loadMetadata(file: File): List<KotlinJavascriptMetadata> {
assert(file.exists()) { "Library $file not found" } assert(file.exists()) { "Library $file not found" }
val metadataList = arrayListOf<KotlinJavascriptMetadata>() val metadataList = arrayListOf<KotlinJavascriptMetadata>()
JsLibraryUtils.traverseJsLibrary(file) { content, _ -> JsLibraryUtils.traverseJsLibrary(file) { library ->
parseMetadata(content, metadataList) parseMetadata(library.content, metadataList)
} }
return metadataList return metadataList
@@ -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.config.JsConfig
import org.jetbrains.kotlin.js.inline.util.IdentitySet import org.jetbrains.kotlin.js.inline.util.IdentitySet
import org.jetbrains.kotlin.js.inline.util.isCallInvocation 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.parseFunction
import org.jetbrains.kotlin.js.parser.sourcemaps.*
import org.jetbrains.kotlin.js.translate.context.Namer import org.jetbrains.kotlin.js.translate.context.Namer
import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getModuleName import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getModuleName
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension 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.JsLibraryUtils
import org.jetbrains.kotlin.utils.sure import org.jetbrains.kotlin.utils.sure
import java.io.File import java.io.File
import java.io.StringReader
// TODO: add hash checksum to defineModule? // TODO: add hash checksum to defineModule?
/** /**
@@ -57,7 +60,14 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
* kotlinVariable: kotlin object variable. * kotlinVariable: kotlin object variable.
* The default variable is Kotlin, but it can be renamed by minifier. * 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<String, ModuleInfo>() private val moduleNameToInfo = HashMultimap.create<String, ModuleInfo>()
@@ -68,22 +78,40 @@ class FunctionReader(private val config: JsConfig, private val currentModuleName
moduleNameMap = buildModuleNameMap(fragments) moduleNameMap = buildModuleNameMap(fragments)
JsLibraryUtils.traverseJsLibraries(libs) { fileContent, filePath -> JsLibraryUtils.traverseJsLibraries(libs) { (content, path, sourceMapContent) ->
var current = 0 var current = 0
while (true) { while (true) {
var index = fileContent.indexOf(DEFINE_MODULE_FIND_PATTERN, current) var index = content.indexOf(DEFINE_MODULE_FIND_PATTERN, current)
if (index < 0) break if (index < 0) break
current = index + 1 current = index + 1
index = rewindToIdentifierStart(fileContent, index) index = rewindToIdentifierStart(content, index)
val preciseMatcher = DEFINE_MODULE_PATTERN.matcher(offset(fileContent, index)) val preciseMatcher = DEFINE_MODULE_PATTERN.matcher(offset(content, index))
if (!preciseMatcher.lookingAt()) continue if (!preciseMatcher.lookingAt()) continue
val moduleName = preciseMatcher.group(3) val moduleName = preciseMatcher.group(3)
val moduleVariable = preciseMatcher.group(4) val moduleVariable = preciseMatcher.group(4)
val kotlinVariable = preciseMatcher.group(1) 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++ 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 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, val replacements = hashMapOf(info.moduleVariable to moduleReference,
info.kotlinVariable to Namer.kotlinObject()) info.kotlinVariable to Namer.kotlinObject())
replaceExternalNames(function, replacements) replaceExternalNames(function, replacements)
@@ -64,9 +64,10 @@ final class LineBuffer {
*/ */
static final int BUFLEN = 256; static final int BUFLEN = 256;
LineBuffer(Reader in, int lineno) { LineBuffer(Reader in, CodePosition position) {
this.in = in; this.in = in;
this.lineno = lineno; this.lineno = position.getLine();
this.lineStart = -position.getOffset();
} }
int read() throws IOException { int read() throws IOException {
@@ -495,14 +495,15 @@ public class TokenStream {
} }
public TokenStream(Reader in, 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.pushbackToken = EOF;
this.sourceName = sourceName; this.sourceName = sourceName;
flags = 0; flags = 0;
secondToLastPosition = new CodePosition(lineno, 0); secondToLastPosition = position;
lastPosition = new CodePosition(lineno, 0); lastPosition = position;
lastTokenPosition = position;
} }
/* return and pop the token from the stream if it matches... /* return and pop the token from the stream if it matches...
@@ -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")
}
}
@@ -16,29 +16,26 @@
package org.jetbrains.kotlin.js.parser 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 com.google.gwt.dev.js.rhino.*
import org.jetbrains.kotlin.js.backend.ast.JsFunction import org.jetbrains.kotlin.js.backend.ast.JsFunction
import org.jetbrains.kotlin.js.backend.ast.JsFunctionScope import org.jetbrains.kotlin.js.backend.ast.JsFunctionScope
import org.jetbrains.kotlin.js.backend.ast.JsScope import org.jetbrains.kotlin.js.backend.ast.JsScope
import org.jetbrains.kotlin.js.backend.ast.JsStatement import org.jetbrains.kotlin.js.backend.ast.JsStatement
import org.jetbrains.kotlin.js.common.SourceInfoImpl import java.io.Reader
import java.io.* import java.io.StringReader
import java.util.* 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<JsStatement> { fun parse(code: String, reporter: ErrorReporter, scope: JsScope, fileName: String): List<JsStatement> {
val insideFunction = scope is JsFunctionScope 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) { return node.toJsAst(scope, fileName) {
mapStatements(it) mapStatements(it)
} }
} }
fun parseFunction(code: String, fileName: String, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction = fun parseFunction(code: String, fileName: String, position: CodePosition, offset: Int, reporter: ErrorReporter, scope: JsScope): JsFunction =
parse(code, offset, reporter, insideFunction = false) { parse(code, position, offset, reporter, insideFunction = false) {
addObserver(FunctionParsingObserver()) addObserver(FunctionParsingObserver())
primaryExpr(it) primaryExpr(it)
}.toJsAst(scope, fileName, JsAstMapper::mapFunction) }.toJsAst(scope, fileName, JsAstMapper::mapFunction)
@@ -65,6 +62,7 @@ private class FunctionParsingObserver : Observer {
inline inline
private fun parse( private fun parse(
code: String, code: String,
startPosition: CodePosition,
offset: Int, offset: Int,
reporter: ErrorReporter, reporter: ErrorReporter,
insideFunction: Boolean, insideFunction: Boolean,
@@ -73,7 +71,7 @@ private fun parse(
Context.enter().errorReporter = reporter Context.enter().errorReporter = reporter
try { try {
val ts = TokenStream(StringReader(code, offset), "<parser>", FAKE_SOURCE_INFO.line) val ts = TokenStream(StringReader(code, offset), "<parser>", startPosition)
val parser = Parser(IRFactory(ts), insideFunction) val parser = Parser(IRFactory(ts), insideFunction)
return parser.parseAction(ts) as Node return parser.parseAction(ts) as Node
} finally { } finally {
@@ -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.test.utils.LineOutputToStringVisitor
import org.jetbrains.kotlin.js.util.TextOutputImpl import org.jetbrains.kotlin.js.util.TextOutputImpl
import org.jetbrains.kotlin.psi.KtFile 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.serialization.js.ModuleKind
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.KotlinTestWithEnvironment import org.jetbrains.kotlin.test.KotlinTestWithEnvironment
import org.jetbrains.kotlin.utils.DFS
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File import java.io.File
import java.io.PrintStream import java.io.PrintStream
import java.nio.charset.Charset import java.nio.charset.Charset
abstract class AbstractJsLineNumberTest : KotlinTestWithEnvironment() { abstract class AbstractJsLineNumberTest : KotlinTestWithEnvironment() {
fun doTest(filePath: String) { fun doTest(filePath: String) {
val translator = K2JSTranslator(createConfig()) val file = File(filePath)
val unit = TranslationUnit.SourceFile(createPsiFile(filePath)) val sourceCode = file.readText()
val translationResult = translator.translateUnits(listOf(unit), MainCallParameters.noCall())
if (translationResult !is TranslationResult.Success) { TestFileFactoryImpl().use { testFactory ->
val outputStream = ByteArrayOutputStream() val inputFiles = KotlinTestUtils.createTestFiles(file.name, sourceCode, testFactory, true)
val collector = PrintingMessageCollector(PrintStream(outputStream), MessageRenderer.PLAIN_FULL_PATHS, true) val modules = inputFiles
AnalyzerWithCompilerReport.reportDiagnostics(translationResult.diagnostics, collector) .map { it.module }.distinct()
val messages = outputStream.toByteArray().toString(Charset.forName("UTF-8")) .associateBy { it.name }
throw AssertionError("The following errors occurred compiling test:\n" + messages)
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 { override fun createEnvironment(): KotlinCoreEnvironment {
return KotlinCoreEnvironment.createForTests(testRootDisposable, CompilerConfiguration(), EnvironmentConfigFiles.JS_CONFIG_FILES) return KotlinCoreEnvironment.createForTests(testRootDisposable, CompilerConfiguration(), EnvironmentConfigFiles.JS_CONFIG_FILES)
} }
private fun createConfig(): JsConfig { private fun createConfig(module: TestModule, inputFile: File, modules: Map<String, TestModule>): JsConfig {
val dependencies = module.dependencies
.mapNotNull { modules[it]?.outputFileName(inputFile) }
.map { "$it.meta.js" }
val configuration = environment.configuration.copy() 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.MODULE_KIND, ModuleKind.PLAIN)
configuration.put(JSConfigurationKeys.TARGET, EcmaVersion.v5) configuration.put(JSConfigurationKeys.TARGET, EcmaVersion.v5)
@@ -109,6 +152,44 @@ abstract class AbstractJsLineNumberTest : KotlinTestWithEnvironment() {
return psiManager.findFile(fileSystem.findFileByPath(fileName)!!) as KtFile return psiManager.findFile(fileSystem.findFileByPath(fileName)!!) as KtFile
} }
private inner class TestFileFactoryImpl : KotlinTestUtils.TestFileFactory<TestModule, TestFile>, 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<String, String>): 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<String>, friends: List<String>): TestModule? {
return TestModule(name, dependencies)
}
override fun close() {
FileUtil.delete(tmpDir)
}
}
private class TestModule(
val name: String,
dependencies: List<String>
) {
val dependencies = dependencies.toMutableList()
val files = mutableListOf<TestFile>()
}
private class TestFile(val fileName: String, val module: TestModule) {
init {
module.files += this
}
}
companion object { companion object {
private val DIR_NAME = "lineNumbers" private val DIR_NAME = "lineNumbers"
private val LINES_PATTERN = Regex("^ *// *LINES: *(.*)$", RegexOption.MULTILINE) private val LINES_PATTERN = Regex("^ *// *LINES: *(.*)$", RegexOption.MULTILINE)
@@ -87,9 +87,7 @@ abstract class BasicBoxTest(
val outputPostfixFile = getOutputPostfixFile(filePath) val outputPostfixFile = getOutputPostfixFile(filePath)
TestFileFactoryImpl().use { testFactory -> TestFileFactoryImpl().use { testFactory ->
testFactory.defaultModule.moduleKind val inputFiles = KotlinTestUtils.createTestFiles(file.name, fileContent, testFactory, true)
val inputFiles = KotlinTestUtils.createTestFiles(file.name, fileContent, testFactory)
val modules = inputFiles val modules = inputFiles
.map { it.module }.distinct() .map { it.module }.distinct()
.map { it.name to it }.toMap() .map { it.name to it }.toMap()
@@ -627,7 +625,7 @@ abstract class BasicBoxTest(
private val METADATA_EXTENSION = "jsmeta" private val METADATA_EXTENSION = "jsmeta"
private val HEADER_FILE = "header.$METADATA_EXTENSION" 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 DEFAULT_MODULE = "main"
private val TEST_FUNCTION = "box" private val TEST_FUNCTION = "box"
private val OLD_MODULE_SUFFIX = "-old" private val OLD_MODULE_SUFFIX = "-old"
@@ -269,4 +269,19 @@ public class JsLineNumberTestGenerated extends AbstractJsLineNumberTest {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/lineNumbers/whileWithComplexCondition.kt"); String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/lineNumbers/whileWithComplexCondition.kt");
doTest(fileName); 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);
}
}
} }
@@ -47,8 +47,8 @@ abstract class TranslationResult protected constructor(val diagnostics: Diagnost
val program: JsProgram, val program: JsProgram,
diagnostics: Diagnostics, diagnostics: Diagnostics,
private val importedModules: List<String>, private val importedModules: List<String>,
private val moduleDescriptor: ModuleDescriptor, val moduleDescriptor: ModuleDescriptor,
private val bindingContext: BindingContext, val bindingContext: BindingContext,
val metadataHeader: ByteArray?, val metadataHeader: ByteArray?,
val fileTranslationResults: Map<KtFile, FileTranslationResult> val fileTranslationResults: Map<KtFile, FileTranslationResult>
) : TranslationResult(diagnostics) { ) : TranslationResult(diagnostics) {
+8 -2
View File
@@ -1,10 +1,16 @@
class A { class A {
val z by val z by
lazy { 23 } Delegate { 23 }
} }
fun foo() { fun foo() {
println(A().z) 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
@@ -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