js-property-provider.js (23861B)
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 "use strict"; 6 7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 8 9 const { 10 evalWithDebugger, 11 } = require("resource://devtools/server/actors/webconsole/eval-with-debugger.js"); 12 13 if (!isWorker) { 14 loader.lazyRequireGetter( 15 this, 16 "getSyntaxTrees", 17 "resource://devtools/shared/webconsole/parser-helper.js", 18 true 19 ); 20 } 21 const lazy = {}; 22 if (!isWorker) { 23 ChromeUtils.defineESModuleGetters( 24 lazy, 25 { 26 Reflect: "resource://gre/modules/reflect.sys.mjs", 27 }, 28 { global: "contextual" } 29 ); 30 } 31 loader.lazyRequireGetter( 32 this, 33 [ 34 "analyzeInputString", 35 "shouldInputBeAutocompleted", 36 "shouldInputBeEagerlyEvaluated", 37 ], 38 "resource://devtools/shared/webconsole/analyze-input-string.js", 39 true 40 ); 41 42 // Provide an easy way to bail out of even attempting an autocompletion 43 // if an object has way too many properties. Protects against large objects 44 // with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS. 45 const MAX_AUTOCOMPLETE_ATTEMPTS = (exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000); 46 // Prevent iterating over too many properties during autocomplete suggestions. 47 const MAX_AUTOCOMPLETIONS = (exports.MAX_AUTOCOMPLETIONS = 1500); 48 49 /** 50 * Provides a list of properties, that are possible matches based on the passed 51 * Debugger.Environment/Debugger.Object and inputValue. 52 * 53 * @param {object} An object of the following shape: 54 * - {Object} dbgObject 55 * When the debugger is not paused this Debugger.Object wraps 56 * the scope for autocompletion. 57 * It is null if the debugger is paused. 58 * - {Object} environment 59 * When the debugger is paused this Debugger.Environment is the 60 * scope for autocompletion. 61 * It is null if the debugger is not paused. 62 * - {String} inputValue 63 * Value that should be completed. 64 * - {Number} cursor (defaults to inputValue.length). 65 * Optional offset in the input where the cursor is located. If this is 66 * omitted then the cursor is assumed to be at the end of the input 67 * value. 68 * - {Array} authorizedEvaluations (defaults to []). 69 * Optional array containing all the different properties access that the engine 70 * can execute in order to retrieve its result's properties. 71 * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects 72 * in the content page ⚠️ 73 * - {WebconsoleActor} webconsoleActor 74 * A reference to a webconsole actor which we can use to retrieve the last 75 * evaluation result or create a debuggee value. 76 * - {String}: selectedNodeActor 77 * The actor id of the selected node in the inspector. 78 * - {Array<string>}: expressionVars 79 * Optional array containing variable defined in the expression. Those variables 80 * are extracted from CodeMirror state. 81 * @returns null or object 82 * If the inputValue is an unsafe getter and invokeUnsafeGetter is false, the 83 * following form is returned: 84 * 85 * { 86 * isUnsafeGetter: true, 87 * getterPath: {Array<String>} An array of the property chain leading to the 88 * getter. Example: ["x", "myGetter"] 89 * } 90 * 91 * If no completion valued could be computed, and the input is not an unsafe 92 * getter, null is returned. 93 * 94 * Otherwise an object with the following form is returned: 95 * { 96 * matches: Set<string> 97 * matchProp: Last part of the inputValue that was used to find 98 * the matches-strings. 99 * isElementAccess: Boolean set to true if the evaluation is an element 100 * access (e.g. `window["addEvent`). 101 * } 102 */ 103 // eslint-disable-next-line complexity 104 function jsPropertyProvider({ 105 dbgObject, 106 environment, 107 frameActorId, 108 inputValue, 109 cursor, 110 authorizedEvaluations = [], 111 webconsoleActor, 112 selectedNodeActor, 113 expressionVars = [], 114 }) { 115 if (cursor === undefined) { 116 cursor = inputValue.length; 117 } 118 119 inputValue = inputValue.substring(0, cursor); 120 121 // Analyse the inputValue and find the beginning of the last part that 122 // should be completed. 123 const inputAnalysis = analyzeInputString(inputValue); 124 125 if (!shouldInputBeAutocompleted(inputAnalysis)) { 126 return null; 127 } 128 129 let { 130 lastStatement, 131 isElementAccess, 132 mainExpression, 133 matchProp, 134 isPropertyAccess, 135 } = inputAnalysis; 136 137 // Eagerly evaluate the main expression and return the results properties. 138 // e.g. `obj.func().a` will evaluate `obj.func()` and return properties matching `a`. 139 // NOTE: this is only useful when the input has a property access. 140 if (webconsoleActor && shouldInputBeEagerlyEvaluated(inputAnalysis)) { 141 const eagerResponse = evalWithDebugger( 142 mainExpression, 143 { eager: true, selectedNodeActor, frameActor: frameActorId }, 144 webconsoleActor 145 ); 146 147 const ret = eagerResponse?.result?.return; 148 149 // Only send matches if eager evaluation returned something meaningful 150 if (ret && ret !== undefined) { 151 const matches = 152 typeof ret != "object" 153 ? getMatchedProps(ret, matchProp) 154 : getMatchedPropsInDbgObject(ret, matchProp); 155 156 return prepareReturnedObject({ 157 matches, 158 search: matchProp, 159 isElementAccess, 160 }); 161 } 162 } 163 164 // AST representation of the expression before the last access char (`.` or `[`). 165 let astExpression; 166 const startQuoteRegex = /^('|"|`)/; 167 const env = environment || dbgObject.asEnvironment(); 168 169 // Catch literals like [1,2,3] or "foo" and return the matches from 170 // their prototypes. 171 // Don't run this is a worker, migrating to acorn should allow this 172 // to run in a worker - Bug 1217198. 173 if (!isWorker && isPropertyAccess) { 174 const syntaxTrees = getSyntaxTrees(mainExpression); 175 const lastTree = syntaxTrees[syntaxTrees.length - 1]; 176 const lastBody = lastTree?.body[lastTree.body.length - 1]; 177 178 // Finding the last expression since we've sliced up until the dot. 179 // If there were parse errors this won't exist. 180 if (lastBody) { 181 if (!lastBody.expression) { 182 return null; 183 } 184 185 astExpression = lastBody.expression; 186 let matchingObject; 187 188 if (astExpression.type === "ArrayExpression") { 189 matchingObject = getContentPrototypeObject(env, "Array"); 190 } else if ( 191 astExpression.type === "Literal" && 192 typeof astExpression.value === "string" 193 ) { 194 matchingObject = getContentPrototypeObject(env, "String"); 195 } else if ( 196 astExpression.type === "Literal" && 197 Number.isFinite(astExpression.value) 198 ) { 199 // The parser rightfuly indicates that we have a number in some cases (e.g. `1.`), 200 // but we don't want to return Number proto properties in that case since 201 // the result would be invalid (i.e. `1.toFixed()` throws). 202 // So if the expression value is an integer, it should not end with `{Number}.` 203 // (but the following are fine: `1..`, `(1.).`). 204 if ( 205 !Number.isInteger(astExpression.value) || 206 /\d[^\.]{0}\.$/.test(lastStatement) === false 207 ) { 208 matchingObject = getContentPrototypeObject(env, "Number"); 209 } else { 210 return null; 211 } 212 } 213 214 if (matchingObject) { 215 let search = matchProp; 216 217 let elementAccessQuote; 218 if (isElementAccess && startQuoteRegex.test(matchProp)) { 219 elementAccessQuote = matchProp[0]; 220 search = matchProp.replace(startQuoteRegex, ""); 221 } 222 223 let props = getMatchedPropsInDbgObject(matchingObject, search); 224 225 if (isElementAccess) { 226 props = wrapMatchesInQuotes(props, elementAccessQuote); 227 } 228 229 return { 230 isElementAccess, 231 matchProp, 232 matches: props, 233 }; 234 } 235 } 236 } 237 238 // We are completing a variable / a property lookup. 239 let properties = []; 240 241 if (astExpression) { 242 if (isPropertyAccess) { 243 properties = getPropertiesFromAstExpression(astExpression); 244 245 if (properties === null) { 246 return null; 247 } 248 } 249 } else { 250 properties = lastStatement.split("."); 251 if (isElementAccess) { 252 const lastPart = properties[properties.length - 1]; 253 const openBracketIndex = lastPart.lastIndexOf("["); 254 matchProp = lastPart.substr(openBracketIndex + 1); 255 properties[properties.length - 1] = lastPart.substring( 256 0, 257 openBracketIndex 258 ); 259 } else { 260 matchProp = properties.pop().trimLeft(); 261 } 262 } 263 264 let search = matchProp; 265 let elementAccessQuote; 266 if (isElementAccess && startQuoteRegex.test(search)) { 267 elementAccessQuote = search[0]; 268 search = search.replace(startQuoteRegex, ""); 269 } 270 271 let obj = dbgObject; 272 if (properties.length === 0) { 273 const environmentProperties = getMatchedPropsInEnvironment(env, search); 274 const expressionVariables = new Set( 275 expressionVars.filter(variableName => variableName.startsWith(matchProp)) 276 ); 277 278 for (const prop of environmentProperties) { 279 expressionVariables.add(prop); 280 } 281 282 return { 283 isElementAccess, 284 matchProp, 285 matches: expressionVariables, 286 }; 287 } 288 289 let firstProp = properties.shift(); 290 if (typeof firstProp == "string") { 291 firstProp = firstProp.trim(); 292 } 293 294 if (firstProp === "this") { 295 // Special case for 'this' - try to get the Object from the Environment. 296 // No problem if it throws, we will just not autocomplete. 297 try { 298 obj = env.object; 299 } catch (e) { 300 // Ignore. 301 } 302 } else if (firstProp === "$_" && webconsoleActor) { 303 obj = webconsoleActor.getLastConsoleInputEvaluation(); 304 } else if (firstProp === "$0" && selectedNodeActor && webconsoleActor) { 305 const actor = webconsoleActor.conn.getActor(selectedNodeActor); 306 if (actor) { 307 try { 308 obj = webconsoleActor.makeDebuggeeValue(actor.rawNode); 309 } catch (e) { 310 // Ignore. 311 } 312 } 313 } else if (hasArrayIndex(firstProp)) { 314 obj = getArrayMemberProperty(null, env, firstProp); 315 } else { 316 obj = getVariableInEnvironment(env, firstProp); 317 } 318 319 if (!isObjectUsable(obj)) { 320 return null; 321 } 322 323 // We get the rest of the properties recursively starting from the 324 // Debugger.Object that wraps the first property 325 for (let [index, prop] of properties.entries()) { 326 if (typeof prop === "string") { 327 prop = prop.trim(); 328 } 329 330 if (prop === undefined || prop === null || prop === "") { 331 return null; 332 } 333 334 const propPath = [firstProp].concat(properties.slice(0, index + 1)); 335 const authorized = authorizedEvaluations.some( 336 x => JSON.stringify(x) === JSON.stringify(propPath) 337 ); 338 339 if (!authorized && DevToolsUtils.isUnsafeGetter(obj, prop)) { 340 // If we try to access an unsafe getter, return its name so we can consume that 341 // on the frontend. 342 return { 343 isUnsafeGetter: true, 344 getterPath: propPath, 345 }; 346 } 347 348 if (hasArrayIndex(prop)) { 349 // The property to autocomplete is a member of array. For example 350 // list[i][j]..[n]. Traverse the array to get the actual element. 351 obj = getArrayMemberProperty(obj, null, prop); 352 } else { 353 obj = DevToolsUtils.getProperty(obj, prop, authorized); 354 } 355 356 if (!isObjectUsable(obj)) { 357 return null; 358 } 359 } 360 361 const matches = 362 typeof obj != "object" 363 ? getMatchedProps(obj, search) 364 : getMatchedPropsInDbgObject(obj, search); 365 return prepareReturnedObject({ 366 matches, 367 search, 368 isElementAccess, 369 elementAccessQuote, 370 }); 371 } 372 373 function hasArrayIndex(str) { 374 return /\[\d+\]$/.test(str); 375 } 376 377 /** 378 * For a given environment and constructor name, returns its Debugger.Object wrapped 379 * prototype. 380 * 381 * @param {Environment} env 382 * @param {string} name: Name of the constructor object we want the prototype of. 383 * @returns {Debugger.Object|null} the prototype, or null if it not found. 384 */ 385 function getContentPrototypeObject(env, name) { 386 // Retrieve the outermost environment to get the global object. 387 let outermostEnv = env; 388 while (outermostEnv?.parent) { 389 outermostEnv = outermostEnv.parent; 390 } 391 392 const constructorObj = DevToolsUtils.getProperty(outermostEnv.object, name); 393 if (!constructorObj) { 394 return null; 395 } 396 397 return DevToolsUtils.getProperty(constructorObj, "prototype"); 398 } 399 400 /** 401 * @param {object} ast: An AST representing a property access (e.g. `foo.bar["baz"].x`) 402 * @returns {Array|null} An array representing the property access 403 * (e.g. ["foo", "bar", "baz", "x"]). 404 */ 405 function getPropertiesFromAstExpression(ast) { 406 let result = []; 407 if (!ast) { 408 return result; 409 } 410 const { type, property, object, name, expression } = ast; 411 if (type === "ThisExpression") { 412 result.unshift("this"); 413 } else if (type === "Identifier" && name) { 414 result.unshift(name); 415 } else if (type === "OptionalExpression" && expression) { 416 result = (getPropertiesFromAstExpression(expression) || []).concat(result); 417 } else if ( 418 type === "MemberExpression" || 419 type === "OptionalMemberExpression" 420 ) { 421 if (property) { 422 if (property.type === "Identifier" && property.name) { 423 result.unshift(property.name); 424 } else if (property.type === "Literal") { 425 result.unshift(property.value); 426 } 427 } 428 if (object) { 429 result = (getPropertiesFromAstExpression(object) || []).concat(result); 430 } 431 } else { 432 return null; 433 } 434 return result; 435 } 436 437 function wrapMatchesInQuotes(matches, quote = `"`) { 438 return new Set( 439 [...matches].map(p => { 440 // Escape as a double-quoted string literal 441 p = JSON.stringify(p); 442 443 // We don't have to do anything more when using double quotes 444 if (quote == `"`) { 445 return p; 446 } 447 448 // Remove surrounding double quotes 449 p = p.slice(1, -1); 450 451 // Unescape inner double quotes (all must be escaped, so no need to count backslashes) 452 p = p.replace(/\\(?=")/g, ""); 453 454 // Escape the specified quote (assuming ' or `, which are treated literally in regex) 455 p = p.replace(new RegExp(quote, "g"), "\\$&"); 456 457 // Template literals treat ${ specially, escape it 458 if (quote == "`") { 459 p = p.replace(/\${/g, "\\$&"); 460 } 461 462 // Surround the result with quotes 463 return `${quote}${p}${quote}`; 464 }) 465 ); 466 } 467 468 /** 469 * Get the array member of obj for the given prop. For example, given 470 * prop='list[0][1]' the element at [0][1] of obj.list is returned. 471 * 472 * @param object obj 473 * The object to operate on. Should be null if env is passed. 474 * @param object env 475 * The Environment to operate in. Should be null if obj is passed. 476 * @param string prop 477 * The property to return. 478 * @return null or Object 479 * Returns null if the property couldn't be located. Otherwise the array 480 * member identified by prop. 481 */ 482 function getArrayMemberProperty(obj, env, prop) { 483 // First get the array. 484 const propWithoutIndices = prop.substr(0, prop.indexOf("[")); 485 486 if (env) { 487 obj = getVariableInEnvironment(env, propWithoutIndices); 488 } else { 489 obj = DevToolsUtils.getProperty(obj, propWithoutIndices); 490 } 491 492 if (!isObjectUsable(obj)) { 493 return null; 494 } 495 496 // Then traverse the list of indices to get the actual element. 497 let result; 498 const arrayIndicesRegex = /\[[^\]]*\]/g; 499 while ((result = arrayIndicesRegex.exec(prop)) !== null) { 500 const indexWithBrackets = result[0]; 501 const indexAsText = indexWithBrackets.substr( 502 1, 503 indexWithBrackets.length - 2 504 ); 505 const index = parseInt(indexAsText, 10); 506 507 if (isNaN(index)) { 508 return null; 509 } 510 511 obj = DevToolsUtils.getProperty(obj, index); 512 513 if (!isObjectUsable(obj)) { 514 return null; 515 } 516 } 517 518 return obj; 519 } 520 521 /** 522 * Check if the given Debugger.Object can be used for autocomplete. 523 * 524 * @param Debugger.Object object 525 * The Debugger.Object to check. 526 * @return boolean 527 * True if further inspection into the object is possible, or false 528 * otherwise. 529 */ 530 function isObjectUsable(object) { 531 if (object == null) { 532 return false; 533 } 534 535 if (typeof object == "object" && object.class == "DeadObject") { 536 return false; 537 } 538 539 return true; 540 } 541 542 /** 543 * @see getExactMatchImpl() 544 */ 545 function getVariableInEnvironment(environment, name) { 546 return getExactMatchImpl(environment, name, DebuggerEnvironmentSupport); 547 } 548 549 function prepareReturnedObject({ 550 matches, 551 search, 552 isElementAccess, 553 elementAccessQuote, 554 }) { 555 if (isElementAccess) { 556 // If it's an element access, we need to wrap properties in quotes (either the one 557 // the user already typed, or `"`). 558 559 matches = wrapMatchesInQuotes(matches, elementAccessQuote); 560 } else if (!isWorker) { 561 // If we're not performing an element access, we need to check that the property 562 // are suited for a dot access. (reflect.sys.mjs is not available in worker context yet, 563 // see Bug 1507181). 564 for (const match of matches) { 565 try { 566 // In order to know if the property is suited for dot notation, we use Reflect 567 // to parse an expression where we try to access the property with a dot. If it 568 // throws, this means that we need to do an element access instead. 569 lazy.Reflect.parse(`({${match}: true})`); 570 } catch (e) { 571 matches.delete(match); 572 } 573 } 574 } 575 576 return { isElementAccess, matchProp: search, matches }; 577 } 578 579 /** 580 * @see getMatchedPropsImpl() 581 */ 582 function getMatchedPropsInEnvironment(environment, match) { 583 return getMatchedPropsImpl(environment, match, DebuggerEnvironmentSupport); 584 } 585 586 /** 587 * @see getMatchedPropsImpl() 588 */ 589 function getMatchedPropsInDbgObject(dbgObject, match) { 590 return getMatchedPropsImpl(dbgObject, match, DebuggerObjectSupport); 591 } 592 593 /** 594 * @see getMatchedPropsImpl() 595 */ 596 function getMatchedProps(obj, match) { 597 if (typeof obj != "object") { 598 obj = obj.constructor.prototype; 599 } 600 return getMatchedPropsImpl(obj, match, JSObjectSupport); 601 } 602 603 /** 604 * Get all properties in the given object (and its parent prototype chain) that 605 * match a given prefix. 606 * 607 * @param {Mixed} obj 608 * Object whose properties we want to filter. 609 * @param {string} match 610 * Filter for properties that match this string. 611 * @returns {Set} List of matched properties. 612 */ 613 function getMatchedPropsImpl(obj, match, { chainIterator, getProperties }) { 614 const matches = new Set(); 615 let numProps = 0; 616 617 const insensitiveMatching = match && match[0].toUpperCase() !== match[0]; 618 const propertyMatches = prop => { 619 return insensitiveMatching 620 ? prop.toLocaleLowerCase().startsWith(match.toLocaleLowerCase()) 621 : prop.startsWith(match); 622 }; 623 624 // We need to go up the prototype chain. 625 const iter = chainIterator(obj); 626 for (obj of iter) { 627 const props = getProperties(obj); 628 if (!props) { 629 continue; 630 } 631 numProps += props.length; 632 633 // If there are too many properties to event attempt autocompletion, 634 // or if we have already added the max number, then stop looping 635 // and return the partial set that has already been discovered. 636 if ( 637 numProps >= MAX_AUTOCOMPLETE_ATTEMPTS || 638 matches.size >= MAX_AUTOCOMPLETIONS 639 ) { 640 break; 641 } 642 643 for (let i = 0; i < props.length; i++) { 644 const prop = props[i]; 645 if (!propertyMatches(prop)) { 646 continue; 647 } 648 649 // If it is an array index, we can't take it. 650 // This uses a trick: converting a string to a number yields NaN if 651 // the operation failed, and NaN is not equal to itself. 652 // eslint-disable-next-line no-self-compare 653 if (+prop != +prop || prop === "Infinity") { 654 matches.add(prop); 655 } 656 657 if (matches.size >= MAX_AUTOCOMPLETIONS) { 658 break; 659 } 660 } 661 } 662 663 return matches; 664 } 665 666 /** 667 * Returns a property value based on its name from the given object, by 668 * recursively checking the object's prototype. 669 * 670 * @param object obj 671 * An object to look the property into. 672 * @param string name 673 * The property that is looked up. 674 * @returns object|undefined 675 * A Debugger.Object if the property exists in the object's prototype 676 * chain, undefined otherwise. 677 */ 678 function getExactMatchImpl(obj, name, { chainIterator, getProperty }) { 679 // We need to go up the prototype chain. 680 const iter = chainIterator(obj); 681 for (obj of iter) { 682 const prop = getProperty(obj, name, obj); 683 if (prop) { 684 return prop.value; 685 } 686 } 687 return undefined; 688 } 689 690 var JSObjectSupport = { 691 *chainIterator(obj) { 692 while (obj) { 693 yield obj; 694 try { 695 obj = Object.getPrototypeOf(obj); 696 } catch (error) { 697 // The above can throw e.g. for some proxy objects. 698 return; 699 } 700 } 701 }, 702 703 getProperties(obj) { 704 try { 705 return Object.getOwnPropertyNames(obj); 706 } catch (error) { 707 // The above can throw e.g. for some proxy objects. 708 return null; 709 } 710 }, 711 712 getProperty() { 713 // getProperty is unsafe with raw JS objects. 714 throw new Error("Unimplemented!"); 715 }, 716 }; 717 718 var DebuggerObjectSupport = { 719 *chainIterator(obj) { 720 while (obj) { 721 yield obj; 722 try { 723 // There could be transparent security wrappers, unwrap to check if it's a proxy. 724 const unwrapped = DevToolsUtils.unwrap(obj); 725 if (unwrapped === undefined) { 726 // Objects belonging to an invisible-to-debugger compartment can't be unwrapped. 727 return; 728 } 729 730 if (unwrapped.isProxy) { 731 // Proxies might have a `getPrototypeOf` method, which is triggered by `obj.proto`, 732 // but this does not impact the actual prototype chain. 733 // In such case, we need to use the proxy target prototype. 734 // We retrieve proxyTarget from `obj` (and not `unwrapped`) to avoid exposing 735 // the unwrapped target. 736 obj = unwrapped.proxyTarget; 737 } 738 obj = obj.proto; 739 } catch (error) { 740 // The above can throw e.g. for some proxy objects. 741 return; 742 } 743 } 744 }, 745 746 getProperties(obj) { 747 try { 748 return obj.getOwnPropertyNames(); 749 } catch (error) { 750 // The above can throw e.g. for some proxy objects. 751 return null; 752 } 753 }, 754 755 getProperty() { 756 // This is left unimplemented in favor to DevToolsUtils.getProperty(). 757 throw new Error("Unimplemented!"); 758 }, 759 }; 760 761 var DebuggerEnvironmentSupport = { 762 *chainIterator(obj) { 763 while (obj) { 764 yield obj; 765 obj = obj.parent; 766 } 767 }, 768 769 getProperties(obj) { 770 const names = obj.names(); 771 772 // Include 'this' in results (in sorted order) 773 for (let i = 0; i < names.length; i++) { 774 if (i === names.length - 1 || names[i + 1] > "this") { 775 names.splice(i + 1, 0, "this"); 776 break; 777 } 778 } 779 780 return names; 781 }, 782 783 getProperty(obj, name) { 784 let result; 785 // Try/catch since name can be anything, and getVariable throws if 786 // it's not a valid ECMAScript identifier name 787 try { 788 // TODO: we should use getVariableDescriptor() here - bug 725815. 789 result = obj.getVariable(name); 790 } catch (e) { 791 // Ignore. 792 } 793 794 // FIXME: Need actual UI, bug 941287. 795 if ( 796 result == null || 797 (typeof result == "object" && 798 (result.optimizedOut || result.missingArguments)) 799 ) { 800 return null; 801 } 802 return { value: result }; 803 }, 804 }; 805 806 exports.jsPropertyProvider = DevToolsUtils.makeInfallible(jsPropertyProvider); 807 808 // Export a version that will throw (for tests) 809 exports.fallibleJsPropertyProvider = jsPropertyProvider;