diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.java b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.java index d8e0f5d0828..d4028bedc51 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.java +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.java @@ -41,6 +41,18 @@ public class K2JSCompilerArguments extends CommonCompilerArguments { @Argument(value = "-source-map", description = "Generate source map") public boolean sourceMap; + @GradleOption(DefaultValues.StringNullDefault.class) + @Argument(value = "-source-map-prefix", description = "Prefix for paths in a source map") + public String sourceMapPrefix; + + @GradleOption(DefaultValues.StringNullDefault.class) + @Argument( + value = "-source-map-source-roots", + valueDescription = "", + description = "Base directories which are used to calculate relative paths to source files in source map" + ) + public String sourceMapSourceRoots; + @GradleOption(DefaultValues.BooleanTrueDefault.class) @Argument(value = "-meta-info", description = "Generate .meta.js and .kjsm files with metadata. Use to create a library") public boolean metaInfo; diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/js/K2JSCompiler.java b/compiler/cli/src/org/jetbrains/kotlin/cli/js/K2JSCompiler.java index 1e9da5645a5..ddd74527974 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/js/K2JSCompiler.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/js/K2JSCompiler.java @@ -19,7 +19,9 @@ package org.jetbrains.kotlin.cli.js; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ExceptionUtil; import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashMap; @@ -34,6 +36,7 @@ import org.jetbrains.kotlin.cli.common.ExitCode; import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments; import org.jetbrains.kotlin.cli.common.arguments.K2JsArgumentConstants; import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport; +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; import org.jetbrains.kotlin.cli.common.messages.MessageCollector; import org.jetbrains.kotlin.cli.common.output.outputUtils.OutputUtilsKt; import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; @@ -58,8 +61,8 @@ import org.jetbrains.kotlin.utils.PathUtil; import org.jetbrains.kotlin.utils.StringsKt; import java.io.File; -import java.util.List; -import java.util.Map; +import java.io.IOException; +import java.util.*; import static org.jetbrains.kotlin.cli.common.ExitCode.COMPILATION_ERROR; import static org.jetbrains.kotlin.cli.common.ExitCode.OK; @@ -261,6 +264,23 @@ public class K2JSCompiler extends CLICompiler { if (arguments.sourceMap) { configuration.put(JSConfigurationKeys.SOURCE_MAP, true); + if (arguments.sourceMapPrefix != null) { + configuration.put(JSConfigurationKeys.SOURCE_MAP_PREFIX, arguments.sourceMapPrefix); + } + + String sourceMapSourceRoots = arguments.sourceMapSourceRoots != null ? + arguments.sourceMapSourceRoots : + calculateSourceMapSourceRoot(messageCollector, arguments); + List sourceMapSourceRootList = StringUtil.split(sourceMapSourceRoots, File.pathSeparator); + configuration.put(JSConfigurationKeys.SOURCE_MAP_SOURCE_ROOTS, sourceMapSourceRootList); + } + else { + if (arguments.sourceMapPrefix != null) { + messageCollector.report(WARNING, "source-map-prefix argument has no effect without source map", null); + } + if (arguments.sourceMapSourceRoots != null) { + messageCollector.report(WARNING, "source-map-source-root argument has no effect without source map", null); + } } if (arguments.metaInfo) { configuration.put(JSConfigurationKeys.META_INFO, true); @@ -299,6 +319,58 @@ public class K2JSCompiler extends CLICompiler { configuration.put(JSConfigurationKeys.MODULE_KIND, moduleKind); } + @NotNull + private static String calculateSourceMapSourceRoot( + @NotNull MessageCollector messageCollector, + @NotNull K2JSCompilerArguments arguments + ) { + File commonPath = null; + List pathToRoot = new ArrayList<>(); + Map pathToRootIndexes = new HashMap<>(); + + try { + for (String path : arguments.freeArgs) { + File file = new File(path).getCanonicalFile(); + if (commonPath == null) { + commonPath = file; + + while (file != null) { + pathToRoot.add(file); + file = file.getParentFile(); + } + Collections.reverse(pathToRoot); + + for (int i = 0; i < pathToRoot.size(); ++i) { + pathToRootIndexes.put(pathToRoot.get(i), i); + } + } + else { + while (file != null) { + Integer existingIndex = pathToRootIndexes.get(file); + if (existingIndex != null) { + existingIndex = Math.min(existingIndex, pathToRoot.size() - 1); + pathToRoot.subList(existingIndex + 1, pathToRoot.size()).clear(); + commonPath = pathToRoot.get(pathToRoot.size() - 1); + break; + } + file = file.getParentFile(); + } + if (file == null) { + break; + } + } + } + } + catch (IOException e) { + String text = ExceptionUtil.getThrowableText(e); + messageCollector.report(CompilerMessageSeverity.ERROR, "IO error occurred calculating source root:\n" + text, null); + return "."; + } + + return commonPath != null ? commonPath.getPath() : "."; + } + + private static MainCallParameters createMainCallParameters(String main) { if (K2JsArgumentConstants.NO_CALL.equals(main)) { return MainCallParameters.noCall(); diff --git a/compiler/testData/cli/js/jsHelp.out b/compiler/testData/cli/js/jsHelp.out index ce9b772c35e..a96737b4520 100644 --- a/compiler/testData/cli/js/jsHelp.out +++ b/compiler/testData/cli/js/jsHelp.out @@ -4,6 +4,9 @@ where possible options include: -no-stdlib Don't use bundled Kotlin stdlib -libraries Paths to Kotlin libraries with .meta.js and .kjsm files, separated by system path separator -source-map Generate source map + -source-map-prefix Prefix for paths in a source map + -source-map-source-roots + Base directories which are used to calculate relative paths to source files in source map -meta-info Generate .meta.js and .kjsm files with metadata. Use to create a library -target { v5 } Generate JS files for specific ECMA version -module-kind { plain, amd, commonjs, umd } diff --git a/compiler/testData/cli/js/sourceMap.args b/compiler/testData/cli/js/sourceMap.args new file mode 100644 index 00000000000..ba955d97b1f --- /dev/null +++ b/compiler/testData/cli/js/sourceMap.args @@ -0,0 +1,5 @@ +$TESTDATA_DIR$/sourceMap.kt +-no-stdlib +-source-map +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMap.kt b/compiler/testData/cli/js/sourceMap.kt new file mode 100644 index 00000000000..81d6dbb4ad0 --- /dev/null +++ b/compiler/testData/cli/js/sourceMap.kt @@ -0,0 +1,5 @@ +var log = "" + +fun foo(x: String) { + log += "$x;" +} \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMap.out b/compiler/testData/cli/js/sourceMap.out new file mode 100644 index 00000000000..d86bac9de59 --- /dev/null +++ b/compiler/testData/cli/js/sourceMap.out @@ -0,0 +1 @@ +OK diff --git a/compiler/testData/cli/js/sourceMap.test b/compiler/testData/cli/js/sourceMap.test new file mode 100644 index 00000000000..9a569dde5b4 --- /dev/null +++ b/compiler/testData/cli/js/sourceMap.test @@ -0,0 +1,2 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "sourceMap.kt" diff --git a/compiler/testData/cli/js/sourceMapPrefix.args b/compiler/testData/cli/js/sourceMapPrefix.args new file mode 100644 index 00000000000..94bc9b9ae44 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapPrefix.args @@ -0,0 +1,7 @@ +$TESTDATA_DIR$/sourceMap.kt +-no-stdlib +-source-map +-source-map-prefix +http://localhost:8080/src/ +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMapPrefix.out b/compiler/testData/cli/js/sourceMapPrefix.out new file mode 100644 index 00000000000..d86bac9de59 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapPrefix.out @@ -0,0 +1 @@ +OK diff --git a/compiler/testData/cli/js/sourceMapPrefix.test b/compiler/testData/cli/js/sourceMapPrefix.test new file mode 100644 index 00000000000..a834845edca --- /dev/null +++ b/compiler/testData/cli/js/sourceMapPrefix.test @@ -0,0 +1,2 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "http://localhost:8080/src/sourceMap.kt" diff --git a/compiler/testData/cli/js/sourceMapRoot/bar/baz/file3.kt b/compiler/testData/cli/js/sourceMapRoot/bar/baz/file3.kt new file mode 100644 index 00000000000..fab815427bd --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRoot/bar/baz/file3.kt @@ -0,0 +1,3 @@ +fun h(): Long { + return 42L +} \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRoot/bar/file2.kt b/compiler/testData/cli/js/sourceMapRoot/bar/file2.kt new file mode 100644 index 00000000000..08b5cb2d72f --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRoot/bar/file2.kt @@ -0,0 +1,3 @@ +fun g(): Int { + return 23 +} \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRoot/foo/baz/file4.kt b/compiler/testData/cli/js/sourceMapRoot/foo/baz/file4.kt new file mode 100644 index 00000000000..696627a6699 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRoot/foo/baz/file4.kt @@ -0,0 +1,3 @@ +fun j(): Float { + return 3.14159f +} \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRoot/foo/file1.kt b/compiler/testData/cli/js/sourceMapRoot/foo/file1.kt new file mode 100644 index 00000000000..1df2d649272 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRoot/foo/file1.kt @@ -0,0 +1,3 @@ +fun f(): String { + return "f()" +} \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRootAuto.args b/compiler/testData/cli/js/sourceMapRootAuto.args new file mode 100644 index 00000000000..4e1cdb93599 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootAuto.args @@ -0,0 +1,6 @@ +$TESTDATA_DIR$/sourceMapRoot/foo/file1.kt +$TESTDATA_DIR$/sourceMapRoot/bar/file2.kt +-no-stdlib +-source-map +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMapRootAuto.out b/compiler/testData/cli/js/sourceMapRootAuto.out new file mode 100644 index 00000000000..a0aba9318ad --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootAuto.out @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRootAuto.test b/compiler/testData/cli/js/sourceMapRootAuto.test new file mode 100644 index 00000000000..a0340f0e762 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootAuto.test @@ -0,0 +1,3 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "foo/file1.kt" +// CONTAINS: out.js.map, "bar/file2.kt" diff --git a/compiler/testData/cli/js/sourceMapRootManual.args b/compiler/testData/cli/js/sourceMapRootManual.args new file mode 100644 index 00000000000..92d3231bc81 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootManual.args @@ -0,0 +1,8 @@ +$TESTDATA_DIR$/sourceMapRoot/foo/file1.kt +$TESTDATA_DIR$/sourceMapRoot/bar/file2.kt +-no-stdlib +-source-map +-source-map-source-roots +$TESTDATA_DIR$/ +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMapRootManual.out b/compiler/testData/cli/js/sourceMapRootManual.out new file mode 100644 index 00000000000..a0aba9318ad --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootManual.out @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/compiler/testData/cli/js/sourceMapRootManual.test b/compiler/testData/cli/js/sourceMapRootManual.test new file mode 100644 index 00000000000..bf96754a61e --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootManual.test @@ -0,0 +1,3 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "sourceMapRoot/foo/file1.kt" +// CONTAINS: out.js.map, "sourceMapRoot/bar/file2.kt" diff --git a/compiler/testData/cli/js/sourceMapRootMissing.args b/compiler/testData/cli/js/sourceMapRootMissing.args new file mode 100644 index 00000000000..1afe84d1637 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMissing.args @@ -0,0 +1,10 @@ +$TESTDATA_DIR$/sourceMapRoot/foo/file1.kt +$TESTDATA_DIR$/sourceMapRoot/bar/file2.kt +$TESTDATA_DIR$/sourceMapRoot/bar/baz/file3.kt +$TESTDATA_DIR$/sourceMapRoot/foo/baz/file4.kt +-no-stdlib +-source-map +-source-map-source-roots +$TESTDATA_DIR$/sourceMapRoot/foo +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMapRootMissing.out b/compiler/testData/cli/js/sourceMapRootMissing.out new file mode 100644 index 00000000000..d86bac9de59 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMissing.out @@ -0,0 +1 @@ +OK diff --git a/compiler/testData/cli/js/sourceMapRootMissing.test b/compiler/testData/cli/js/sourceMapRootMissing.test new file mode 100644 index 00000000000..2d231b283ba --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMissing.test @@ -0,0 +1,5 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "file1.kt" +// CONTAINS: out.js.map, "baz/file4.kt" +// CONTAINS: out.js.map, "file2.kt" +// CONTAINS: out.js.map, "file3.kt" diff --git a/compiler/testData/cli/js/sourceMapRootMultiple.args b/compiler/testData/cli/js/sourceMapRootMultiple.args new file mode 100644 index 00000000000..1cc53c0a34b --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMultiple.args @@ -0,0 +1,8 @@ +$TESTDATA_DIR$/sourceMapRoot/foo/file1.kt +$TESTDATA_DIR$/sourceMapRoot/bar/file2.kt +-no-stdlib +-source-map +-source-map-source-roots +$TESTDATA_DIR$/foo:$TESTDATA_DIR$/bar +-output +$TEMP_DIR$/out.js diff --git a/compiler/testData/cli/js/sourceMapRootMultiple.out b/compiler/testData/cli/js/sourceMapRootMultiple.out new file mode 100644 index 00000000000..d86bac9de59 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMultiple.out @@ -0,0 +1 @@ +OK diff --git a/compiler/testData/cli/js/sourceMapRootMultiple.test b/compiler/testData/cli/js/sourceMapRootMultiple.test new file mode 100644 index 00000000000..2ce3a5f0ec6 --- /dev/null +++ b/compiler/testData/cli/js/sourceMapRootMultiple.test @@ -0,0 +1,3 @@ +// EXISTS: out.js +// CONTAINS: out.js.map, "file1.kt" +// CONTAINS: out.js.map, "file2.kt" diff --git a/compiler/tests/org/jetbrains/kotlin/cli/AbstractCliTest.java b/compiler/tests/org/jetbrains/kotlin/cli/AbstractCliTest.java index 93434e49e40..be0f5a227d1 100644 --- a/compiler/tests/org/jetbrains/kotlin/cli/AbstractCliTest.java +++ b/compiler/tests/org/jetbrains/kotlin/cli/AbstractCliTest.java @@ -24,7 +24,6 @@ import kotlin.collections.CollectionsKt; import kotlin.io.FilesKt; import kotlin.text.Charsets; import org.jetbrains.annotations.NotNull; -import org.jetbrains.kotlin.cli.common.CLICompiler; import org.jetbrains.kotlin.cli.common.CLITool; import org.jetbrains.kotlin.cli.common.ExitCode; import org.jetbrains.kotlin.cli.js.K2JSCompiler; @@ -120,6 +119,26 @@ public abstract class AbstractCliTest extends TestCaseWithTmpdir { } } + List containsTextList = InTextDirectivesUtils.findLinesWithPrefixesRemoved(content, "// CONTAINS: "); + for (String containsSpec : containsTextList) { + String[] parts = containsSpec.split(",", 2); + String fileName = parts[0].trim(); + String contentToSearch = parts[1].trim(); + File file = new File(tmpdir, fileName); + if (!file.exists()) { + diagnostics.add("File does not exist: " + fileName); + } + else if (file.isDirectory()) { + diagnostics.add("File is a directory: " + fileName); + } + else { + String text = FilesKt.readText(file, Charsets.UTF_8); + if (!text.contains(contentToSearch)) { + diagnostics.add("File " + fileName + " does not contain string: " + contentToSearch); + } + } + } + if (!diagnostics.isEmpty()) { diagnostics.add(0, diagnostics.size() + " problem(s) found:"); Assert.fail(StringsKt.join(diagnostics, "\n")); @@ -127,7 +146,7 @@ public abstract class AbstractCliTest extends TestCaseWithTmpdir { } @NotNull - static List readArgs(@NotNull String argsFilePath, @NotNull String tempDir) throws IOException { + private static List readArgs(@NotNull String argsFilePath, @NotNull String tempDir) throws IOException { List lines = FilesKt.readLines(new File(argsFilePath), Charsets.UTF_8); return CollectionsKt.mapNotNull(lines, arg -> { diff --git a/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java index 0c540634ba4..a0cd68f27db 100644 --- a/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java @@ -515,6 +515,42 @@ public class CliTestGenerated extends AbstractCliTest { doJsTest(fileName); } + @TestMetadata("sourceMap.args") + public void testSourceMap() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMap.args"); + doJsTest(fileName); + } + + @TestMetadata("sourceMapPrefix.args") + public void testSourceMapPrefix() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMapPrefix.args"); + doJsTest(fileName); + } + + @TestMetadata("sourceMapRootAuto.args") + public void testSourceMapRootAuto() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMapRootAuto.args"); + doJsTest(fileName); + } + + @TestMetadata("sourceMapRootManual.args") + public void testSourceMapRootManual() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMapRootManual.args"); + doJsTest(fileName); + } + + @TestMetadata("sourceMapRootMissing.args") + public void testSourceMapRootMissing() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMapRootMissing.args"); + doJsTest(fileName); + } + + @TestMetadata("sourceMapRootMultiple.args") + public void testSourceMapRootMultiple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/sourceMapRootMultiple.args"); + doJsTest(fileName); + } + @TestMetadata("suppressAllWarningsJS.args") public void testSuppressAllWarningsJS() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js/suppressAllWarningsJS.args"); diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/config/JSConfigurationKeys.java b/js/js.frontend/src/org/jetbrains/kotlin/js/config/JSConfigurationKeys.java index 4e3bcc1602f..662a97d2ec0 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/config/JSConfigurationKeys.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/config/JSConfigurationKeys.java @@ -29,6 +29,12 @@ public class JSConfigurationKeys { public static final CompilerConfigurationKey SOURCE_MAP = CompilerConfigurationKey.create("generate source map"); + public static final CompilerConfigurationKey SOURCE_MAP_PREFIX = + CompilerConfigurationKey.create("prefix to add to paths in source map"); + + public static final CompilerConfigurationKey> SOURCE_MAP_SOURCE_ROOTS = + CompilerConfigurationKey.create("base directories used to calculate relative paths for source map"); + public static final CompilerConfigurationKey META_INFO = CompilerConfigurationKey.create("generate .meta.js and .kjsm files"); diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/config/JsConfig.java b/js/js.frontend/src/org/jetbrains/kotlin/js/config/JsConfig.java index 926f8398a47..42319aaf41f 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/config/JsConfig.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/config/JsConfig.java @@ -103,6 +103,16 @@ public class JsConfig { return getConfiguration().getList(JSConfigurationKeys.LIBRARIES); } + @NotNull + public String getSourceMapPrefix() { + return configuration.get(JSConfigurationKeys.SOURCE_MAP_PREFIX, ""); + } + + @NotNull + public List getSourceMapRoots() { + return configuration.get(JSConfigurationKeys.SOURCE_MAP_SOURCE_ROOTS, Collections.singletonList(".")); + } + @NotNull public List getFriends() { if (getConfiguration().getBoolean(JSConfigurationKeys.FRIEND_PATHS_DISABLED)) return Collections.emptyList(); diff --git a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt index 604a7eb2838..3c341366032 100644 --- a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt +++ b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/ast/JsAstSerializer.kt @@ -22,11 +22,12 @@ import org.jetbrains.kotlin.js.backend.ast.metadata.* import org.jetbrains.kotlin.serialization.js.ast.JsAstProtoBuf.* import org.jetbrains.kotlin.serialization.js.ast.JsAstProtoBuf.BinaryOperation.Type.* import org.jetbrains.kotlin.serialization.js.ast.JsAstProtoBuf.UnaryOperation.Type.* +import java.io.File import java.io.OutputStream import java.util.* import org.jetbrains.kotlin.resolve.inline.InlineStrategy as KotlinInlineStrategy -class JsAstSerializer { +class JsAstSerializer(private val pathResolver: (File) -> String) { private val nameTableBuilder = NameTable.newBuilder() private val stringTableBuilder = StringTable.newBuilder() private val nameMap = mutableMapOf() @@ -567,9 +568,9 @@ class JsAstSerializer { if (location != null) { val lastFile = fileStack.peek() val newFile = location.file - fileChanged = lastFile != newFile && newFile != null + fileChanged = lastFile != newFile if (fileChanged) { - fileConsumer(serialize(newFile!!)) + fileConsumer(serialize(newFile)) fileStack.push(location.file) } val locationBuilder = Location.newBuilder() @@ -598,7 +599,7 @@ class JsAstSerializer { val file = element.containingFile val document = file.viewProvider.document!! - val path = file.viewProvider.virtualFile.path + val path = pathResolver(File(file.viewProvider.virtualFile.path)) val startOffset = element.node.startOffset val startLine = document.getLineNumber(startOffset) 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 f01d048cddf..8d7fe778e6a 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 @@ -42,6 +42,7 @@ import org.jetbrains.kotlin.js.facade.* import org.jetbrains.kotlin.js.parser.parse import org.jetbrains.kotlin.js.parser.sourcemaps.* import org.jetbrains.kotlin.js.sourceMap.JsSourceGenerationVisitor +import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder import org.jetbrains.kotlin.js.test.utils.* import org.jetbrains.kotlin.js.util.TextOutputImpl @@ -409,7 +410,8 @@ abstract class BasicBoxTest( generatedProgram.accept(AmbiguousAstSourcePropagation()) val output = TextOutputImpl() - val sourceMapBuilder = SourceMap3Builder(outputFile, output, SourceMapBuilderConsumer()) + val pathResolver = SourceFilePathResolver(mutableListOf(File("."))) + val sourceMapBuilder = SourceMap3Builder(outputFile, output, "", SourceMapBuilderConsumer(pathResolver)) generatedProgram.accept(JsSourceGenerationVisitor(output, sourceMapBuilder)) val code = output.toString() val generatedSourceMap = sourceMapBuilder.build() diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.java b/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.java index 53d5fc599ba..b990476d834 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.java @@ -32,6 +32,7 @@ import org.jetbrains.kotlin.js.inline.JsInliner; import org.jetbrains.kotlin.js.inline.clean.LabeledBlockToDoWhileTransformation; import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedImportsKt; import org.jetbrains.kotlin.js.inline.clean.ResolveTemporaryNamesKt; +import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver; import org.jetbrains.kotlin.js.translate.general.AstGenerationResult; import org.jetbrains.kotlin.js.translate.general.FileTranslationResult; import org.jetbrains.kotlin.js.translate.general.Translation; @@ -45,10 +46,13 @@ import org.jetbrains.kotlin.serialization.js.KotlinJavascriptSerializationUtil; import org.jetbrains.kotlin.serialization.js.ast.JsAstSerializer; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static org.jetbrains.kotlin.diagnostics.DiagnosticUtils.hasError; @@ -144,8 +148,18 @@ public final class K2JSTranslator { ExpandIsCallsKt.expandIsCalls(newFragments); ProgressIndicatorAndCompilationCanceledStatus.checkCanceled(); + List sourceRoots = config.getSourceMapRoots().stream().map(File::new).collect(Collectors.toList()); + SourceFilePathResolver pathResolver = new SourceFilePathResolver(sourceRoots); + Map fileMap = new HashMap<>(); - JsAstSerializer serializer = new JsAstSerializer(); + JsAstSerializer serializer = new JsAstSerializer(file -> { + try { + return pathResolver.getPathRelativeToSourceRoots(file); + } + catch (IOException e) { + throw new RuntimeException("IO error occurred resolving path to source file", e); + } + }); byte[] metadataHeader = null; boolean serializeFragments = config.getConfiguration().get(JSConfigurationKeys.SERIALIZE_FRAGMENTS, false); for (KtFile file : files) { diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/facade/SourceMapBuilderConsumer.java b/js/js.translator/src/org/jetbrains/kotlin/js/facade/SourceMapBuilderConsumer.java index 76d11e64509..1b362345b85 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/facade/SourceMapBuilderConsumer.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/facade/SourceMapBuilderConsumer.java @@ -20,21 +20,40 @@ import com.intellij.openapi.editor.Document; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.PairConsumer; +import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.js.backend.ast.JsLocation; +import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver; import org.jetbrains.kotlin.js.sourceMap.SourceMapBuilder; +import java.io.File; +import java.io.IOException; + public class SourceMapBuilderConsumer implements PairConsumer { + @NotNull + private final SourceFilePathResolver pathResolver; + + public SourceMapBuilderConsumer(@NotNull SourceFilePathResolver pathResolver) { + this.pathResolver = pathResolver; + } + @Override public void consume(SourceMapBuilder builder, Object sourceInfo) { if (sourceInfo instanceof PsiElement) { PsiElement element = (PsiElement) sourceInfo; - PsiFile file = element.getContainingFile(); + PsiFile psiFile = element.getContainingFile(); int offset = element.getNode().getStartOffset(); - Document document = file.getViewProvider().getDocument(); + Document document = psiFile.getViewProvider().getDocument(); assert document != null; int line = document.getLineNumber(offset); int column = offset - document.getLineStartOffset(line); - builder.addMapping(file.getViewProvider().getVirtualFile().getPath(), line, column); + + File file = new File(psiFile.getViewProvider().getVirtualFile().getPath()); + try { + builder.addMapping(pathResolver.getPathRelativeToSourceRoots(file), line, column); + } + catch (IOException e) { + throw new RuntimeException("IO error occurred generating source maps", e); + } } else if (sourceInfo instanceof JsLocation) { JsLocation location = (JsLocation) sourceInfo; 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 b2803c3dc8b..f2bc8b42958 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 @@ -24,6 +24,7 @@ import org.jetbrains.kotlin.js.backend.ast.JsProgram import org.jetbrains.kotlin.js.config.JSConfigurationKeys import org.jetbrains.kotlin.js.config.JsConfig import org.jetbrains.kotlin.js.sourceMap.JsSourceGenerationVisitor +import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder import org.jetbrains.kotlin.js.sourceMap.SourceMapBuilder import org.jetbrains.kotlin.js.translate.general.FileTranslationResult @@ -58,9 +59,14 @@ abstract class TranslationResult protected constructor(val diagnostics: Diagnost fun getOutputFiles(outputFile: File, outputPrefixFile: File?, outputPostfixFile: File?): OutputFileCollection { val output = TextOutputImpl() val sourceMapBuilder = - if (config.configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP)) - SourceMap3Builder(outputFile, output, SourceMapBuilderConsumer()) - else null + if (config.configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP)) { + val sourceRoots = config.sourceMapRoots.map { File(it) } + val pathResolver = SourceFilePathResolver(sourceRoots) + SourceMap3Builder(outputFile, output, config.sourceMapPrefix, SourceMapBuilderConsumer(pathResolver)) + } + else { + null + } val code = getCode(output, sourceMapBuilder) val prefix = outputPrefixFile?.readText() ?: "" diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceFilePathResolver.java b/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceFilePathResolver.java new file mode 100644 index 00000000000..eeee0bbdc82 --- /dev/null +++ b/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceFilePathResolver.java @@ -0,0 +1,67 @@ +/* + * 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.sourceMap; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class SourceFilePathResolver { + @NotNull + private final Set sourceRoots; + + @NotNull + private final Map cache = new HashMap<>(); + + public SourceFilePathResolver(@NotNull List sourceRoots) { + this.sourceRoots = new HashSet<>(); + for (File sourceRoot : sourceRoots) { + this.sourceRoots.add(sourceRoot.getAbsoluteFile()); + } + } + + @NotNull + public String getPathRelativeToSourceRoots(@NotNull File file) throws IOException { + String path = cache.get(file); + if (path == null) { + path = calculatePathRelativeToSourceRoots(file); + cache.put(file, path); + } + return path; + } + + private String calculatePathRelativeToSourceRoots(@NotNull File file) throws IOException { + List parts = new ArrayList<>(); + File currentFile = file.getCanonicalFile(); + + while (currentFile != null) { + if (sourceRoots.contains(currentFile)) { + if (parts.isEmpty()) { + break; + } + Collections.reverse(parts); + return StringUtil.join(parts, File.separator); + } + parts.add(currentFile.getName()); + currentFile = currentFile.getParentFile(); + } + return file.getName(); + } +} diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceMap3Builder.java b/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceMap3Builder.java index af268fb3a85..ec40e58e51a 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceMap3Builder.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/sourceMap/SourceMap3Builder.java @@ -30,6 +30,7 @@ public class SourceMap3Builder implements SourceMapBuilder { private final StringBuilder out = new StringBuilder(8192); private final File generatedFile; private final TextOutput textOutput; + private final String pathPrefix; private final PairConsumer sourceInfoConsumer; private String lastSource; @@ -50,9 +51,11 @@ public class SourceMap3Builder implements SourceMapBuilder { private int previousSourceLine; private int previousSourceColumn; - public SourceMap3Builder(File generatedFile, TextOutput textOutput, PairConsumer sourceInfoConsumer) { + public SourceMap3Builder(File generatedFile, TextOutput textOutput, String pathPrefix, + PairConsumer sourceInfoConsumer) { this.generatedFile = generatedFile; this.textOutput = textOutput; + this.pathPrefix = pathPrefix; this.sourceInfoConsumer = sourceInfoConsumer; } @@ -83,7 +86,7 @@ public class SourceMap3Builder implements SourceMapBuilder { else { isNotFirst = true; } - sb.append('"').append("file://").append(source).append('"'); + sb.append('"').append(pathPrefix).append(source).append('"'); } sb.append(']'); } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptions.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptions.kt index 27a97dc24ae..cad669b7df1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptions.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptions.kt @@ -48,6 +48,18 @@ interface KotlinJsOptions : org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions */ var sourceMap: kotlin.Boolean + /** + * Prefix for paths in a source map + * Default value: null + */ + var sourceMapPrefix: kotlin.String? + + /** + * Base directories which are used to calculate relative paths to source files in source map + * Default value: null + */ + var sourceMapSourceRoots: kotlin.String? + /** * Generate JS files for specific ECMA version * Possible values: "v5" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptionsBase.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptionsBase.kt index c7421bfb078..b13c41bda87 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptionsBase.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinJsOptionsBase.kt @@ -59,6 +59,16 @@ internal abstract class KotlinJsOptionsBase : org.jetbrains.kotlin.gradle.dsl.Ko get() = sourceMapField ?: false set(value) { sourceMapField = value } + private var sourceMapPrefixField: kotlin.String?? = null + override var sourceMapPrefix: kotlin.String? + get() = sourceMapPrefixField ?: null + set(value) { sourceMapPrefixField = value } + + private var sourceMapSourceRootsField: kotlin.String?? = null + override var sourceMapSourceRoots: kotlin.String? + get() = sourceMapSourceRootsField ?: null + set(value) { sourceMapSourceRootsField = value } + private var targetField: kotlin.String? = null override var target: kotlin.String get() = targetField ?: "v5" @@ -81,6 +91,8 @@ internal abstract class KotlinJsOptionsBase : org.jetbrains.kotlin.gradle.dsl.Ko noStdlibField?.let { args.noStdlib = it } outputFileField?.let { args.outputFile = it } sourceMapField?.let { args.sourceMap = it } + sourceMapPrefixField?.let { args.sourceMapPrefix = it } + sourceMapSourceRootsField?.let { args.sourceMapSourceRoots = it } targetField?.let { args.target = it } typedArraysField?.let { args.typedArrays = it } } @@ -98,6 +110,8 @@ internal fun org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments.fil noStdlib = true outputFile = null sourceMap = false + sourceMapPrefix = null + sourceMapSourceRoots = null target = "v5" typedArrays = false }