1/** 2 * Copyright (c) 650 Industries. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8 9import UTFSequence from 'react-native/Libraries/UTFSequence'; 10 11import parseErrorStack from '../modules/parseErrorStack'; 12import stringifySafe from '../modules/stringifySafe'; 13import type { LogBoxLogData } from './LogBoxLog'; 14type ExceptionData = any; 15 16const BABEL_TRANSFORM_ERROR_FORMAT = 17 /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/; 18const BABEL_CODE_FRAME_ERROR_FORMAT = 19 /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u; 20const METRO_ERROR_FORMAT = 21 /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u; 22 23export type ExtendedExceptionData = ExceptionData & { 24 isComponentError: boolean; 25 [key: string]: any; 26}; 27export type Category = string; 28export type CodeFrame = { 29 content: string; 30 location?: { 31 row: number; 32 column: number; 33 [key: string]: any; 34 } | null; 35 fileName: string; 36 37 // TODO: When React switched to using call stack frames, 38 // we gained the ability to use the collapse flag, but 39 // it is not integrated into the LogBox UI. 40 collapse?: boolean; 41}; 42 43export type Message = { 44 content: string; 45 substitutions: { 46 length: number; 47 offset: number; 48 }[]; 49}; 50 51export type ComponentStack = CodeFrame[]; 52 53const SUBSTITUTION = UTFSequence.BOM + '%s'; 54 55export function parseInterpolation(args: readonly any[]): { 56 category: Category; 57 message: Message; 58} { 59 const categoryParts: string[] = []; 60 const contentParts: string[] = []; 61 const substitutionOffsets: { length: number; offset: number }[] = []; 62 63 const remaining = [...args]; 64 if (typeof remaining[0] === 'string') { 65 const formatString = String(remaining.shift()); 66 const formatStringParts = formatString.split('%s'); 67 const substitutionCount = formatStringParts.length - 1; 68 const substitutions = remaining.splice(0, substitutionCount); 69 70 let categoryString = ''; 71 let contentString = ''; 72 73 let substitutionIndex = 0; 74 for (const formatStringPart of formatStringParts) { 75 categoryString += formatStringPart; 76 contentString += formatStringPart; 77 78 if (substitutionIndex < substitutionCount) { 79 if (substitutionIndex < substitutions.length) { 80 // Don't stringify a string type. 81 // It adds quotation mark wrappers around the string, 82 // which causes the LogBox to look odd. 83 const substitution = 84 typeof substitutions[substitutionIndex] === 'string' 85 ? substitutions[substitutionIndex] 86 : stringifySafe(substitutions[substitutionIndex]); 87 substitutionOffsets.push({ 88 length: substitution.length, 89 offset: contentString.length, 90 }); 91 92 categoryString += SUBSTITUTION; 93 contentString += substitution; 94 } else { 95 substitutionOffsets.push({ 96 length: 2, 97 offset: contentString.length, 98 }); 99 100 categoryString += '%s'; 101 contentString += '%s'; 102 } 103 104 substitutionIndex++; 105 } 106 } 107 108 categoryParts.push(categoryString); 109 contentParts.push(contentString); 110 } 111 112 const remainingArgs = remaining.map((arg) => { 113 // Don't stringify a string type. 114 // It adds quotation mark wrappers around the string, 115 // which causes the LogBox to look odd. 116 return typeof arg === 'string' ? arg : stringifySafe(arg); 117 }); 118 categoryParts.push(...remainingArgs); 119 contentParts.push(...remainingArgs); 120 121 return { 122 category: categoryParts.join(' '), 123 message: { 124 content: contentParts.join(' '), 125 substitutions: substitutionOffsets, 126 }, 127 }; 128} 129 130function isComponentStack(consoleArgument: string) { 131 const isOldComponentStackFormat = / {4}in/.test(consoleArgument); 132 const isNewComponentStackFormat = / {4}at/.test(consoleArgument); 133 const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument); 134 135 return isOldComponentStackFormat || isNewComponentStackFormat || isNewJSCComponentStackFormat; 136} 137 138export function parseComponentStack(message: string): ComponentStack { 139 // In newer versions of React, the component stack is formatted as a call stack frame. 140 // First try to parse the component stack as a call stack frame, and if that doesn't 141 // work then we'll fallback to the old custom component stack format parsing. 142 const stack = parseErrorStack(message); 143 if (stack && stack.length > 0) { 144 return stack.map((frame) => ({ 145 content: frame.methodName, 146 collapse: frame.collapse || false, 147 fileName: frame.file == null ? 'unknown' : frame.file, 148 location: { 149 column: frame.column == null ? -1 : frame.column, 150 row: frame.lineNumber == null ? -1 : frame.lineNumber, 151 }, 152 })); 153 } 154 155 return message 156 .split(/\n {4}in /g) 157 .map((s) => { 158 if (!s) { 159 return null; 160 } 161 const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/); 162 if (!match) { 163 return null; 164 } 165 166 const [content, fileName, row] = match.slice(1); 167 return { 168 content, 169 fileName, 170 location: { column: -1, row: parseInt(row, 10) }, 171 }; 172 }) 173 .filter(Boolean) as ComponentStack; 174} 175 176export function parseLogBoxException(error: ExtendedExceptionData): LogBoxLogData { 177 const message = error.originalMessage != null ? error.originalMessage : 'Unknown'; 178 179 const metroInternalError = message.match(METRO_ERROR_FORMAT); 180 if (metroInternalError) { 181 const [content, fileName, row, column, codeFrame] = metroInternalError.slice(1); 182 183 return { 184 level: 'fatal', 185 type: 'Metro Error', 186 stack: [], 187 isComponentError: false, 188 componentStack: [], 189 codeFrame: { 190 fileName, 191 location: { 192 row: parseInt(row, 10), 193 column: parseInt(column, 10), 194 }, 195 content: codeFrame, 196 }, 197 message: { 198 content, 199 substitutions: [], 200 }, 201 category: `${fileName}-${row}-${column}`, 202 }; 203 } 204 205 const babelTransformError = message.match(BABEL_TRANSFORM_ERROR_FORMAT); 206 if (babelTransformError) { 207 // Transform errors are thrown from inside the Babel transformer. 208 const [fileName, content, row, column, codeFrame] = babelTransformError.slice(1); 209 210 return { 211 level: 'syntax', 212 stack: [], 213 isComponentError: false, 214 componentStack: [], 215 codeFrame: { 216 fileName, 217 location: { 218 row: parseInt(row, 10), 219 column: parseInt(column, 10), 220 }, 221 content: codeFrame, 222 }, 223 message: { 224 content, 225 substitutions: [], 226 }, 227 category: `${fileName}-${row}-${column}`, 228 }; 229 } 230 231 const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); 232 233 if (babelCodeFrameError) { 234 // Codeframe errors are thrown from any use of buildCodeFrameError. 235 const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); 236 return { 237 level: 'syntax', 238 stack: [], 239 isComponentError: false, 240 componentStack: [], 241 codeFrame: { 242 fileName, 243 location: null, // We are not given the location. 244 content: codeFrame, 245 }, 246 message: { 247 content, 248 substitutions: [], 249 }, 250 category: `${fileName}-${1}-${1}`, 251 }; 252 } 253 254 if (message.match(/^TransformError /)) { 255 return { 256 level: 'syntax', 257 stack: error.stack, 258 isComponentError: error.isComponentError, 259 componentStack: [], 260 message: { 261 content: message, 262 substitutions: [], 263 }, 264 category: message, 265 }; 266 } 267 268 const componentStack = error.componentStack; 269 if (error.isFatal || error.isComponentError) { 270 return { 271 level: 'fatal', 272 stack: error.stack, 273 isComponentError: error.isComponentError, 274 componentStack: componentStack != null ? parseComponentStack(componentStack) : [], 275 ...parseInterpolation([message]), 276 }; 277 } 278 279 if (componentStack != null) { 280 // It is possible that console errors have a componentStack. 281 return { 282 level: 'error', 283 stack: error.stack, 284 isComponentError: error.isComponentError, 285 componentStack: parseComponentStack(componentStack), 286 ...parseInterpolation([message]), 287 }; 288 } 289 290 // Most `console.error` calls won't have a componentStack. We parse them like 291 // regular logs which have the component stack burried in the message. 292 return { 293 level: 'error', 294 stack: error.stack, 295 isComponentError: error.isComponentError, 296 ...parseLogBoxLog([message]), 297 }; 298} 299 300export function parseLogBoxLog(args: readonly any[]): { 301 componentStack: ComponentStack; 302 category: Category; 303 message: Message; 304} { 305 const message = args[0]; 306 let argsWithoutComponentStack: any[] = []; 307 let componentStack: ComponentStack = []; 308 309 // Extract component stack from warnings like "Some warning%s". 310 if (typeof message === 'string' && message.slice(-2) === '%s' && args.length > 0) { 311 const lastArg = args[args.length - 1]; 312 if (typeof lastArg === 'string' && isComponentStack(lastArg)) { 313 argsWithoutComponentStack = args.slice(0, -1); 314 argsWithoutComponentStack[0] = message.slice(0, -2); 315 componentStack = parseComponentStack(lastArg); 316 } 317 } 318 319 if (componentStack.length === 0) { 320 // Try finding the component stack elsewhere. 321 for (const arg of args) { 322 if (typeof arg === 'string' && isComponentStack(arg)) { 323 // Strip out any messages before the component stack. 324 let messageEndIndex = arg.search(/\n {4}(in|at) /); 325 if (messageEndIndex < 0) { 326 // Handle JSC component stacks. 327 messageEndIndex = arg.search(/\n/); 328 } 329 if (messageEndIndex > 0) { 330 argsWithoutComponentStack.push(arg.slice(0, messageEndIndex)); 331 } 332 333 componentStack = parseComponentStack(arg); 334 } else { 335 argsWithoutComponentStack.push(arg); 336 } 337 } 338 } 339 340 return { 341 ...parseInterpolation(argsWithoutComponentStack), 342 componentStack, 343 }; 344} 345