error.mjs (8431B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /* eslint no-shadow: ["error", { "allow": ["name", "location", "frames"] }] */ 6 7 import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs"; 8 import { 9 div, 10 span, 11 } from "resource://devtools/client/shared/vendor/react-dom-factories.mjs"; 12 13 import { wrapRender } from "./rep-utils.mjs"; 14 import { cleanFunctionName } from "./function.mjs"; 15 import { isLongString } from "./string.mjs"; 16 import { MODE } from "./constants.mjs"; 17 18 const IGNORED_SOURCE_URLS = ["debugger eval code"]; 19 20 /** 21 * Renders Error objects. 22 */ 23 ErrorRep.propTypes = { 24 object: PropTypes.object.isRequired, 25 mode: PropTypes.oneOf(Object.values(MODE)), 26 // An optional function that will be used to render the Error stacktrace. 27 renderStacktrace: PropTypes.func, 28 shouldRenderTooltip: PropTypes.bool, 29 }; 30 31 /** 32 * Render an Error object. 33 * The customFormat prop allows to print a simplified view of the object, with only the 34 * message and the stacktrace, e.g.: 35 * Error: "blah" 36 * <anonymous> debugger eval code:1 37 * 38 * The customFormat prop will only be taken into account if the mode isn't tiny and the 39 * depth is 0. This is because we don't want error in previews or in object to be 40 * displayed unlike other objects: 41 * - Object { err: Error } 42 * - â–¼ { 43 * err: Error: "blah" 44 * } 45 */ 46 function ErrorRep(props) { 47 const { object, mode, shouldRenderTooltip, depth } = props; 48 const preview = object.preview; 49 const customFormat = 50 props.customFormat && mode !== MODE.TINY && mode !== MODE.HEADER && !depth; 51 52 const name = getErrorName(props); 53 const errorTitle = 54 mode === MODE.TINY || mode === MODE.HEADER ? name : `${name}: `; 55 const content = []; 56 57 if (customFormat) { 58 content.push(errorTitle); 59 } else { 60 content.push(span({ className: "objectTitle", key: "title" }, errorTitle)); 61 } 62 63 if (mode !== MODE.TINY && mode !== MODE.HEADER) { 64 content.push( 65 props.Rep({ 66 ...props, 67 key: "message", 68 object: preview.message, 69 mode: props.mode || MODE.TINY, 70 useQuotes: false, 71 }) 72 ); 73 } 74 const renderStack = preview.stack && customFormat; 75 if (renderStack) { 76 const stacktrace = props.renderStacktrace 77 ? props.renderStacktrace(parseStackString(preview.stack)) 78 : getStacktraceElements(props, preview); 79 content.push(stacktrace); 80 } 81 82 const renderCause = customFormat && preview.hasOwnProperty("cause"); 83 if (renderCause) { 84 content.push(getCauseElement(props, preview)); 85 } 86 87 return span( 88 { 89 "data-link-actor-id": object.actor, 90 className: `objectBox-stackTrace ${ 91 customFormat ? "reps-custom-format" : "" 92 }`, 93 title: shouldRenderTooltip ? `${name}: "${preview.message}"` : null, 94 }, 95 ...content 96 ); 97 } 98 99 function getErrorName(props) { 100 const { object } = props; 101 const preview = object.preview; 102 103 let name; 104 if (typeof preview?.name === "string" && preview.kind) { 105 switch (preview.kind) { 106 case "Error": 107 name = preview.name; 108 break; 109 case "DOMException": 110 name = preview.kind; 111 break; 112 default: 113 throw new Error("Unknown preview kind for the Error rep."); 114 } 115 } else { 116 name = "Error"; 117 } 118 119 return name; 120 } 121 122 /** 123 * Returns a React element reprensenting the Error stacktrace, i.e. 124 * transform error.stack from: 125 * 126 * ``` 127 * semicolon@debugger eval code:1:109 128 * jkl@debugger eval code:1:63 129 * asdf@debugger eval code:1:28 130 * 131 * @debugger eval code:1:227 132 * ``` 133 * 134 * Into a column layout: 135 * 136 * ``` 137 * semicolon (<anonymous>:8:10) 138 * jkl (<anonymous>:5:10) 139 * asdf (<anonymous>:2:10) 140 * (<anonymous>:11:1) 141 * ``` 142 */ 143 function getStacktraceElements(props, preview) { 144 const stack = []; 145 if (!preview.stack) { 146 return stack; 147 } 148 149 parseStackString(preview.stack).forEach((frame, index) => { 150 let onLocationClick; 151 const { filename, lineNumber, columnNumber, functionName, location } = 152 frame; 153 154 if ( 155 props.onViewSourceInDebugger && 156 !IGNORED_SOURCE_URLS.includes(filename) 157 ) { 158 onLocationClick = e => { 159 // Don't trigger ObjectInspector expand/collapse. 160 e.stopPropagation(); 161 props.onViewSourceInDebugger({ 162 url: filename, 163 line: lineNumber, 164 column: columnNumber, 165 }); 166 }; 167 } 168 169 stack.push( 170 "\t", 171 span( 172 { 173 key: `fn${index}`, 174 className: "objectBox-stackTrace-fn", 175 }, 176 cleanFunctionName(functionName) 177 ), 178 " ", 179 span( 180 { 181 key: `location${index}`, 182 className: "objectBox-stackTrace-location", 183 onClick: onLocationClick, 184 title: onLocationClick 185 ? `View source in debugger → ${location}` 186 : undefined, 187 }, 188 location 189 ), 190 "\n" 191 ); 192 }); 193 194 return span( 195 { 196 key: "stack", 197 className: "objectBox-stackTrace-grid", 198 }, 199 stack 200 ); 201 } 202 203 /** 204 * Returns a React element representing the cause of the Error i.e. the `cause` 205 * property in the second parameter of the Error constructor (`new Error("message", { cause })`) 206 * 207 * Example: 208 * Caused by: Error: original error 209 */ 210 function getCauseElement(props, preview) { 211 return div( 212 { 213 key: "cause-container", 214 className: "error-rep-cause", 215 }, 216 "Caused by: ", 217 props.Rep({ 218 ...props, 219 key: "cause", 220 object: preview.cause, 221 mode: props.mode || MODE.TINY, 222 }) 223 ); 224 } 225 226 /** 227 * Parse a string that should represent a stack trace and returns an array of 228 * the frames. The shape of the frames are extremely important as they can then 229 * be processed here or in the toolbox by other components. 230 * 231 * @param {string} stack 232 * @returns {Array} Array of frames, which are object with the following shape: 233 * - {String} filename 234 * - {String} functionName 235 * - {String} location 236 * - {Number} columnNumber 237 * - {Number} lineNumber 238 */ 239 function parseStackString(stack) { 240 if (!stack) { 241 return []; 242 } 243 244 const isStacktraceALongString = isLongString(stack); 245 const stackString = isStacktraceALongString ? stack.initial : stack; 246 247 if (typeof stackString !== "string") { 248 return []; 249 } 250 251 const res = []; 252 stackString.split("\n").forEach((frame, index, frames) => { 253 if (!frame) { 254 // Skip any blank lines 255 return; 256 } 257 258 // If the stacktrace is a longString, don't include the last frame in the 259 // array, since it is certainly incomplete. 260 // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833 261 // is fixed. 262 if (isStacktraceALongString && index === frames.length - 1) { 263 return; 264 } 265 266 let functionName; 267 let location; 268 269 // Retrieve the index of the first @ to split the frame string. 270 const atCharIndex = frame.indexOf("@"); 271 if (atCharIndex > -1) { 272 functionName = frame.slice(0, atCharIndex); 273 location = frame.slice(atCharIndex + 1); 274 } 275 276 if (location && location.includes(" -> ")) { 277 // If the resource was loaded by base-loader.sys.mjs, the location looks like: 278 // resource://devtools/shared/base-loader.sys.mjs -> resource://path/to/file.js . 279 // What's needed is only the last part after " -> ". 280 location = location.split(" -> ").pop(); 281 } 282 283 if (!functionName) { 284 functionName = "<anonymous>"; 285 } 286 287 // Given the input: "scriptLocation:2:100" 288 // Result: 289 // ["scriptLocation:2:100", "scriptLocation", "2", "100"] 290 const locationParts = location 291 ? location.match(/^(.*):(\d+):(\d+)$/) 292 : null; 293 294 if (location && locationParts) { 295 const [, filename, line, column] = locationParts; 296 res.push({ 297 filename, 298 functionName, 299 location, 300 columnNumber: Number(column), 301 lineNumber: Number(line), 302 }); 303 } 304 }); 305 306 return res; 307 } 308 309 // Registration 310 function supportsObject(object) { 311 return ( 312 object?.isError || 313 object?.class === "DOMException" || 314 object?.class === "Exception" 315 ); 316 } 317 318 const rep = wrapRender(ErrorRep); 319 320 // Exports from this module 321 export { rep, supportsObject };