|
#!/usr/bin/env kotlin |
|
|
|
@file:Repository("https://maven.atlassian.com/content/repositories/atlassian-public") |
|
@file:DependsOn("com.atlassian.sourcemap:sourcemap:1.7.7") |
|
@file:DependsOn("com.github.ajalt:clikt:2.6.0") |
|
|
|
import com.atlassian.sourcemap.Mapping |
|
import com.atlassian.sourcemap.SourceMap |
|
import com.atlassian.sourcemap.SourceMapImpl |
|
import com.atlassian.sourcemap.Util |
|
import com.github.ajalt.clikt.core.CliktCommand |
|
import com.github.ajalt.clikt.parameters.options.flag |
|
import com.github.ajalt.clikt.parameters.options.option |
|
import com.github.ajalt.clikt.parameters.options.prompt |
|
import com.github.ajalt.clikt.parameters.types.path |
|
import java.io.File |
|
import java.nio.file.Path |
|
|
|
data class ProjectLayout( |
|
val mainModuleName: String, |
|
val sourceMapDirectory: String, |
|
val sourceDirectory: String, |
|
val buildDirectory: String, |
|
val jsDirectory: String |
|
) |
|
|
|
data class StackFrame( |
|
val module: String, |
|
val symbol: String, |
|
val line: Int, |
|
val column: Int |
|
) |
|
|
|
class RunResolver : CliktCommand() { |
|
val name: String by option( |
|
"--name", |
|
"-n", |
|
help = "The name of the file published as main.js in screeps. It is usually the name of the project in settings.gradle" |
|
).prompt() |
|
val projectDirectory: Path by option("--projectDirectory", "-d", help = "Project directory").path(mustExist = true) |
|
.prompt() |
|
val debug: Boolean by option("--debug").flag() |
|
val quiet: Boolean by option("-q", "--quiet", help = "non interactive.").flag(default = false) |
|
val stacktrace: String? by option(help = "stacktrace") |
|
|
|
companion object { |
|
const val SOURCE_MAP = ".js.map" |
|
} |
|
|
|
fun calculatePosition(rawSourceMap: SourceMap, stackFrame: StackFrame): Mapping? { |
|
return calculatePosition(rawSourceMap, stackFrame.line, stackFrame.column) |
|
} |
|
|
|
fun calculatePosition(rawSourceMap: SourceMap, line: Int, column: Int): Mapping? { |
|
return rawSourceMap.getMapping(line, column) |
|
} |
|
|
|
fun resolveSourceFile(source: String, projectLayout: ProjectLayout): String { |
|
if (source.contains("/src")) { |
|
return projectLayout.sourceDirectory + source.substringAfter("/src") |
|
} |
|
|
|
if (source.contains("js/packages/")) { |
|
// TODO it is a library |
|
return source |
|
} |
|
|
|
return source |
|
} |
|
|
|
fun loadSourceMap(path: String?, offset: Int = 1): SourceMap? { |
|
if (path == null) { |
|
return null |
|
} |
|
val file = File(path) |
|
if (!file.exists()) { |
|
return null |
|
} |
|
|
|
val sourceMap = SourceMapImpl(file.readText()) |
|
if (offset == 0) { |
|
return sourceMap |
|
} else { |
|
return Util.offset(sourceMap, offset) |
|
} |
|
} |
|
|
|
fun debug(string: String? = null) { |
|
if (debug) { |
|
println(string) |
|
} |
|
} |
|
|
|
fun resolveModulePath( |
|
moduleName: String, |
|
projectLayout: ProjectLayout |
|
): String? { |
|
val effectiveModuleName = when { |
|
moduleName == "main" -> projectLayout.mainModuleName |
|
moduleName.startsWith("__") -> projectLayout.mainModuleName |
|
moduleName == "<runtime>" -> null |
|
else -> moduleName |
|
} ?: return null |
|
|
|
return projectLayout.sourceMapDirectory + "/" + effectiveModuleName + SOURCE_MAP |
|
} |
|
|
|
fun recoverStacktrace(stacktrace: String, projectLayout: ProjectLayout): String { |
|
|
|
fun parseLine(line: String): StackFrame { |
|
// ... |
|
// at ConstructionOperation.run (main:391:68) |
|
// at __mainLoop:1:52 |
|
// ... |
|
var remaining = line.trim().removePrefix("at ") |
|
val symbolName = if (remaining.first() == '_') "" else remaining.substringBefore(" ") |
|
remaining = remaining.removePrefix(symbolName).trim() |
|
// remaining = "__mainLoop:1:52" or "(main:391:68)" |
|
remaining = remaining.removePrefix("(").removeSuffix(")") |
|
// remaining = "__mainLoop:1:52" or "main:391:68" |
|
val (module, lineNo, columNo) = remaining.split(":") |
|
return StackFrame(module, symbol = symbolName, line = lineNo.toInt(), column = columNo.toInt()) |
|
} |
|
|
|
val sourceMaps: MutableMap<String?, SourceMap?> = mutableMapOf() |
|
|
|
|
|
return stacktrace.lines().joinToString(System.lineSeparator()) { line -> |
|
val canParse = line.trim().startsWith("at ") |
|
if (!canParse) { |
|
return@joinToString line |
|
} |
|
val frame = parseLine(line) |
|
|
|
val path = resolveModulePath(frame.module, projectLayout) |
|
val sourceMap: SourceMap? = sourceMaps.getOrPut(path) { |
|
debug("loading module '${frame.module}' from $path") |
|
loadSourceMap(path) |
|
} |
|
if (sourceMap == null) { |
|
debug("skipping $frame because does there is no source map for the module") |
|
return@joinToString line |
|
} |
|
|
|
var position = calculatePosition(sourceMap, frame) |
|
if (position == null) { |
|
calculatePosition(sourceMap, frame.copy(line = 1, column = 1)) |
|
} |
|
|
|
debug("stackframe=${frame}, position = ${position}, sourcesymbol=${position?.sourceSymbolName}") |
|
|
|
val sourceFile = resolveSourceFile(position?.sourceFileName ?: "", projectLayout) |
|
// needs to be of the form |
|
// <projectDir>/src/test/kotlin/Stacktrace.kt: (<line>, <column>): |
|
val ideaClickableLink = sourceFile + ": (${position?.sourceLine}, ${position?.sourceColumn}):" |
|
line.substringBeforeLast(" (") + " " + ideaClickableLink |
|
} |
|
} |
|
|
|
override fun run() { |
|
val projectLayout = ProjectLayout( |
|
name, |
|
"${projectDirectory}/build/minified-js", |
|
"$projectDirectory/src", |
|
"$projectDirectory/build", |
|
"$projectDirectory/build/js" |
|
) |
|
|
|
debug() |
|
debug("using $projectLayout") |
|
val sourceMapDir = File(projectLayout.sourceMapDirectory) |
|
if (!sourceMapDir.isDirectory || sourceMapDir.list()!!.none { it.endsWith(SOURCE_MAP) }) { |
|
error("no source maps found in $sourceMapDir") |
|
} |
|
|
|
var input = stacktrace |
|
if (input == null) { |
|
if (!quiet) { |
|
println("enter a stacktrace (reading until EOF or empty line):") |
|
} |
|
val buffer = StringBuffer() |
|
do { |
|
val line = readLine()?.also { |
|
buffer.appendln(it) |
|
} |
|
} while (!line.isNullOrBlank()) |
|
input = buffer.toString() |
|
} |
|
|
|
debug("translating stacktrace...") |
|
debug() |
|
|
|
val translation = recoverStacktrace(input.trimIndent(), projectLayout) |
|
println(translation) |
|
} |
|
} |
|
|
|
RunResolver().main(args) |