summaryrefslogtreecommitdiff
path: root/library/src/main/scala/scala/scalajs/runtime/StackTrace.scala
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/main/scala/scala/scalajs/runtime/StackTrace.scala')
-rw-r--r--library/src/main/scala/scala/scalajs/runtime/StackTrace.scala507
1 files changed, 507 insertions, 0 deletions
diff --git a/library/src/main/scala/scala/scalajs/runtime/StackTrace.scala b/library/src/main/scala/scala/scalajs/runtime/StackTrace.scala
new file mode 100644
index 0000000..a9e2c00
--- /dev/null
+++ b/library/src/main/scala/scala/scalajs/runtime/StackTrace.scala
@@ -0,0 +1,507 @@
+package scala.scalajs.runtime
+
+import scala.annotation.tailrec
+
+import scala.scalajs.js
+import scala.scalajs.js.prim.{String => jsString}
+
+/** Conversions of JavaScript stack traces to Java stack traces.
+ */
+object StackTrace {
+
+ /* !!! Note that in this unit, we go to great lengths *not* to use anything
+ * from the Scala collections library.
+ *
+ * This minimizes the risk of runtime errors during the process of decoding
+ * errors, which would be very bad if it happened.
+ */
+
+ /** Captures browser-specific state recording the current stack trace.
+ * The state is stored as a magic field of the throwable, and will be used
+ * by `extract()` to create an Array[StackTraceElement].
+ */
+ def captureState(throwable: Throwable): Unit = {
+ captureState(throwable, createException())
+ }
+
+ /** Creates a JS Error with the current stack trace state. */
+ private def createException(): Any = {
+ try {
+ this.asInstanceOf[js.Dynamic].undef() // it does not exist, that's the point
+ } catch {
+ case js.JavaScriptException(e) => e
+ }
+ }
+
+ /** Captures browser-specific state recording the stack trace of a JS error.
+ * The state is stored as a magic field of the throwable, and will be used
+ * by `extract()` to create an Array[StackTraceElement].
+ */
+ def captureState(throwable: Throwable, e: Any): Unit = {
+ throwable.asInstanceOf[js.Dynamic].stackdata = e.asInstanceOf[js.Any]
+ }
+
+ /** Tests whether we're running under Rhino. */
+ private lazy val isRhino: Boolean = {
+ try {
+ js.Dynamic.global.Packages.org.mozilla.javascript.JavaScriptException
+ true
+ } catch {
+ case js.JavaScriptException(_) => false
+ }
+ }
+
+ /** Extracts a throwable's stack trace from captured browser-specific state.
+ * If no stack trace state has been recorded, or if the state cannot be
+ * analyzed in meaningful way (because we don't know the browser), an
+ * empty array is returned.
+ */
+ def extract(throwable: Throwable): Array[StackTraceElement] =
+ extract(throwable.asInstanceOf[js.Dynamic].stackdata)
+
+ /** Extracts a stack trace from captured browser-specific stackdata.
+ * If no stack trace state has been recorded, or if the state cannot be
+ * analyzed in meaningful way (because we don't know the browser), an
+ * empty array is returned.
+ */
+ def extract(stackdata: js.Dynamic): Array[StackTraceElement] = {
+ val lines = normalizeStackTraceLines(stackdata)
+ normalizedLinesToStackTrace(lines)
+ }
+
+ /* Converts an array of frame entries in normalized form to a stack trace.
+ * Each line must have either the format
+ * <functionName>@<fileName>:<lineNumber>:<columnNumber>
+ * or
+ * <functionName>@<fileName>:<lineNumber>
+ * For some reason, on some browsers, we sometimes have empty lines too.
+ * In the rest of the function, we convert the non-empty lines into
+ * StackTraceElements.
+ */
+ private def normalizedLinesToStackTrace(
+ lines: js.Array[jsString]): Array[StackTraceElement] = {
+ val NormalizedFrameLine = """^([^\@]*)\@(.*):([0-9]+)$""".re
+ val NormalizedFrameLineWithColumn = """^([^\@]*)\@(.*):([0-9]+):([0-9]+)$""".re
+
+ val trace = new js.Array[JSStackTraceElem]
+ var i = 0
+ while (i < lines.length) {
+ val line = lines(i)
+ if (!line.isEmpty) {
+ val mtch1 = NormalizedFrameLineWithColumn.exec(line)
+ if (mtch1 ne null) {
+ val (className, methodName) = extractClassMethod(mtch1(1).get)
+ trace.push(JSStackTraceElem(className, methodName, mtch1(2).get,
+ mtch1(3).get.toInt, mtch1(4).get.toInt))
+ } else {
+ val mtch2 = NormalizedFrameLine.exec(line)
+ if (mtch2 ne null) {
+ val (className, methodName) = extractClassMethod(mtch2(1).get)
+ trace.push(JSStackTraceElem(className,
+ methodName, mtch2(2).get, mtch2(3).get.toInt))
+ } else {
+ // just in case
+ trace.push(JSStackTraceElem("<jscode>", line, null, -1))
+ }
+ }
+ }
+ i += 1
+ }
+
+ // Map stack trace through environment (if supported)
+ val envInfo = environmentInfo
+ val hasMapper = envInfo != js.undefined && envInfo != null &&
+ js.typeOf(envInfo.sourceMapper) == "function"
+
+ val mappedTrace =
+ if (hasMapper)
+ envInfo.sourceMapper(trace).asInstanceOf[js.Array[JSStackTraceElem]]
+ else
+ trace
+
+ // Convert JS objects to java.lang.StackTraceElements
+ // While loop due to space concerns
+ val result = new Array[StackTraceElement](mappedTrace.length)
+
+ i = 0
+ while (i < mappedTrace.length) {
+ val jsSte = mappedTrace(i)
+ val ste = new StackTraceElement(jsSte.declaringClass, jsSte.methodName,
+ jsSte.fileName, jsSte.lineNumber)
+
+ jsSte.columnNumber foreach { cn =>
+ // Store column in magic field
+ ste.asInstanceOf[js.Dynamic].columnNumber = cn
+ }
+
+ result(i) = ste
+ i += 1
+ }
+
+ result
+ }
+
+ /** Tries and extract the class name and method from the JS function name.
+ * The recognized patterns are
+ * ScalaJS.c.<encoded class name>.prototype.<encoded method name>
+ * ScalaJS.c.<encoded class name>.<encoded method name>
+ * ScalaJS.i.<encoded trait impl name>__<encoded method name>
+ * ScalaJS.m.<encoded module name>
+ * When the function name is none of those, the pair
+ * ("<jscode>", functionName)
+ * is returned, which will instruct StackTraceElement.toString() to only
+ * display the function name.
+ */
+ private def extractClassMethod(functionName: String): (String, String) = {
+ val PatC = """^ScalaJS\.c\.([^\.]+)(?:\.prototype)?\.([^\.]+)$""".re
+ val PatI = """^(?:Object\.)?ScalaJS\.i\.((?:_[^_]|[^_])+)__([^\.]+)$""".re
+ val PatM = """^(?:Object\.)?ScalaJS\.m\.([^.\.]+)$""".re
+
+ var isModule = false
+ var mtch = PatC.exec(functionName)
+ if (mtch eq null) {
+ mtch = PatI.exec(functionName)
+ if (mtch eq null) {
+ mtch = PatM.exec(functionName)
+ isModule = true
+ }
+ }
+
+ if (mtch ne null) {
+ val className = decodeClassName(mtch(1).get + (if (isModule) "$" else ""))
+ val methodName = if (isModule)
+ "<clinit>" // that's how it would be reported on the JVM
+ else
+ decodeMethodName(mtch(2).get)
+ (className, methodName)
+ } else {
+ ("<jscode>", functionName)
+ }
+ }
+
+ // decodeClassName -----------------------------------------------------------
+
+ // !!! Duplicate logic: this code must be in sync with ir.Definitions
+
+ private def decodeClassName(encodedName: String): String = {
+ val encoded =
+ if (encodedName.charAt(0) == '$') encodedName.substring(1)
+ else encodedName
+ val base = if (decompressedClasses.hasOwnProperty(encoded)) {
+ decompressedClasses(encoded)
+ } else {
+ @tailrec
+ def loop(i: Int): String = {
+ if (i < compressedPrefixes.length) {
+ val prefix = compressedPrefixes(i)
+ if (encoded.startsWith(prefix))
+ decompressedPrefixes(prefix) + encoded.substring(prefix.length)
+ else
+ loop(i+1)
+ } else {
+ // no prefix matches
+ if (encoded.startsWith("L")) encoded.substring(1)
+ else encoded // just in case
+ }
+ }
+ loop(0)
+ }
+ base.replace("_", ".").replace("$und", "_")
+ }
+
+ private val decompressedClasses: js.Dictionary[String] = {
+ val dict = js.Dynamic.literal(
+ O = "java_lang_Object",
+ T = "java_lang_String",
+ V = "scala_Unit",
+ Z = "scala_Boolean",
+ C = "scala_Char",
+ B = "scala_Byte",
+ S = "scala_Short",
+ I = "scala_Int",
+ J = "scala_Long",
+ F = "scala_Float",
+ D = "scala_Double"
+ ).asInstanceOf[js.Dictionary[String]]
+
+ var index = 0
+ while (index <= 22) {
+ if (index >= 2)
+ dict("T"+index) = "scala_Tuple"+index
+ dict("F"+index) = "scala_Function"+index
+ index += 1
+ }
+
+ dict
+ }
+
+ private val decompressedPrefixes = js.Dynamic.literal(
+ sjsr_ = "scala_scalajs_runtime_",
+ sjs_ = "scala_scalajs_",
+ sci_ = "scala_collection_immutable_",
+ scm_ = "scala_collection_mutable_",
+ scg_ = "scala_collection_generic_",
+ sc_ = "scala_collection_",
+ sr_ = "scala_runtime_",
+ s_ = "scala_",
+ jl_ = "java_lang_",
+ ju_ = "java_util_"
+ ).asInstanceOf[js.Dictionary[String]]
+
+ private val compressedPrefixes = js.Object.keys(decompressedPrefixes)
+
+ // end of decodeClassName ----------------------------------------------------
+
+ private def decodeMethodName(encodedName: String): String = {
+ if (encodedName startsWith "init___") {
+ "<init>"
+ } else {
+ val methodNameLen = encodedName.indexOf("__")
+ if (methodNameLen < 0) encodedName
+ else encodedName.substring(0, methodNameLen)
+ }
+ }
+
+ private implicit class StringRE(val s: String) extends AnyVal {
+ def re: js.RegExp = new js.RegExp(s)
+ def re(mods: String): js.RegExp = new js.RegExp(s, mods)
+ }
+
+ /* ---------------------------------------------------------------------------
+ * Start copy-paste-translate from stacktrace.js
+ *
+ * From here on, most of the code has been copied from
+ * https://github.com/stacktracejs/stacktrace.js
+ * and translated to Scala.js almost literally, with some adaptations.
+ *
+ * Most comments -and lack thereof- have also been copied therefrom.
+ */
+
+ private def normalizeStackTraceLines(e: js.Dynamic): js.Array[jsString] = {
+ /* You would think that we could test once and for all which "mode" to
+ * adopt. But the format can actually differ for different exceptions
+ * on some browsers, e.g., exceptions in Chrome there may or may not have
+ * arguments or stack.
+ */
+ if (!e) {
+ js.Array[jsString]()
+ } else if (isRhino) {
+ extractRhino(e)
+ } else if (!(!e.arguments) && !(!e.stack)) {
+ extractChrome(e)
+ } else if (!(!e.stack) && !(!e.sourceURL)) {
+ extractSafari(e)
+ } else if (!(!e.stack) && !(!e.number)) {
+ extractIE(e)
+ } else if (!(!e.stack) && !(!e.fileName)) {
+ extractFirefox(e)
+ } else if (!(!e.message) && !(!e.`opera#sourceloc`)) {
+ // e.message.indexOf("Backtrace:") > -1 -> opera9
+ // 'opera#sourceloc' in e -> opera9, opera10a
+ // !e.stacktrace -> opera9
+ if (!e.stacktrace) {
+ extractOpera9(e) // use e.message
+ } else if ((e.message.indexOf("\n") > -1) &&
+ (e.message.split("\n").length > e.stacktrace.split("\n").length)) {
+ // e.message may have more stack entries than e.stacktrace
+ extractOpera9(e) // use e.message
+ } else {
+ extractOpera10a(e) // use e.stacktrace
+ }
+ } else if (!(!e.message) && !(!e.stack) && !(!e.stacktrace)) {
+ // e.stacktrace && e.stack -> opera10b
+ if (e.stacktrace.indexOf("called from line") < 0) {
+ extractOpera10b(e)
+ } else {
+ extractOpera11(e)
+ }
+ } else if (!(!e.stack) && !e.fileName) {
+ /* Chrome 27 does not have e.arguments as earlier versions,
+ * but still does not have e.fileName as Firefox */
+ extractChrome(e)
+ } else {
+ extractOther(e)
+ }
+ }
+
+ private def extractRhino(e: js.Dynamic): js.Array[jsString] = {
+ (e.stack.asInstanceOf[js.UndefOr[jsString]]).getOrElse[jsString]("")
+ .replace("""^\s+at\s+""".re("gm"), "") // remove 'at' and indentation
+ .replace("""^(.+?)(?: \((.+)\))?$""".re("gm"), "$2@$1")
+ .replace("""\r\n?""".re("gm"), "\n") // Rhino has platform-dependent EOL's
+ .split("\n")
+ }
+
+ private def extractChrome(e: js.Dynamic): js.Array[jsString] = {
+ (e.stack.asInstanceOf[jsString] + "\n")
+ .replace("""^[\s\S]+?\s+at\s+""".re, " at ") // remove message
+ .replace("""^\s+(at eval )?at\s+""".re("gm"), "") // remove 'at' and indentation
+ .replace("""^([^\(]+?)([\n])""".re("gm"), "{anonymous}() ($1)$2") // see note
+ .replace("""^Object.<anonymous>\s*\(([^\)]+)\)""".re("gm"), "{anonymous}() ($1)")
+ .replace("""^([^\(]+|\{anonymous\}\(\)) \((.+)\)$""".re("gm"), "$1@$2")
+ .split("\n")
+ .jsSlice(0, -1)
+
+ /* Note: there was a $ next to the \n here in the original code, but it
+ * chokes with method names with $'s, which are generated often by Scala.js.
+ */
+ }
+
+ private def extractFirefox(e: js.Dynamic): js.Array[jsString] = {
+ (e.stack.asInstanceOf[jsString])
+ .replace("""(?:\n@:0)?\s+$""".re("m"), "")
+ .replace("""^(?:\((\S*)\))?@""".re("gm"), "{anonymous}($1)@")
+ .split("\n")
+ }
+
+ private def extractIE(e: js.Dynamic): js.Array[jsString] = {
+ (e.stack.asInstanceOf[jsString])
+ .replace("""^\s*at\s+(.*)$""".re("gm"), "$1")
+ .replace("""^Anonymous function\s+""".re("gm"), "{anonymous}() ")
+ .replace("""^([^\(]+|\{anonymous\}\(\))\s+\((.+)\)$""".re("gm"), "$1@$2")
+ .split("\n")
+ .jsSlice(1)
+ }
+
+ private def extractSafari(e: js.Dynamic): js.Array[jsString] = {
+ (e.stack.asInstanceOf[jsString])
+ .replace("""\[native code\]\n""".re("m"), "")
+ .replace("""^(?=\w+Error\:).*$\n""".re("m"), "")
+ .replace("""^@""".re("gm"), "{anonymous}()@")
+ .split("\n")
+ }
+
+ private def extractOpera9(e: js.Dynamic): js.Array[jsString] = {
+ // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n"
+ // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n"
+ val lineRE = """Line (\d+).*script (?:in )?(\S+)""".re("i")
+ val lines = (e.message.asInstanceOf[jsString]).split("\n")
+ val result = new js.Array[jsString]
+
+ var i = 2
+ val len = lines.length.toInt
+ while (i < len) {
+ val mtch = lineRE.exec(lines(i))
+ if (mtch ne null) {
+ result.push("{anonymous}()@" + mtch(2).get + ":" + mtch(1).get
+ /* + " -- " + lines(i+1).replace("""^\s+""".re, "") */)
+ }
+ i += 2
+ }
+
+ result
+ }
+
+ private def extractOpera10a(e: js.Dynamic): js.Array[jsString] = {
+ // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n"
+ // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n"
+ val lineRE = """Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$""".re("i")
+ val lines = (e.stacktrace.asInstanceOf[jsString]).split("\n")
+ val result = new js.Array[jsString]
+
+ var i = 0
+ val len = lines.length.toInt
+ while (i < len) {
+ val mtch = lineRE.exec(lines(i))
+ if (mtch ne null) {
+ val fnName = mtch(3).getOrElse("{anonymous}")
+ result.push(fnName + "()@" + mtch(2).get + ":" + mtch(1).get
+ /* + " -- " + lines(i+1).replace("""^\s+""".re, "")*/)
+ }
+ i += 2
+ }
+
+ result
+ }
+
+ private def extractOpera10b(e: js.Dynamic): js.Array[jsString] = {
+ // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" +
+ // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" +
+ // "@file://localhost/G:/js/test/functional/testcase1.html:15"
+ val lineRE = """^(.*)@(.+):(\d+)$""".re
+ val lines = (e.stacktrace.asInstanceOf[jsString]).split("\n")
+ val result = new js.Array[jsString]
+
+ var i = 0
+ val len = lines.length.toInt
+ while (i < len) {
+ val mtch = lineRE.exec(lines(i))
+ if (mtch ne null) {
+ val fnName = mtch(1).fold("global code")(_ + "()")
+ result.push(fnName + "@" + mtch(2).get + ":" + mtch(3).get)
+ }
+ i += 1
+ }
+
+ result
+ }
+
+ private def extractOpera11(e: js.Dynamic): js.Array[jsString] = {
+ val lineRE = """^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$""".re
+ val lines = (e.stacktrace.asInstanceOf[jsString]).split("\n")
+ val result = new js.Array[jsString]
+
+ var i = 0
+ val len = lines.length.toInt
+ while (i < len) {
+ val mtch = lineRE.exec(lines(i))
+ if (mtch ne null) {
+ val location = mtch(4).get + ":" + mtch(1).get + ":" + mtch(2).get
+ val fnName0 = mtch(2).getOrElse("global code")
+ val fnName = (fnName0: jsString)
+ .replace("""<anonymous function: (\S+)>""".re, "$1")
+ .replace("""<anonymous function>""".re, "{anonymous}")
+ result.push(fnName + "@" + location
+ /* + " -- " + lines(i+1).replace("""^\s+""".re, "")*/)
+ }
+ i += 2
+ }
+
+ result
+ }
+
+ private def extractOther(e: js.Dynamic): js.Array[jsString] = {
+ js.Array()
+ }
+
+ /* End copy-paste-translate from stacktrace.js
+ * ---------------------------------------------------------------------------
+ */
+
+ trait JSStackTraceElem extends js.Object {
+ var declaringClass: String = js.native
+ var methodName: String = js.native
+ var fileName: String = js.native
+ /** 1-based line number */
+ var lineNumber: Int = js.native
+ /** 1-based optional columnNumber */
+ var columnNumber: js.UndefOr[Int] = js.native
+ }
+
+ object JSStackTraceElem {
+ @inline
+ def apply(declaringClass: String, methodName: String,
+ fileName: String, lineNumber: Int,
+ columnNumber: js.UndefOr[Int] = js.undefined): JSStackTraceElem = {
+ js.Dynamic.literal(
+ declaringClass = declaringClass,
+ methodName = methodName,
+ fileName = fileName,
+ lineNumber = lineNumber,
+ columnNumber = columnNumber
+ ).asInstanceOf[JSStackTraceElem]
+ }
+ }
+
+ /**
+ * Implicit class to access magic column element created in STE
+ */
+ implicit class ColumnStackTraceElement(ste: StackTraceElement) {
+ def getColumnNumber: Int = {
+ val num = ste.asInstanceOf[js.Dynamic].columnNumber
+ if (!(!num)) num.asInstanceOf[Int]
+ else -1 // Not very Scala-ish, but consistent with StackTraceElemnt
+ }
+ }
+
+}