/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.react.devsupport import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableType import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.interfaces.StackFrame import com.facebook.react.interfaces.exceptionmanager.ReactJsExceptionHandler.ProcessedError import java.io.File import java.util.regex.Pattern import org.json.JSONArray import org.json.JSONException import org.json.JSONObject /** Helper class converting JS and Java stack traces into arrays of [StackFrame] objects. */ @OptIn(UnstableReactNativeAPI::class) public object StackTraceHelper { public const val COLUMN_KEY: String = "column" public const val LINE_NUMBER_KEY: String = "lineNumber" public const val FILE_KEY: String = "file" public const val METHOD_NAME_KEY: String = "methodName" public const val COLLAPSE_KEY: String = "collapse" public const val MESSAGE_KEY: String = "message" public const val ORIGINAL_MESSAGE_KEY: String = "originalMessage" public const val NAME_KEY: String = "name" public const val COMPONENT_STACK_KEY: String = "componentStack" public const val STACK_KEY: String = "stack" public const val ID_KEY: String = "id" public const val IS_FATAL_KEY: String = "isFatal" public const val EXTRA_DATA_KEY: String = "extraData" private val STACK_FRAME_PATTERN1: Pattern = Pattern.compile("^(?:(.*?)@)?(.*?)\\:([0-9]+)\\:([0-9]+)$") private val STACK_FRAME_PATTERN2: Pattern = Pattern.compile("\\s*(?:at)\\s*(.+?)\\s*[@(](.*):([0-9]+):([0-9]+)[)]$") /** Convert a JavaScript stack trace (see `parseErrorStack` JS module) to an array of [ ]s. */ @JvmStatic public fun convertJsStackTrace(stack: ReadableArray?): Array { val size = stack?.size() ?: 0 if (stack == null) { return arrayOf() } return Array(size) { i -> val type = stack.getType(i) if (type == ReadableType.Map) { val frame = requireNotNull(stack.getMap(i)) val method = requireNotNull(frame.getString(METHOD_NAME_KEY)) val file = requireNotNull(frame.getString(FILE_KEY)) val collapse = frame.hasKey(COLLAPSE_KEY) && !frame.isNull(COLLAPSE_KEY) && frame.getBoolean(COLLAPSE_KEY) var lineNumber = -1 if (frame.hasKey(LINE_NUMBER_KEY) && !frame.isNull(LINE_NUMBER_KEY)) { lineNumber = frame.getInt(LINE_NUMBER_KEY) } var columnNumber = -1 if (frame.hasKey(COLUMN_KEY) && !frame.isNull(COLUMN_KEY)) { columnNumber = frame.getInt(COLUMN_KEY) } StackFrameImpl( file = file, method = method, line = lineNumber, column = columnNumber, isCollapsed = collapse, ) } else if (type == ReadableType.String) { StackFrameImpl( file = null, method = checkNotNull(stack.getString(i)), line = -1, column = -1, ) } else { error("Cannot parse the stackframe for $stack") } } } /** * Convert a JavaScript stack trace (see `parseErrorStack` JS module) to an array of [StackFrame]. */ public fun convertJsStackTrace(stack: JSONArray?): Array { if (stack == null) { return arrayOf() } val size = stack.length() try { return Array(size) { i -> val frame = stack.getJSONObject(i) val method = frame.getString(METHOD_NAME_KEY) val file = frame.getString(FILE_KEY) var lineNumber = -1 if (frame.has(LINE_NUMBER_KEY) && !frame.isNull(LINE_NUMBER_KEY)) { lineNumber = frame.getInt(LINE_NUMBER_KEY) } var columnNumber = -1 if (frame.has(COLUMN_KEY) && !frame.isNull(COLUMN_KEY)) { columnNumber = frame.getInt(COLUMN_KEY) } val collapse = frame.has(COLLAPSE_KEY) && !frame.isNull(COLLAPSE_KEY) && frame.getBoolean(COLLAPSE_KEY) StackFrameImpl( file = file, method = method, line = lineNumber, column = columnNumber, isCollapsed = collapse, ) } } catch (exception: JSONException) { throw RuntimeException(exception) } } /** Convert a JavaScript stack trace to an array of [StackFrame]s. */ public fun convertJsStackTrace(stack: String): Array { val stackTrace = stack.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return Array(stackTrace.size) { i -> val matcher1 = STACK_FRAME_PATTERN1.matcher(stackTrace[i]) val matcher2 = STACK_FRAME_PATTERN2.matcher(stackTrace[i]) val matcher = when { matcher2.find() -> matcher2 matcher1.find() -> matcher1 else -> null } if (matcher != null) { val file = matcher.group(2) val method = matcher.group(1) ?: "(unknown)" val lineString = matcher.group(3) val columnString = matcher.group(4) StackFrameImpl( file = file, method = method, line = checkNotNull(lineString).toInt(), column = checkNotNull(columnString).toInt(), ) } else { StackFrameImpl(file = null, method = stackTrace[i], line = -1, column = -1) } } } /** Convert a [Throwable] to an array of [StackFrame]s. */ @JvmStatic public fun convertJavaStackTrace(exception: Throwable): Array { val stackTrace = exception.stackTrace return Array(stackTrace.size) { i -> StackFrameImpl( file = stackTrace[i].className, fileName = stackTrace[i].fileName, method = stackTrace[i].methodName, line = stackTrace[i].lineNumber, column = -1, ) } } /** Format a [StackFrame] to a String (method name is not included). */ public fun formatFrameSource(frame: StackFrame): String { val lineInfo = StringBuilder() lineInfo.append(frame.fileName) val line = frame.line if (line > 0) { lineInfo.append(":").append(line) val column = frame.column if (column > 0) { lineInfo.append(":").append(column) } } return lineInfo.toString() } /** Format an array of [StackFrame]s with the error title to a String. */ @JvmStatic public fun formatStackTrace(title: String?, stack: Array): String { val stackTrace = StringBuilder() stackTrace.append(title).append("\n") for (frame in stack) { stackTrace .append(frame.method) .append("\n") .append(" ") .append(formatFrameSource(frame)) .append("\n") } return stackTrace.toString() } @JvmStatic internal fun convertProcessedError(error: ProcessedError): JavaOnlyMap { val stack = JavaOnlyArray() for (frame in error.stack) { stack.pushMap( JavaOnlyMap().apply { frame.column?.let { putDouble(COLUMN_KEY, it.toDouble()) } frame.lineNumber?.let { putDouble(LINE_NUMBER_KEY, it.toDouble()) } putString(FILE_KEY, frame.file) putString(METHOD_NAME_KEY, frame.methodName) } ) } return JavaOnlyMap().apply { putString(MESSAGE_KEY, error.message) error.originalMessage?.let { putString(ORIGINAL_MESSAGE_KEY, it) } error.name?.let { putString(NAME_KEY, it) } error.componentStack?.let { putString(COMPONENT_STACK_KEY, it) } putArray(STACK_KEY, stack) putInt(ID_KEY, error.id) putBoolean(IS_FATAL_KEY, error.isFatal) putMap(EXTRA_DATA_KEY, error.extraData) } } /** * Represents a generic entry in a stack trace, be it originally from JS or Java. * * @property file the file this stack frame points to. JS traces return the full path to the file * here, while Java traces only return the file name (the path is not known). * @property fileName the name of the file this frame points to. For JS traces this is different * from [getFile] in that it only returns the file name, not the full path. For Java traces * there is no difference. * @property method the name of the method this frame points to. * @property line the line number this frame points to in the file returned by [file] * @property column the column this frame points to in the file returned by [file] */ public class StackFrameImpl( public override val file: String?, public override val fileName: String? = file?.let { File(it).name }, public override val method: String, public override val line: Int, public override val column: Int, public override val isCollapsed: Boolean = false, ) : StackFrame { /** Convert the stack frame to a JSON representation. */ override fun toJSON(): JSONObject = JSONObject( mapOf( FILE_KEY to (file.orEmpty()), METHOD_NAME_KEY to method, LINE_NUMBER_KEY to line, COLUMN_KEY to column, COLLAPSE_KEY to isCollapsed, ) ) } }