summaryrefslogtreecommitdiff
path: root/platform/script-debugger/backend/src/debugger/sourcemap
diff options
context:
space:
mode:
Diffstat (limited to 'platform/script-debugger/backend/src/debugger/sourcemap')
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java79
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt37
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt210
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt119
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt81
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt372
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt186
7 files changed, 1084 insertions, 0 deletions
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java
new file mode 100644
index 00000000..7576f500
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap;
+
+final class Base64VLQ {
+ private Base64VLQ() {
+ }
+
+ interface CharIterator {
+ boolean hasNext();
+ char next();
+ }
+
+ // A Base64 VLQ digit can represent 5 bits, so it is base-32.
+ private static final int VLQ_BASE_SHIFT = 5;
+ private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT;
+
+ // A mask of bits for a VLQ digit (11111), 31 decimal.
+ private static final int VLQ_BASE_MASK = VLQ_BASE - 1;
+
+ // The continuation bit is the 6th bit.
+ private static final int VLQ_CONTINUATION_BIT = VLQ_BASE;
+
+ /**
+ * Decodes the next VLQValue from the provided CharIterator.
+ */
+ public static int decode(CharIterator in) {
+ int result = 0;
+ int shift = 0;
+ int digit;
+ do {
+ digit = Base64.BASE64_DECODE_MAP[in.next()];
+ assert (digit != -1) : "invalid char";
+
+ result += (digit & VLQ_BASE_MASK) << shift;
+ shift += VLQ_BASE_SHIFT;
+ }
+ while ((digit & VLQ_CONTINUATION_BIT) != 0);
+
+ boolean negate = (result & 1) == 1;
+ result >>= 1;
+ return negate ? -result : result;
+ }
+
+ private static final class Base64 {
+ /**
+ * A map used to convert integer values in the range 0-63 to their base64
+ * values.
+ */
+ private static final String BASE64_MAP =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+ "abcdefghijklmnopqrstuvwxyz" +
+ "0123456789+/";
+
+ /**
+ * A map used to convert base64 character into integer values.
+ */
+ private static final int[] BASE64_DECODE_MAP = new int[256];
+
+ static {
+ for (int i = 0; i < BASE64_MAP.length(); i++) {
+ BASE64_DECODE_MAP[BASE64_MAP.charAt(i)] = i;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt
new file mode 100644
index 00000000..8f9aad86
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap
+
+/**
+ * Mapping entry in the source map
+ */
+interface MappingEntry {
+ val generatedColumn: Int
+
+ val generatedLine: Int
+
+ val sourceLine: Int
+
+ val sourceColumn: Int
+
+ val source: Int
+ get() = -1
+
+ val name: String?
+ get() = null
+
+ val nextGenerated: MappingEntry
+}
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt
new file mode 100644
index 00000000..c87786c0
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap
+
+import com.intellij.openapi.editor.Document
+import java.util.*
+
+interface Mappings {
+ fun get(line: Int, column: Int): MappingEntry?
+
+ fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean = true): MappingEntry?
+
+ fun getNext(mapping: MappingEntry): MappingEntry?
+
+ fun getNextOnTheSameLine(mapping: MappingEntry): MappingEntry? {
+ val nextMapping = getNext(mapping)
+ return if (nextMapping != null && getLine(nextMapping) == getLine(mapping)) nextMapping else null
+ }
+
+ fun indexOf(line: Int, column: Int): Int
+
+ fun getByIndex(index: Int): MappingEntry
+
+ fun getLine(mapping: MappingEntry): Int
+
+ fun getColumn(mapping: MappingEntry): Int
+}
+
+abstract class MappingList(private val mappings: List<MappingEntry>) : Mappings {
+ val size: Int
+ get() = mappings.size
+
+ protected abstract val comparator: Comparator<MappingEntry>
+
+ override fun indexOf(line: Int, column: Int): Int {
+ var low = 0
+ var high = mappings.size - 1
+ if (mappings.isEmpty() || getLine(mappings[low]) > line || getLine(mappings[high]) < line) {
+ return -1
+ }
+
+ while (low <= high) {
+ val middle = (low + high).ushr(1)
+ val mapping = mappings[middle]
+ val mappingLine = getLine(mapping)
+ if (line == mappingLine) {
+ if (column == getColumn(mapping)) {
+ // find first
+ var firstIndex = middle
+ while (firstIndex > 0) {
+ val prevMapping = mappings[firstIndex - 1]
+ if (getLine(prevMapping) == line && getColumn(prevMapping) == column) {
+ firstIndex--
+ }
+ else {
+ break
+ }
+ }
+ return firstIndex
+ }
+ else if (column < getColumn(mapping)) {
+ if (column == 0 || column == -1) {
+ // find first
+ var firstIndex = middle
+ while (firstIndex > 0 && getLine(mappings[firstIndex - 1]) == line) {
+ firstIndex--
+ }
+ return firstIndex
+ }
+
+ if (middle == 0) {
+ return -1
+ }
+
+ val prevMapping = mappings[middle - 1]
+ when {
+ line != getLine(prevMapping) -> return -1
+ column >= getColumn(prevMapping) -> return middle - 1
+ else -> high = middle - 1
+ }
+ }
+ else {
+ // https://code.google.com/p/google-web-toolkit/issues/detail?id=9103
+ // We skipIfColumnEquals because GWT has two entries - source position equals, but generated no. We must use first entry (at least, in case of GWT it is correct)
+ val nextMapping = getNextOnTheSameLine(middle)
+ if (nextMapping == null) {
+ return middle
+ }
+ else {
+ low = middle + 1
+ }
+ }
+ }
+ else if (line > mappingLine) {
+ low = middle + 1
+ }
+ else {
+ high = middle - 1
+ }
+ }
+
+ return -1
+ }
+
+ // todo honor Google Chrome bug related to paused location
+ override fun get(line: Int, column: Int): MappingEntry? = mappings.getOrNull(indexOf(line, column))
+
+ private fun getNext(index: Int) = mappings.getOrNull(index + 1)
+
+ override fun getNext(mapping: MappingEntry): MappingEntry? {
+ if (comparator == MAPPING_COMPARATOR_BY_GENERATED_POSITION) {
+ return mapping.nextGenerated
+ }
+
+ var index = mappings.binarySearch(mapping, comparator)
+ if (index < 0) {
+ return null
+ }
+ index++
+
+ var result: MappingEntry?
+ do {
+ result = mappings.getOrNull(index++)
+ }
+ while (mapping === result)
+ return result
+ }
+
+ override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean): MappingEntry? {
+ var nextMapping = getNext(index) ?: return null
+
+ val mapping = getByIndex(index)
+ if (getLine(nextMapping) != getLine(mapping)) {
+ return null
+ }
+
+ if (skipIfColumnEquals) {
+ var i = index
+ // several generated segments can point to one source segment, so, in mapping list ordered by source, could be several mappings equal in terms of source position
+ while (getColumn(nextMapping) == getColumn(mapping)) {
+ nextMapping = getNextOnTheSameLine(++i, false) ?: return null
+ }
+ }
+
+ return nextMapping
+ }
+
+ fun getEndOffset(mapping: MappingEntry, lineStartOffset: Int, document: Document): Int {
+ val nextMapping = getNextOnTheSameLine(Collections.binarySearch(mappings, mapping, comparator))
+ return if (nextMapping == null) document.getLineEndOffset(getLine(mapping)) else lineStartOffset + getColumn(nextMapping)
+ }
+
+ override fun getByIndex(index: Int): MappingEntry = mappings.get(index)
+
+ // entries will be processed in this list order
+ fun processMappingsInLine(line: Int, entryProcessor: MappingsProcessorInLine): Boolean {
+ var low = 0
+ var high = mappings.size - 1
+ while (low <= high) {
+ val middle = (low + high).ushr(1)
+ val mapping = mappings.get(middle)
+ val mappingLine = getLine(mapping)
+ when {
+ line == mappingLine -> {
+ // find first
+ var firstIndex = middle
+ while (firstIndex > 0 && getLine(mappings.get(firstIndex - 1)) == line) {
+ firstIndex--
+ }
+
+ var entry: MappingEntry? = mappings.get(firstIndex)
+ do {
+ var nextEntry = mappings.getOrNull(++firstIndex)
+ if (nextEntry != null && getLine(nextEntry) != line) {
+ nextEntry = null
+ }
+
+ if (!entryProcessor.process(entry!!, nextEntry)) {
+ return true
+ }
+
+ entry = nextEntry
+ }
+ while (entry != null)
+ return true
+ }
+ line > mappingLine -> low = middle + 1
+ else -> high = middle - 1
+ }
+ }
+ return false
+ }
+}
+
+interface MappingsProcessorInLine {
+ fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean
+}
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt
new file mode 100644
index 00000000..c4a5ebfc
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap
+
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.util.Url
+import gnu.trove.THashMap
+
+class NestedSourceMap(private val childMap: SourceMap, private val parentMap: SourceMap) : SourceMap {
+ override val sourceResolver: SourceResolver
+ get() = parentMap.sourceResolver
+
+ override val sources: Array<Url>
+ get() = parentMap.sources
+
+ private val sourceIndexToSourceMappings = arrayOfNulls<Mappings>(parentMap.sources.size)
+
+ private val childMappingToTransformed = THashMap<MappingEntry, MappingEntry>()
+
+ override val outFile: String?
+ get() = childMap.outFile
+
+ override val hasNameMappings: Boolean
+ get() = childMap.hasNameMappings || parentMap.hasNameMappings
+
+ override val generatedMappings: Mappings by lazy {
+ NestedMappings(childMap.generatedMappings, parentMap.generatedMappings, false)
+ }
+
+ override fun findSourceMappings(sourceIndex: Int): Mappings {
+ var result = sourceIndexToSourceMappings.get(sourceIndex)
+ if (result == null) {
+ result = NestedMappings(childMap.findSourceMappings(sourceIndex), parentMap.findSourceMappings(sourceIndex), true)
+ sourceIndexToSourceMappings.set(sourceIndex, result)
+ }
+ return result
+ }
+
+ override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceFile, localFileUrlOnly)
+
+ override fun findSourceIndex(sourceUrls: List<Url>,
+ sourceFile: VirtualFile?,
+ resolver: Lazy<SourceFileResolver?>?,
+ localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly)
+
+ override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean {
+ val childSourceMappings = childMap.findSourceMappings(sourceIndex)
+ return (parentMap.findSourceMappings(sourceIndex) as MappingList).processMappingsInLine(sourceLine, object: MappingsProcessorInLine {
+ override fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean {
+ val childIndex = childSourceMappings.indexOf(entry.generatedLine, entry.generatedColumn)
+ if (childIndex == -1) {
+ return true
+ }
+
+ val childEntry = childSourceMappings.getByIndex(childIndex)
+ // todo not clear - should we resolve next child entry by current child index or by provided parent nextEntry?
+ val nextChildEntry = if (nextEntry == null) null else childSourceMappings.getNextOnTheSameLine(childIndex)
+ return mappingProcessor.process(childMappingToTransformed.getOrPut(childEntry) { NestedMappingEntry(childEntry, entry) },
+ nextChildEntry?.let { childMappingToTransformed.getOrPut(it) { NestedMappingEntry(it, entry) } })
+ }
+ })
+ }
+}
+
+private class NestedMappings(private val child: Mappings, private val parent: Mappings, private val isSourceMappings: Boolean) : Mappings {
+ override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean) = parent.getNextOnTheSameLine(index, skipIfColumnEquals)
+
+ override fun getNext(mapping: MappingEntry) = parent.getNext(mapping)
+
+ override fun indexOf(line: Int, column: Int) = parent.indexOf(line, column)
+
+ override fun getByIndex(index: Int) = parent.getByIndex(index)
+
+ override fun getLine(mapping: MappingEntry) = parent.getLine(mapping)
+
+ override fun getColumn(mapping: MappingEntry) = parent.getColumn(mapping)
+
+ override fun get(line: Int, column: Int): MappingEntry? {
+ return if (isSourceMappings) {
+ parent.get(line, column)?.let { child.get(it.generatedLine, it.generatedColumn) }
+ }
+ else {
+ child.get(line, column)?.let { parent.get(it.sourceLine, it.sourceColumn) }
+ }
+ }
+}
+
+private data class NestedMappingEntry(private val child: MappingEntry, private val parent: MappingEntry) : MappingEntry {
+ override val generatedLine: Int
+ get() = child.generatedLine
+
+ override val generatedColumn: Int
+ get() = child.generatedColumn
+
+ override val sourceLine: Int
+ get() = parent.sourceLine
+
+ override val sourceColumn: Int
+ get() = parent.sourceColumn
+
+ override val name: String?
+ get() = parent.name
+
+ override val nextGenerated: MappingEntry
+ get() = child.nextGenerated
+} \ No newline at end of file
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt
new file mode 100644
index 00000000..283a5b6c
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap
+
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.util.Url
+
+// sources - is not originally specified, but canonicalized/normalized
+// lines and columns are zero-based according to specification
+interface SourceMap {
+ val outFile: String?
+
+ /**
+ * note: Nested map returns only parent sources
+ */
+ val sources: Array<Url>
+
+ val generatedMappings: Mappings
+ val hasNameMappings: Boolean
+ val sourceResolver: SourceResolver
+
+ fun findSourceMappings(sourceIndex: Int): Mappings
+
+ fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int
+
+ fun findSourceMappings(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Mappings? {
+ val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly)
+ return if (sourceIndex >= 0) findSourceMappings(sourceIndex) else null
+ }
+
+ fun getSourceLineByRawLocation(rawLine: Int, rawColumn: Int): Int = generatedMappings.get(rawLine, rawColumn)?.sourceLine ?: -1
+
+ fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int
+
+ fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean
+
+ fun processSourceMappingsInLine(sourceUrls: List<Url>, sourceLine: Int, mappingProcessor: MappingsProcessorInLine, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Boolean {
+ val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly)
+ return sourceIndex >= 0 && processSourceMappingsInLine(sourceIndex, sourceLine, mappingProcessor)
+ }
+}
+
+
+class OneLevelSourceMap(override val outFile: String?,
+ override val generatedMappings: Mappings,
+ private val sourceIndexToMappings: Array<MappingList?>,
+ override val sourceResolver: SourceResolver,
+ override val hasNameMappings: Boolean) : SourceMap {
+ override val sources: Array<Url>
+ get() = sourceResolver.canonicalizedUrls
+
+ override fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int {
+ val index = sourceResolver.findSourceIndex(sourceUrls, sourceFile, localFileUrlOnly)
+ if (index == -1 && resolver != null) {
+ return resolver.value?.let { sourceResolver.findSourceIndex(it) } ?: -1
+ }
+ return index
+ }
+
+ // returns SourceMappingList
+ override fun findSourceMappings(sourceIndex: Int): MappingList = sourceIndexToMappings.get(sourceIndex)!!
+
+ override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = sourceResolver.findSourceIndexByFile(sourceFile, localFileUrlOnly)
+
+ override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean {
+ return findSourceMappings(sourceIndex).processMappingsInLine(sourceLine, mappingProcessor)
+ }
+} \ No newline at end of file
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt
new file mode 100644
index 00000000..3ecdd792
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2000-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.debugger.sourcemap
+
+import com.google.gson.stream.JsonToken
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.util.text.StringUtilRt
+import com.intellij.util.PathUtil
+import com.intellij.util.SmartList
+import com.intellij.util.UriUtil
+import com.intellij.util.containers.isNullOrEmpty
+import org.jetbrains.debugger.sourcemap.Base64VLQ.CharIterator
+import org.jetbrains.io.JsonReaderEx
+import java.io.IOException
+import java.util.*
+import kotlin.properties.Delegates.notNull
+
+private val MAPPING_COMPARATOR_BY_SOURCE_POSITION = Comparator<MappingEntry> { o1, o2 ->
+ if (o1.sourceLine == o2.sourceLine) {
+ o1.sourceColumn - o2.sourceColumn
+ }
+ else {
+ o1.sourceLine - o2.sourceLine
+ }
+}
+
+val MAPPING_COMPARATOR_BY_GENERATED_POSITION: Comparator<MappingEntry> = Comparator { o1, o2 ->
+ if (o1.generatedLine == o2.generatedLine) {
+ o1.generatedColumn - o2.generatedColumn
+ }
+ else {
+ o1.generatedLine - o2.generatedLine
+ }
+}
+
+internal const val UNMAPPED = -1
+
+// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US
+fun decodeSourceMap(`in`: CharSequence, sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? {
+ if (`in`.isEmpty()) {
+ throw IOException("source map contents cannot be empty")
+ }
+
+ val reader = JsonReaderEx(`in`)
+ reader.isLenient = true
+ return parseMap(reader, 0, 0, ArrayList(), sourceResolverFactory)
+}
+
+private fun parseMap(reader: JsonReaderEx,
+ line: Int,
+ column: Int,
+ mappings: MutableList<MappingEntry>,
+ sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? {
+ reader.beginObject()
+ var sourceRoot: String? = null
+ var sourcesReader: JsonReaderEx? = null
+ var names: List<String>? = null
+ var encodedMappings: String? = null
+ var file: String? = null
+ var version = -1
+ var sourcesContent: MutableList<String?>? = null
+ while (reader.hasNext()) {
+ when (reader.nextName()) {
+ "sections" -> throw IOException("sections is not supported yet")
+ "version" -> {
+ version = reader.nextInt()
+ }
+ "sourceRoot" -> {
+ sourceRoot = StringUtil.nullize(readSourcePath(reader))
+ if (sourceRoot != null && sourceRoot != "/") {
+ sourceRoot = UriUtil.trimTrailingSlashes(sourceRoot)
+ }
+ }
+ "sources" -> {
+ sourcesReader = reader.subReader()
+ reader.skipValue()
+ }
+ "names" -> {
+ reader.beginArray()
+ if (reader.hasNext()) {
+ names = ArrayList()
+ do {
+ if (reader.peek() == JsonToken.BEGIN_OBJECT) {
+ // polymer map
+ reader.skipValue()
+ names.add("POLYMER UNKNOWN NAME")
+ }
+ else {
+ names.add(reader.nextString(true))
+ }
+ }
+ while (reader.hasNext())
+ }
+ else {
+ names = emptyList()
+ }
+ reader.endArray()
+ }
+ "mappings" -> {
+ encodedMappings = reader.nextString()
+ }
+ "file" -> {
+ file = reader.nextNullableString()
+ }
+ "sourcesContent" -> {
+ reader.beginArray()
+ if (reader.peek() != JsonToken.END_ARRAY) {
+ sourcesContent = SmartList<String>()
+ do {
+ if (reader.peek() == JsonToken.STRING) {
+ sourcesContent.add(StringUtilRt.convertLineSeparators(reader.nextString()))
+ }
+ else if (reader.peek() == JsonToken.NULL) {
+ // null means source file should be resolved by url
+ sourcesContent.add(null)
+ reader.nextNull()
+ }
+ else {
+ logger<SourceMap>().warn("Unknown sourcesContent element: ${reader.peek().name}")
+ reader.skipValue()
+ }
+ }
+ while (reader.hasNext())
+ }
+ reader.endArray()
+ }
+ else -> {
+ // skip file or extensions
+ reader.skipValue()
+ }
+ }
+ }
+ reader.close()
+
+ // check it before other checks, probably it is not a sourcemap file
+ if (encodedMappings.isNullOrEmpty()) {
+ // empty map
+ return null
+ }
+
+ if (Registry.`is`("js.debugger.fix.jspm.source.maps", false) && encodedMappings!!.startsWith(";") && file != null && file.endsWith(".ts!transpiled")) {
+ encodedMappings = encodedMappings.substring(1)
+ }
+
+ if (version != 3) {
+ throw IOException("Unsupported sourcemap version: $version")
+ }
+
+ if (sourcesReader == null) {
+ throw IOException("sources is not specified")
+ }
+
+ val sources = readSources(sourcesReader, sourceRoot)
+ if (sources.isEmpty()) {
+ // empty map, meteor can report such ugly maps
+ return null
+ }
+
+ val reverseMappingsBySourceUrl = arrayOfNulls<MutableList<MappingEntry>?>(sources.size)
+ readMappings(encodedMappings!!, line, column, mappings, reverseMappingsBySourceUrl, names)
+
+ val sourceToEntries = Array<MappingList?>(reverseMappingsBySourceUrl.size) {
+ val entries = reverseMappingsBySourceUrl[it]
+ if (entries == null) {
+ null
+ }
+ else {
+ entries.sortWith(MAPPING_COMPARATOR_BY_SOURCE_POSITION)
+ SourceMappingList(entries)
+ }
+ }
+ return OneLevelSourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty())
+}
+
+private fun readSourcePath(reader: JsonReaderEx): String = PathUtil.toSystemIndependentName(reader.nextString().trim { it <= ' ' })
+
+private fun readMappings(value: String,
+ initialLine: Int,
+ initialColumn: Int,
+ mappings: MutableList<MappingEntry>,
+ reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>,
+ names: List<String>?) {
+ if (value.isEmpty()) {
+ return
+ }
+
+ var line = initialLine
+ var column = initialColumn
+ val charIterator = CharSequenceIterator(value)
+ var sourceIndex = 0
+ var reverseMappings: MutableList<MappingEntry> = getMapping(reverseMappingsBySourceUrl, sourceIndex)
+ var sourceLine = 0
+ var sourceColumn = 0
+ var nameIndex = 0
+ var prevEntry: MutableEntry? = null
+
+ fun addEntry(entry: MutableEntry) {
+ if (prevEntry != null) {
+ prevEntry!!.nextGenerated = entry
+ }
+ prevEntry = entry
+ mappings.add(entry)
+ }
+
+ while (charIterator.hasNext()) {
+ if (charIterator.peek() == ',') {
+ charIterator.next()
+ }
+ else {
+ while (charIterator.peek() == ';') {
+ line++
+ column = 0
+ charIterator.next()
+ if (!charIterator.hasNext()) {
+ return
+ }
+ }
+ }
+
+ column += Base64VLQ.decode(charIterator)
+ if (isSeparator(charIterator)) {
+ addEntry(UnmappedEntry(line, column))
+ continue
+ }
+
+ val sourceIndexDelta = Base64VLQ.decode(charIterator)
+ if (sourceIndexDelta != 0) {
+ sourceIndex += sourceIndexDelta
+ reverseMappings = getMapping(reverseMappingsBySourceUrl, sourceIndex)
+ }
+ sourceLine += Base64VLQ.decode(charIterator)
+ sourceColumn += Base64VLQ.decode(charIterator)
+
+ val entry: MutableEntry
+ if (isSeparator(charIterator)) {
+ entry = UnnamedEntry(line, column, sourceIndex, sourceLine, sourceColumn)
+ }
+ else {
+ nameIndex += Base64VLQ.decode(charIterator)
+ assert(names != null)
+ entry = NamedEntry(names!![nameIndex], line, column, sourceIndex, sourceLine, sourceColumn)
+ }
+ reverseMappings.add(entry)
+ addEntry(entry)
+ }
+}
+
+private fun readSources(reader: JsonReaderEx, sourceRoot: String?): List<String> {
+ reader.beginArray()
+ val sources: List<String>
+ if (reader.peek() == JsonToken.END_ARRAY) {
+ sources = emptyList()
+ }
+ else {
+ sources = SmartList<String>()
+ do {
+ var sourceUrl: String = readSourcePath(reader)
+ if (!sourceRoot.isNullOrEmpty()) {
+ if (sourceRoot == "/") {
+ sourceUrl = "/$sourceUrl"
+ }
+ else {
+ sourceUrl = "$sourceRoot/$sourceUrl"
+ }
+ }
+ sources.add(sourceUrl)
+ }
+ while (reader.hasNext())
+ }
+ reader.endArray()
+ return sources
+}
+
+private fun getMapping(reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, sourceIndex: Int): MutableList<MappingEntry> {
+ var reverseMappings = reverseMappingsBySourceUrl.get(sourceIndex)
+ if (reverseMappings == null) {
+ reverseMappings = ArrayList()
+ reverseMappingsBySourceUrl.set(sourceIndex, reverseMappings)
+ }
+ return reverseMappings
+}
+
+private fun isSeparator(charIterator: CharSequenceIterator): Boolean {
+ if (!charIterator.hasNext()) {
+ return true
+ }
+
+ val current = charIterator.peek()
+ return current == ',' || current == ';'
+}
+
+interface MutableEntry : MappingEntry {
+ override var nextGenerated: MappingEntry
+}
+
+/**
+ * Not mapped to a section in the original source.
+ */
+private data class UnmappedEntry(override val generatedLine: Int, override val generatedColumn: Int) : MappingEntry, MutableEntry {
+ override val sourceLine = UNMAPPED
+
+ override val sourceColumn = UNMAPPED
+
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+/**
+ * Mapped to a section in the original source.
+ */
+private data class UnnamedEntry(override val generatedLine: Int,
+ override val generatedColumn: Int,
+ override val source: Int,
+ override val sourceLine: Int,
+ override val sourceColumn: Int) : MappingEntry, MutableEntry {
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+/**
+ * Mapped to a section in the original source, and is associated with a name.
+ */
+private data class NamedEntry(override val name: String,
+ override val generatedLine: Int,
+ override val generatedColumn: Int,
+ override val source: Int,
+ override val sourceLine: Int,
+ override val sourceColumn: Int) : MappingEntry, MutableEntry {
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+// java CharacterIterator is ugly, next() impl, so, we reinvent
+private class CharSequenceIterator(private val content: CharSequence) : CharIterator {
+ private val length = content.length
+ private var current = 0
+
+ override fun next() = content.get(current++)
+
+ internal fun peek() = content.get(current)
+
+ override fun hasNext() = current < length
+}
+
+private class SourceMappingList(mappings: List<MappingEntry>) : MappingList(mappings) {
+ override fun getLine(mapping: MappingEntry) = mapping.sourceLine
+
+ override fun getColumn(mapping: MappingEntry) = mapping.sourceColumn
+
+ override val comparator = MAPPING_COMPARATOR_BY_SOURCE_POSITION
+}
+
+private class GeneratedMappingList(mappings: List<MappingEntry>) : MappingList(mappings) {
+ override fun getLine(mapping: MappingEntry) = mapping.generatedLine
+
+ override fun getColumn(mapping: MappingEntry) = mapping.generatedColumn
+
+ override val comparator = MAPPING_COMPARATOR_BY_GENERATED_POSITION
+}
+
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt
new file mode 100644
index 00000000..2a9c868f
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt
@@ -0,0 +1,186 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package org.jetbrains.debugger.sourcemap
+
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.vfs.StandardFileSystems
+import com.intellij.openapi.vfs.VfsUtilCore
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.util.Url
+import com.intellij.util.Urls
+import com.intellij.util.containers.ObjectIntHashMap
+import com.intellij.util.containers.isNullOrEmpty
+import com.intellij.util.io.URLUtil
+import java.io.File
+
+interface SourceFileResolver {
+ /**
+ * Return -1 if no match
+ */
+ fun resolve(map: ObjectIntHashMap<Url>): Int = -1
+ fun resolve(rawSources: List<String>): Int = -1
+}
+
+class SourceResolver(private val rawSources: List<String>,
+ trimFileScheme: Boolean,
+ baseUrl: Url?,
+ private val sourceContents: List<String?>?,
+ baseUrlIsFile: Boolean = true) {
+ companion object {
+ fun isAbsolute(path: String): Boolean = path.startsWith('/') || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':'))
+ }
+
+ val canonicalizedUrls: Array<Url> by lazy {
+ Array(rawSources.size) { canonicalizeUrl(rawSources[it], baseUrl, trimFileScheme, baseUrlIsFile) }
+ }
+
+ private val canonicalizedUrlToSourceIndex: ObjectIntHashMap<Url> by lazy {
+ (
+ if (SystemInfo.isFileSystemCaseSensitive) ObjectIntHashMap(rawSources.size)
+ else ObjectIntHashMap(rawSources.size, Urls.caseInsensitiveUrlHashingStrategy)
+ ).also {
+ for (i in rawSources.indices) {
+ it.put(canonicalizedUrls[i], i)
+ }
+ }
+ }
+
+ fun getSource(entry: MappingEntry): Url? {
+ val index = entry.source
+ return if (index < 0) null else canonicalizedUrls[index]
+ }
+
+ fun getSourceContent(entry: MappingEntry): String? {
+ if (sourceContents.isNullOrEmpty()) {
+ return null
+ }
+
+ val index = entry.source
+ return if (index < 0 || index >= sourceContents!!.size) null else sourceContents[index]
+ }
+
+ fun getSourceContent(sourceIndex: Int): String? {
+ if (sourceContents.isNullOrEmpty()) {
+ return null
+ }
+ return if (sourceIndex < 0 || sourceIndex >= sourceContents!!.size) null else sourceContents[sourceIndex]
+ }
+
+ fun getSourceIndex(url: Url): Int = canonicalizedUrlToSourceIndex[url]
+
+ fun getRawSource(entry: MappingEntry): String? {
+ val index = entry.source
+ return if (index < 0) null else rawSources[index]
+ }
+
+ internal fun findSourceIndex(resolver: SourceFileResolver): Int {
+ val resolveByCanonicalizedUrls = resolver.resolve(canonicalizedUrlToSourceIndex)
+ return if (resolveByCanonicalizedUrls != -1) resolveByCanonicalizedUrls else resolver.resolve(rawSources)
+ }
+
+ fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, localFileUrlOnly: Boolean): Int {
+ for (sourceUrl in sourceUrls) {
+ val index = canonicalizedUrlToSourceIndex.get(sourceUrl)
+ if (index != -1) {
+ return index
+ }
+ }
+
+ if (sourceFile != null) {
+ return findSourceIndexByFile(sourceFile, localFileUrlOnly)
+ }
+ return -1
+ }
+
+ internal fun findSourceIndexByFile(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int {
+ if (!localFileUrlOnly) {
+ val index = canonicalizedUrlToSourceIndex.get(Urls.newFromVirtualFile(sourceFile).trimParameters())
+ if (index != -1) {
+ return index
+ }
+ }
+
+ if (!sourceFile.isInLocalFileSystem) {
+ return -1
+ }
+
+ // local file url - without "file" scheme, just path
+ val index = canonicalizedUrlToSourceIndex.get(Urls.newLocalFileUrl(sourceFile))
+ if (index != -1) {
+ return index
+ }
+
+ // ok, search by canonical path
+ val canonicalFile = sourceFile.canonicalFile
+ if (canonicalFile != null && canonicalFile != sourceFile) {
+ for (i in canonicalizedUrls.indices) {
+ val url = canonicalizedUrls.get(i)
+ if (Urls.equalsIgnoreParameters(url, canonicalFile)) {
+ return i
+ }
+ }
+ }
+ return -1
+ }
+
+ fun getUrlIfLocalFile(entry: MappingEntry): Url? = canonicalizedUrls.getOrNull(entry.source)?.let { if (it.isInLocalFileSystem) it else null }
+}
+
+fun canonicalizePath(url: String, baseUrl: Url, baseUrlIsFile: Boolean): String {
+ var path = url
+ if (!FileUtil.isAbsolute(url) && !url.isEmpty() && url[0] != '/') {
+ val basePath = baseUrl.path
+ if (baseUrlIsFile) {
+ val lastSlashIndex = basePath.lastIndexOf('/')
+ val pathBuilder = StringBuilder()
+ if (lastSlashIndex == -1) {
+ pathBuilder.append('/')
+ }
+ else {
+ pathBuilder.append(basePath, 0, lastSlashIndex + 1)
+ }
+ path = pathBuilder.append(url).toString()
+ }
+ else {
+ path = "$basePath/$url"
+ }
+ }
+ return FileUtil.toCanonicalPath(path, '/')
+}
+
+// see canonicalizeUri kotlin impl and https://trac.webkit.org/browser/trunk/Source/WebCore/inspector/front-end/ParsedURL.js completeURL
+fun canonicalizeUrl(url: String, baseUrl: Url?, trimFileScheme: Boolean, baseUrlIsFile: Boolean = true): Url {
+ if (trimFileScheme && url.startsWith(StandardFileSystems.FILE_PROTOCOL_PREFIX)) {
+ return Urls.newLocalFileUrl(FileUtil.toCanonicalPath(VfsUtilCore.toIdeaUrl(url, true).substring(StandardFileSystems.FILE_PROTOCOL_PREFIX.length), '/'))
+ }
+ else if (baseUrl == null || url.contains(URLUtil.SCHEME_SEPARATOR) || url.startsWith("data:") || url.startsWith("blob:") ||
+ url.startsWith("javascript:") || url.startsWith("webpack:")) {
+ // consider checking :/ instead of :// because scheme may be followed by path, not by authority
+ // https://tools.ietf.org/html/rfc3986#section-1.1.2
+ // be careful with windows paths: C:/Users
+ return Urls.parseEncoded(url) ?: Urls.newUri(null, url)
+ }
+ else {
+ return doCanonicalize(url, baseUrl, baseUrlIsFile, true)
+ }
+}
+
+fun doCanonicalize(url: String, baseUrl: Url, baseUrlIsFile: Boolean, asLocalFileIfAbsoluteAndExists: Boolean): Url {
+ val path = canonicalizePath(url, baseUrl, baseUrlIsFile)
+ if ((baseUrl.scheme == null && baseUrl.isInLocalFileSystem) ||
+ asLocalFileIfAbsoluteAndExists && SourceResolver.isAbsolute(path) && File(path).exists()) {
+ // file:///home/user/foo.js.map, foo.ts -> /home/user/foo.ts (baseUrl is in local fs)
+ // http://localhost/home/user/foo.js.map, foo.ts -> /home/user/foo.ts (File(path) exists)
+ return Urls.newLocalFileUrl(path)
+ }
+ else if (!path.startsWith("/")) {
+ // http://localhost/source.js.map, C:/foo.ts webpack-dsj3c45 -> C:/foo.ts webpack-dsj3c45
+ // (we can't append path suffixes unless they start with /
+ return Urls.parse(path, true) ?: Urls.newUnparsable(path)
+ }
+ else {
+ // new url from path and baseUrl's scheme and authority
+ val split = path.split('?', limit = 2)
+ return Urls.newUrl(baseUrl.scheme!!, baseUrl.authority!!, split[0], if (split.size > 1) '?' + split[1] else null)
+ }
+} \ No newline at end of file