All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.scalajs.runtime.StackTrace.scala Maven / Gradle / Ivy

The newest version!
package scala.scalajs.runtime

import scala.annotation.tailrec

import scala.scalajs.js
import js.JSStringOps._

/** 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.
   */

  import Implicits._

  /** Returns the current stack trace.
   *  If the stack trace cannot be analyzed in meaningful way (because we don't
   *  know the browser), an empty array is returned.
   */
  def getCurrentStackTrace(): Array[StackTraceElement] =
    extract(createException().asInstanceOf[js.Dynamic])

  /** 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].
   */
  @inline def captureState(throwable: Throwable): Unit = {
    if (js.isUndefined(js.constructorOf[js.Error].captureStackTrace)) {
      captureState(throwable, createException())
    } else {
      /* V8-specific.
       * The Error.captureStackTrace(e) method records the current stack trace
       * on `e` as would do `new Error()`, thereby turning `e` into a proper
       * exception. This avoids creating a dummy exception, but is mostly
       * important so that Node.js will show stack traces if the exception
       * is never caught and reaches the global event queue.
       */
      js.constructorOf[js.Error].captureStackTrace(throwable.asInstanceOf[js.Any])
      captureState(throwable, throwable)
    }
  }

  /** Creates a JS Error with the current stack trace state. */
  @inline private def createException(): Any = {
    try {
      // Intentionally throw a JavaScript error
      new js.Object().asInstanceOf[js.Dynamic].undef()
    } 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].
   */
  @inline 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
   *   @::
   * or
   *   @:
   * 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[String]): 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("", line, null, -1))
          }
        }
      }
      i += 1
    }

    // Map stack trace through environment (if supported)
    val mappedTrace =
      environmentInfo.sourceMapper.fold(trace)(mapper => mapper(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(ste.setColumnNumber)
      result(i) = ste
      i += 1
    }

    result
  }

  /** Tries and extract the class name and method from the JS function name.
   *
   *  The recognized patterns are
   *  {{{
   *    \$c_.prototype.
   *    \$c_.
   *    \$s___
   *    \$m_
   *  }}}
   *  and their ECMAScript51Global equivalents:
   *  {{{
   *    ScalaJS.c..prototype.
   *    ScalaJS.c..
   *    ScalaJS.s.__
   *    ScalaJS.m.
   *  }}}
   *  all of them optionally prefixed by `Object.` or `[object Object].`.
   *
   *  When the function name is none of those, the pair
   *    `("", functionName)`
   *  is returned, which will instruct [[StackTraceElement.toString()]] to only
   *  display the function name.
   */
  private def extractClassMethod(functionName: String): (String, String) = {
    val PatC = """^(?:Object\.|\[object Object\]\.)?(?:ScalaJS\.c\.|\$c_)([^\.]+)(?:\.prototype)?\.([^\.]+)$""".re
    val PatS = """^(?:Object\.|\[object Object\]\.)?(?:ScalaJS\.(?:s|f)\.|\$(?:s|f)_)((?:_[^_]|[^_])+)__([^\.]+)$""".re
    val PatM = """^(?:Object\.|\[object Object\]\.)?(?:ScalaJS\.m\.|\$m_)([^\.]+)$""".re

    var isModule = false
    var mtch = PatC.exec(functionName)
    if (mtch eq null) {
      mtch = PatS.exec(functionName)
      if (mtch eq null) {
        mtch = PatM.exec(functionName)
        isModule = true
      }
    }

    if (mtch ne null) {
      val className = decodeClassName(mtch(1).get)
      val methodName = if (isModule)
        "" // that's how it would be reported on the JVM
      else
        decodeMethodName(mtch(2).get)
      (className, methodName)
    } else {
      ("", 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.contains(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 lazy 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 lazy 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 lazy val compressedPrefixes =
    js.Object.keys(decompressedPrefixes.asInstanceOf[js.Object])

  // end of decodeClassName ----------------------------------------------------

  private def decodeMethodName(encodedName: String): String = {
    if (encodedName startsWith "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[String] = {
    import js.DynamicImplicits.{truthValue, number2dynamic}

    /* 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.
     */

    /* Ideally we would write tests like
     *   if (e.arguments && e.stack)
     * but Scala 2.10 does not like that (too much Dynamic magic, I suppose).
     * So we define inlineable defs for the fields we're interested in.
     * This way we make the compiler happy
     */
    @inline def arguments = e.arguments
    @inline def stack = e.stack
    @inline def sourceURL = e.sourceURL
    @inline def number = e.number
    @inline def fileName = e.fileName
    @inline def message = e.message
    @inline def `opera#sourceloc` = e.`opera#sourceloc`
    @inline def stacktrace = e.stacktrace

    if (!e) {
      js.Array[String]()
    } else if (isRhino) {
      extractRhino(e)
    } else if (arguments && stack) {
      extractChrome(e)
    } else if (stack && sourceURL) {
      extractSafari(e)
    } else if (stack && number) {
      extractIE(e)
    } else if (stack && fileName) {
      extractFirefox(e)
    } else if (message && `opera#sourceloc`) {
      // e.message.indexOf("Backtrace:") > -1 -> opera9
      // 'opera#sourceloc' in e -> opera9, opera10a
      // !e.stacktrace -> opera9
      @inline def messageIsLongerThanStacktrace =
        message.split("\n").length > stacktrace.split("\n").length
      if (!stacktrace) {
        extractOpera9(e) // use e.message
      } else if ((message.indexOf("\n") > -1) && messageIsLongerThanStacktrace) {
        // e.message may have more stack entries than e.stacktrace
        extractOpera9(e) // use e.message
      } else {
        extractOpera10a(e) // use e.stacktrace
      }
    } else if (message && stack && stacktrace) {
      // stacktrace && stack -> opera10b
      if (stacktrace.indexOf("called from line") < 0) {
        extractOpera10b(e)
      } else {
        extractOpera11(e)
      }
    } else if (stack && !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[String] = {
    (e.stack.asInstanceOf[js.UndefOr[String]]).getOrElse("")
      .jsReplace("""^\s+at\s+""".re("gm"), "") // remove 'at' and indentation
      .jsReplace("""^(.+?)(?: \((.+)\))?$""".re("gm"), "$2@$1")
      .jsReplace("""\r\n?""".re("gm"), "\n") // Rhino has platform-dependent EOL's
      .jsSplit("\n")
  }

  private def extractChrome(e: js.Dynamic): js.Array[String] = {
    (e.stack.asInstanceOf[String] + "\n")
      .jsReplace("""^[\s\S]+?\s+at\s+""".re, " at ") // remove message
      .jsReplace("""^\s+(at eval )?at\s+""".re("gm"), "") // remove 'at' and indentation
      .jsReplace("""^([^\(]+?)([\n])""".re("gm"), "{anonymous}() ($1)$2") // see note
      .jsReplace("""^Object.\s*\(([^\)]+)\)""".re("gm"), "{anonymous}() ($1)")
      .jsReplace("""^([^\(]+|\{anonymous\}\(\)) \((.+)\)$""".re("gm"), "$1@$2")
      .jsSplit("\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[String] = {
    (e.stack.asInstanceOf[String])
      .jsReplace("""(?:\n@:0)?\s+$""".re("m"), "")
      .jsReplace("""^(?:\((\S*)\))?@""".re("gm"), "{anonymous}($1)@")
      .jsSplit("\n")
  }

  private def extractIE(e: js.Dynamic): js.Array[String] = {
    (e.stack.asInstanceOf[String])
      .jsReplace("""^\s*at\s+(.*)$""".re("gm"), "$1")
      .jsReplace("""^Anonymous function\s+""".re("gm"), "{anonymous}() ")
      .jsReplace("""^([^\(]+|\{anonymous\}\(\))\s+\((.+)\)$""".re("gm"), "$1@$2")
      .jsSplit("\n")
      .jsSlice(1)
  }

  private def extractSafari(e: js.Dynamic): js.Array[String] = {
    (e.stack.asInstanceOf[String])
      .jsReplace("""\[native code\]\n""".re("m"), "")
      .jsReplace("""^(?=\w+Error\:).*$\n""".re("m"), "")
      .jsReplace("""^@""".re("gm"), "{anonymous}()@")
      .jsSplit("\n")
  }

  private def extractOpera9(e: js.Dynamic): js.Array[String] = {
    // "  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[String]).jsSplit("\n")
    val result = new js.Array[String]

    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[String] = {
    // "  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[String]).jsSplit("\n")
    val result = new js.Array[String]

    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[String] = {
    // "([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[String]).jsSplit("\n")
    val result = new js.Array[String]

    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[String] = {
    val lineRE = """^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$""".re
    val lines = (e.stacktrace.asInstanceOf[String]).jsSplit("\n")
    val result = new js.Array[String]

    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
          .jsReplace("""""".re, "$1")
          .jsReplace("""""".re, "{anonymous}")
        result.push(fnName + "@" + location
            /* + " -- " + lines(i+1).replace("""^\s+""".re, "")*/)
      }
      i += 2
    }

    result
  }

  private def extractOther(e: js.Dynamic): js.Array[String] = {
    js.Array()
  }

  /* End copy-paste-translate from stacktrace.js
   * ---------------------------------------------------------------------------
   */

  @js.native
  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
   */
  @deprecated("Use Implicits.StackTraceElementOps instead.", "0.6.3")
  implicit class ColumnStackTraceElement(ste: StackTraceElement) {
    def getColumnNumber: Int =
      new Implicits.StackTraceElementOps(ste).getColumnNumber()
  }

  object Implicits {
    /** Access to the additional methods `getColumnNumber` and `setColumnNumber`
     *  of [[java.lang.StackTraceElement StackTraceElement]].
     */
    implicit class StackTraceElementOps(
        val ste: StackTraceElement) extends AnyVal {
      @inline
      def getColumnNumber(): Int =
        ste.asInstanceOf[js.Dynamic].getColumnNumber().asInstanceOf[Int]

      @inline
      def setColumnNumber(columnNumber: Int): Unit =
        ste.asInstanceOf[js.Dynamic].setColumnNumber(columnNumber)
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy