From 174068c462b6abf6270fab1337e708a8a201dbcf Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 23 May 2017 12:14:26 +0300 Subject: [PATCH] Add JS source map parser and remapper JS source map remapper takes parsed source maps together with JS AST with correct JS positioning information and converts the latter into Kotlin positioning information --- .idea/libraries/json_org.xml | 9 + build.xml | 2 + js/js.parser/js.parser.iml | 1 + .../kotlin/js/parser/sourcemaps/SourceMap.kt | 38 ++++ .../sourcemaps/SourceMapLocationRemapper.kt | 70 ++++++++ .../js/parser/sourcemaps/SourceMapParser.kt | 169 ++++++++++++++++++ update_dependencies.xml | 3 + 7 files changed, 292 insertions(+) create mode 100644 .idea/libraries/json_org.xml create mode 100644 js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMap.kt create mode 100644 js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapLocationRemapper.kt create mode 100644 js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapParser.kt diff --git a/.idea/libraries/json_org.xml b/.idea/libraries/json_org.xml new file mode 100644 index 00000000000..37a4912e679 --- /dev/null +++ b/.idea/libraries/json_org.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/build.xml b/build.xml index 44f24010bef..e4db33970dc 100644 --- a/build.xml +++ b/build.xml @@ -65,6 +65,7 @@ + @@ -385,6 +386,7 @@ + diff --git a/js/js.parser/js.parser.iml b/js/js.parser/js.parser.iml index 9add1fd0a16..8066bb12c1a 100644 --- a/js/js.parser/js.parser.iml +++ b/js/js.parser/js.parser.iml @@ -10,5 +10,6 @@ + \ No newline at end of file diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMap.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMap.kt new file mode 100644 index 00000000000..e19e90834a5 --- /dev/null +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMap.kt @@ -0,0 +1,38 @@ +/* + * 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.sourcemaps + +class SourceMap { + val groups = mutableListOf() +} + +class SourceMapSegment( + val generatedColumnNumber: Int, + val sourceFileName: String, + val sourceLineNumber: Int, + val sourceColumnNumber: Int +) + +class SourceMapGroup { + val segments = mutableListOf() +} + +sealed class SourceMapParseResult + +class SourceMapSuccess(val value: SourceMap) : SourceMapParseResult() + +class SourceMapError(val message: String) : SourceMapParseResult() \ No newline at end of file diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapLocationRemapper.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapLocationRemapper.kt new file mode 100644 index 00000000000..5adf792e1ce --- /dev/null +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapLocationRemapper.kt @@ -0,0 +1,70 @@ +/* + * 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.sourcemaps + +import org.jetbrains.kotlin.js.backend.ast.JsLocation +import org.jetbrains.kotlin.js.backend.ast.JsNode +import org.jetbrains.kotlin.js.backend.ast.RecursiveJsVisitor +import org.jetbrains.kotlin.js.backend.ast.SourceInfoAwareJsNode + +class SourceMapLocationRemapper(val sourceMaps: Map) { + fun remap(node: JsNode) { + node.accept(visitor) + } + + private val visitor = object : RecursiveJsVisitor() { + private var lastSourceMap: SourceMap? = null + private var lastGroup: SourceMapGroup? = null + private var lastSegmentIndex = 0 + + override fun visitElement(node: JsNode) { + if (node is SourceInfoAwareJsNode) { + if (!remapNode(node)) { + node.source = null + } + } + super.visitElement(node) + } + + private fun remapNode(node: SourceInfoAwareJsNode): Boolean { + val source = node.source as? JsLocation ?: return false + + val sourceMap = sourceMaps[source.file] ?: return false + val group = sourceMap.groups.getOrElse(source.startLine) { return false } + if (group.segments.isEmpty()) return false + + if (lastSourceMap != sourceMap || lastGroup != group) { + lastSegmentIndex = 0 + } + if (group.segments[lastSegmentIndex].generatedColumnNumber > source.startChar) { + if (lastSegmentIndex == 0) return false + lastSegmentIndex = 0 + } + + while (lastSegmentIndex + 1 < group.segments.size) { + val nextIndex = lastSegmentIndex + 1 + if (group.segments[nextIndex].generatedColumnNumber > source.startChar) break + lastSegmentIndex = nextIndex + } + + val segment = group.segments[lastSegmentIndex] + node.source = JsLocation(segment.sourceFileName, segment.sourceLineNumber, segment.sourceColumnNumber) + + return true + } + } +} \ No newline at end of file diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapParser.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapParser.kt new file mode 100644 index 00000000000..65323b064f1 --- /dev/null +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/SourceMapParser.kt @@ -0,0 +1,169 @@ +/* + * 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. + */ + +/* + * 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.sourcemaps + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONTokener +import java.io.IOException +import java.io.Reader + +object SourceMapParser { + @Throws(IOException::class) + fun parse(reader: Reader): SourceMapParseResult { + val jsonObject = try { + JSONObject(JSONTokener(reader)) + } + catch (e: JSONException) { + return SourceMapError(e.message ?: "parse error") + } + + if (!jsonObject.has("version")) return SourceMapError("Version not defined") + jsonObject.get("version").let { + if (it != 3) return SourceMapError("Unsupported version: $it") + } + + val sourceRoot = if (jsonObject.has("sourceRoot")) { + jsonObject.get("sourceRoot") as? String ?: return SourceMapError("'sourceRoot' property is not of string type") + } + else { + "" + } + + val sources = if (jsonObject.has("sources")) { + val sourcesProperty = jsonObject.get("sources") as? JSONArray ?: + return SourceMapError("'sources' property is not of array type") + sourcesProperty.map { + it as? String ?: return SourceMapError("'sources' array must contain strings") + } + } + else { + emptyList() + } + + if (!jsonObject.has("mappings")) return SourceMapError("'mappings' property not found") + val mappings = jsonObject.get("mappings") as? String ?: return SourceMapError("'mappings' property is not of string type") + + var jsColumn = 0 + var sourceLine = 0 + var sourceColumn = 0 + var sourceIndex = 0 + val stream = MappingStream(mappings) + val sourceMap = SourceMap() + var currentGroup = SourceMapGroup().also { sourceMap.groups += it } + + while (!stream.isEof) { + if (stream.isGroupTerminator) { + currentGroup = SourceMapGroup().also { sourceMap.groups += it } + jsColumn = 0 + stream.skipChar() + continue + } + + jsColumn += stream.readInt() ?: return stream.createError("VLQ-encoded JS column number expected") + + if (stream.isEncodedInt) { + sourceIndex += stream.readInt() ?: return stream.createError("VLQ-encoded source index expected") + sourceLine += stream.readInt() ?: return stream.createError("VLQ-encoded source line expected") + sourceColumn += stream.readInt() ?: return stream.createError("VLQ-encoded source column expected") + if (stream.isEncodedInt) { + stream.readInt() ?: return stream.createError("VLQ-encoded name index expected") + } + } + + if (sourceIndex !in sources.indices) { + return stream.createError("Source index $sourceIndex is out of bounds ${sources.indices}") + } + + currentGroup.segments += SourceMapSegment(jsColumn, sourceRoot + sources[sourceIndex], sourceLine, sourceColumn) + + when { + stream.isEof -> return stream.createError("Unexpected EOF, ',' or ';' expected") + stream.isGroupTerminator -> { + currentGroup = SourceMapGroup().also { sourceMap.groups += it } + jsColumn = 0 + } + !stream.isSegmentTerminator -> return stream.createError("Unexpected char, ',' or ';' expected") + } + stream.skipChar() + } + + return SourceMapSuccess(sourceMap) + } + + internal class MappingStream(val string: String) { + var position = 0 + + val isEof: Boolean get() = position == string.length + + val isSegmentTerminator: Boolean get() = string[position] == ',' + + val isGroupTerminator: Boolean get() = string[position] == ';' + + val isEncodedInt: Boolean get() = !isEof && !isSegmentTerminator && !isGroupTerminator + + fun skipChar() { + position++ + } + + fun readInt(): Int? { + var value = 0 + var shift = 0 + while (true) { + if (isEof) return null + val digit = base64value(string[position++]) ?: return null + + val digitValue = digit and 0x1F + value = value or (digitValue shl shift) + + if ((digit and 0x20) == 0) break + shift += 5 + } + + val unsignedValue = value ushr 1 + return if ((value and 1) == 0) unsignedValue else -unsignedValue + } + + private fun base64value(c: Char): Int? = when (c) { + in 'A'..'Z' -> c.toInt() - 'A'.toInt() + in 'a'..'z' -> c.toInt() - 'a'.toInt() + 26 + in '0'..'9' -> c.toInt() - '0'.toInt() + 52 + '+' -> 62 + '/' -> 63 + else -> null + } + + fun createError(error: String): SourceMapError = SourceMapError("Error parsing stream at offset $position: $error") + } +} diff --git a/update_dependencies.xml b/update_dependencies.xml index 9ed51affeea..e7dfbbba92e 100644 --- a/update_dependencies.xml +++ b/update_dependencies.xml @@ -295,6 +295,9 @@ + + +