nested sourcemaps support for V8 inspector

This commit is contained in:
Vladimir Krivosheev
2016-12-11 17:58:47 +01:00
parent 45b1b86e64
commit 9cfad4bd7f
8 changed files with 254 additions and 93 deletions
@@ -18,17 +18,34 @@ package org.jetbrains.debugger.sourcemap
import com.intellij.openapi.editor.Document
import java.util.*
abstract class MappingList(private val mappings: List<MappingEntry>) {
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>
abstract fun getLine(mapping: MappingEntry): Int
abstract fun getColumn(mapping: MappingEntry): Int
fun indexOf(line: Int, column: Int): Int {
override fun indexOf(line: Int, column: Int): Int {
var low = 0
var high = mappings.size - 1
if (getLine(mappings[low]) > line || getLine(mappings[high]) < line) {
@@ -103,43 +120,37 @@ abstract class MappingList(private val mappings: List<MappingEntry>) {
}
// todo honor Google Chrome bug related to paused location
fun get(line: Int, column: Int) = mappings.getOrNull(indexOf(line, column))
override fun get(line: Int, column: Int) = mappings.getOrNull(indexOf(line, column))
fun getNext(index: Int) = mappings.getOrNull(index + 1)
fun getNext(mapping: MappingEntry) = getNext(mappings.binarySearch(mapping, comparator))
override fun getNext(mapping: MappingEntry) = getNext(mappings.binarySearch(mapping, comparator))
@JvmOverloads
fun getNextOnTheSameLine(_index: Int, skipIfColumnEquals: Boolean = true): MappingEntry? {
var index = _index
override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean): MappingEntry? {
var nextMapping = getNext(index) ?: return null
val mapping = get(index)
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(++index, false) ?: return null
nextMapping = getNextOnTheSameLine(++i, false) ?: return null
}
}
return nextMapping
}
fun getNextOnTheSameLine(mapping: MappingEntry): MappingEntry? {
val nextMapping = getNext(mapping)
return if (nextMapping != null && getLine(nextMapping) == getLine(mapping)) nextMapping else null
}
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)
}
fun get(index: Int) = mappings.get(index)
override fun getByIndex(index: Int) = mappings.get(index)
// entries will be processed in this list order
fun processMappingsInLine(line: Int, entryProcessor: MappingsProcessorInLine): Boolean {
@@ -0,0 +1,116 @@
/*
* Copyright 2000-2016 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) = parentMap.findSourceIndex(sourceFile, localFileUrlOnly)
override fun findSourceIndex(sourceUrls: List<Url>,
sourceFile: VirtualFile?,
resolver: Lazy<SourceFileResolver?>?,
localFileUrlOnly: Boolean) = 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
}
@@ -15,33 +15,66 @@
*/
package org.jetbrains.debugger.sourcemap
import com.intellij.openapi.util.NullableLazyValue
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Url
// sources - is not originally specified, but canonicalized/normalized
class SourceMap(val outFile: String?, val mappings: MappingList, internal val sourceIndexToMappings: Array<MappingList?>, val sourceResolver: SourceResolver, val hasNameMappings: Boolean) {
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) = 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,
internal val sourceIndexToMappings: Array<MappingList?>,
override val sourceResolver: SourceResolver,
override val hasNameMappings: Boolean) : SourceMap {
override val sources: Array<Url>
get() = sourceResolver.canonicalizedUrls
fun getSourceLineByRawLocation(rawLine: Int, rawColumn: Int) = mappings.get(rawLine, rawColumn)?.sourceLine ?: -1
fun findMappingList(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: NullableLazyValue<SourceResolver.Resolver>?): MappingList? {
var mappings = sourceResolver.findMappings(sourceUrls, this, sourceFile)
if (mappings == null && resolver != null) {
mappings = resolver.value?.let { sourceResolver.findMappings(sourceFile, this, it) }
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(sourceFile, it) } ?: -1
}
return mappings
return index
}
fun processMappingsInLine(sourceUrls: List<Url>,
sourceLine: Int,
mappingProcessor: MappingsProcessorInLine,
sourceFile: VirtualFile?,
resolver: NullableLazyValue<SourceResolver.Resolver>?): Boolean {
val mappings = findMappingList(sourceUrls, sourceFile, resolver)
return mappings != null && mappings.processMappingsInLine(sourceLine, mappingProcessor)
}
// returns SourceMappingList
override fun findSourceMappings(sourceIndex: Int) = sourceIndexToMappings.get(sourceIndex)!!
fun getMappingsOrderedBySource(source: Int) = sourceIndexToMappings.get(source)!!
override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean) = sourceResolver.findSourceIndexByFile(sourceFile, localFileUrlOnly)
override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean {
return findSourceMappings(sourceIndex).processMappingsInLine(sourceLine, mappingProcessor)
}
}
@@ -176,7 +176,7 @@ private fun parseMap(reader: JsonReaderEx,
SourceMappingList(entries)
}
}
return SourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty())
return OneLevelSourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty())
}
private fun readSourcePath(reader: JsonReaderEx) = PathUtil.toSystemIndependentName(StringUtil.nullize(reader.nextString().trim { it <= ' ' }))
@@ -269,10 +269,10 @@ private fun readSources(reader: JsonReaderEx, sourceRoot: String?): List<String>
}
private fun getMapping(reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, sourceIndex: Int): MutableList<MappingEntry> {
var reverseMappings = reverseMappingsBySourceUrl[sourceIndex]
var reverseMappings = reverseMappingsBySourceUrl.get(sourceIndex)
if (reverseMappings == null) {
reverseMappings = ArrayList<MappingEntry>()
reverseMappingsBySourceUrl[sourceIndex] = reverseMappings
reverseMappingsBySourceUrl.set(sourceIndex, reverseMappings)
}
return reverseMappings
}
@@ -289,11 +289,7 @@ private fun isSeparator(charIterator: CharSequenceIterator): Boolean {
/**
* Not mapped to a section in the original source.
*/
private open class UnmappedEntry(line: Int, column: Int) : MappingEntry {
override val generatedColumn = column
override val generatedLine = line
private data class UnmappedEntry(override val generatedLine: Int, override val generatedColumn: Int) : MappingEntry {
override val sourceLine = UNMAPPED
override val sourceColumn = UNMAPPED
@@ -302,23 +298,30 @@ private open class UnmappedEntry(line: Int, column: Int) : MappingEntry {
/**
* Mapped to a section in the original source.
*/
private open class UnnamedEntry(line: Int, column: Int, override val source: Int, override val sourceLine: Int, override val sourceColumn: Int) : UnmappedEntry(line, column) {
}
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
/**
* Mapped to a section in the original source, and is associated with a name.
*/
private class NamedEntry(override val name: String, line: Int, column: Int, source: Int, sourceLine: Int, sourceColumn: Int) : UnnamedEntry(line, column, source, sourceLine, sourceColumn) {
}
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
// 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[current++]
override fun next() = content.get(current++)
internal fun peek() = content[current]
internal fun peek() = content.get(current)
override fun hasNext() = current < length
}
@@ -40,7 +40,15 @@ fun SourceResolver(rawSources: List<String>,
return SourceResolver(rawSources, sourceContents) { canonicalizeUrl(it, baseUrl, trimFileScheme, baseUrlIsFile) }
}
interface SourceFileResolver {
fun resolve(sourceFile: VirtualFile?, map: ObjectIntHashMap<Url>): Int
}
class SourceResolver(private val rawSources: List<String>, internal val canonicalizedUrls: Array<Url>, private val sourceContents: List<String>?) {
companion object {
fun isAbsolute(path: String) = path.startsWith('/') || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':'))
}
private val canonicalizedUrlToSourceIndex: ObjectIntHashMap<Url> = if (SystemInfo.isFileSystemCaseSensitive) ObjectIntHashMap(rawSources.size) else ObjectIntHashMap(rawSources.size, Urls.getCaseInsensitiveUrlHashingStrategy())
init {
@@ -49,10 +57,6 @@ class SourceResolver(private val rawSources: List<String>, internal val canonica
}
}
interface Resolver {
fun resolve(sourceFile: VirtualFile?, map: ObjectIntHashMap<Url>): Int
}
fun getSource(entry: MappingEntry): Url? {
val index = entry.source
return if (index < 0) null else canonicalizedUrls[index]
@@ -81,59 +85,54 @@ class SourceResolver(private val rawSources: List<String>, internal val canonica
return if (index < 0) null else rawSources[index]
}
fun findMappings(sourceFile: VirtualFile?, sourceMap: SourceMap, resolver: Resolver): MappingList? {
val index = resolver.resolve(sourceFile, canonicalizedUrlToSourceIndex)
return if (index < 0) null else sourceMap.sourceIndexToMappings[index]
}
internal fun findSourceIndex(sourceFile: VirtualFile?, resolver: SourceFileResolver) = resolver.resolve(sourceFile, canonicalizedUrlToSourceIndex)
fun findMappings(sourceUrls: List<Url>, sourceMap: SourceMap, sourceFile: VirtualFile?): MappingList? {
fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, localFileUrlOnly: Boolean): Int {
for (sourceUrl in sourceUrls) {
val index = canonicalizedUrlToSourceIndex.get(sourceUrl)
if (index != -1) {
return sourceMap.sourceIndexToMappings[index]
return index
}
}
if (sourceFile != null) {
findByFile(sourceMap, sourceFile)?.let {
return it
}
return findSourceIndexByFile(sourceFile, localFileUrlOnly)
}
return null
return -1
}
fun findByFile(sourceMap: SourceMap, sourceFile: VirtualFile): MappingList? {
var index = canonicalizedUrlToSourceIndex.get(Urls.newFromVirtualFile(sourceFile).trimParameters())
if (index != -1) {
return sourceMap.sourceIndexToMappings[index]
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) {
// local file url - without "file" scheme, just path
index = canonicalizedUrlToSourceIndex.get(Urls.newLocalFileUrl(sourceFile))
if (index != -1) {
return sourceMap.sourceIndexToMappings[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[i]
val url = canonicalizedUrls.get(i)
if (Urls.equalsIgnoreParameters(url, canonicalFile)) {
return sourceMap.sourceIndexToMappings[i]
return i
}
}
}
return null
return -1
}
fun getUrlIfLocalFile(entry: MappingEntry) = canonicalizedUrls.getOrNull(entry.source)?.let { if (it.isInLocalFileSystem) it else null }
companion object {
fun isAbsolute(path: String) = path.firstOrNull() == '/' || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':'))
}
}
fun canonicalizePath(url: String, baseUrl: Url, baseUrlIsFile: Boolean): String {
@@ -86,13 +86,11 @@ abstract class LineBreakpointManager(internal val debugProcess: DebugProcessImpl
}
else {
vmBreakpoints = IDE_TO_VM_BREAKPOINTS_KEY.get(vm)?.remove(breakpoint) ?: return nullPromise()
if (!vmBreakpoints.isEmpty()) {
for (vmBreakpoint in vmBreakpoints) {
vmToIdeBreakpoints.remove(vmBreakpoint, breakpoint)
if (vmToIdeBreakpoints.containsKey(vmBreakpoint)) {
// we must not remove vm breakpoint - it is used for another ide breakpoints
return nullPromise()
}
for (vmBreakpoint in vmBreakpoints) {
vmToIdeBreakpoints.remove(vmBreakpoint, breakpoint)
if (vmToIdeBreakpoints.containsKey(vmBreakpoint)) {
// we must not remove vm breakpoint - it is used for another ide breakpoints
return nullPromise()
}
}
}
@@ -23,7 +23,7 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNamedElement
import gnu.trove.THashMap
import org.jetbrains.debugger.sourcemap.MappingEntry
import org.jetbrains.debugger.sourcemap.MappingList
import org.jetbrains.debugger.sourcemap.Mappings
import org.jetbrains.debugger.sourcemap.SourceMap
import org.jetbrains.rpc.LOG
@@ -34,7 +34,7 @@ private val OPERATOR_TRIMMER = CharMatcher.INVISIBLE.or(CharMatcher.anyOf(S1))
val NAME_TRIMMER = CharMatcher.INVISIBLE.or(CharMatcher.anyOf(S1 + ".&:"))
// generateVirtualFile only for debug purposes
open class NameMapper(private val document: Document, private val transpiledDocument: Document, private val sourceMappings: MappingList, protected val sourceMap: SourceMap, private val transpiledFile: VirtualFile? = null) {
open class NameMapper(private val document: Document, private val transpiledDocument: Document, private val sourceMappings: Mappings, protected val sourceMap: SourceMap, private val transpiledFile: VirtualFile? = null) {
var rawNameToSource: MutableMap<String, String>? = null
private set
@@ -50,7 +50,7 @@ open class NameMapper(private val document: Document, private val transpiledDocu
return null
}
val sourceEntry = sourceMappings.get(sourceEntryIndex)
val sourceEntry = sourceMappings.getByIndex(sourceEntryIndex)
val next = sourceMappings.getNextOnTheSameLine(sourceEntryIndex, false)
if (next != null && sourceMappings.getColumn(next) == sourceMappings.getColumn(sourceEntry)) {
warnSeveralMapping(identifierOrNamedElement)
@@ -98,7 +98,7 @@ fun warnSeveralMapping(element: PsiElement) {
private fun getGeneratedName(document: Document, sourceMap: SourceMap, sourceEntry: MappingEntry): CharSequence {
val lineStartOffset = document.getLineStartOffset(sourceEntry.generatedLine)
val nextGeneratedMapping = sourceMap.mappings.getNextOnTheSameLine(sourceEntry)
val nextGeneratedMapping = sourceMap.generatedMappings.getNextOnTheSameLine(sourceEntry)
val endOffset: Int
if (nextGeneratedMapping == null) {
endOffset = document.getLineEndOffset(sourceEntry.generatedLine)
@@ -11,6 +11,7 @@ import com.intellij.util.containers.ObjectIntHashMap
abstract class FileUrlMapper {
companion object {
@JvmField
val EP_NAME = ExtensionPointName.create<FileUrlMapper>("com.jetbrains.fileUrlMapper")
}