/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ const { maybeEscapePropertyName, } = require("resource://devtools/client/shared/components/reps/reps/rep-utils.js"); const ArrayRep = require("resource://devtools/client/shared/components/reps/reps/array.js"); const GripArrayRep = require("resource://devtools/client/shared/components/reps/reps/grip-array.js"); const GripMap = require("resource://devtools/client/shared/components/reps/reps/grip-map.js"); const GripEntryRep = require("resource://devtools/client/shared/components/reps/reps/grip-entry.js"); const ErrorRep = require("resource://devtools/client/shared/components/reps/reps/error.js"); const BigIntRep = require("resource://devtools/client/shared/components/reps/reps/big-int.js"); const { isLongString, } = require("resource://devtools/client/shared/components/reps/reps/string.js"); const MAX_NUMERICAL_PROPERTIES = 100; const NODE_TYPES = { BUCKET: Symbol("[n…m]"), DEFAULT_PROPERTIES: Symbol(""), ENTRIES: Symbol(""), GET: Symbol(""), GRIP: Symbol("GRIP"), MAP_ENTRY_KEY: Symbol(""), MAP_ENTRY_VALUE: Symbol(""), PROMISE_REASON: Symbol(""), PROMISE_STATE: Symbol(""), PROMISE_VALUE: Symbol(""), PROXY_HANDLER: Symbol(""), PROXY_TARGET: Symbol(""), SET: Symbol(""), PROTOTYPE: Symbol(""), BLOCK: Symbol("☲"), }; let WINDOW_PROPERTIES = {}; if (typeof window === "object") { WINDOW_PROPERTIES = Object.getOwnPropertyNames(window); } function getType(item) { return item.type; } function getValue(item) { if (nodeHasValue(item)) { return item.contents.value; } if (nodeHasGetterValue(item)) { return item.contents.getterValue; } if (nodeHasAccessors(item)) { return item.contents; } return undefined; } function getFront(item) { return item && item.contents && item.contents.front; } function getActor(item, roots) { const isRoot = isNodeRoot(item, roots); const value = getValue(item); return isRoot || !value ? null : value.actor; } function isNodeRoot(item, roots) { const gripItem = getClosestGripNode(item); const value = getValue(gripItem); return ( value && roots.some(root => { const rootValue = getValue(root); return rootValue && rootValue.actor === value.actor; }) ); } function nodeIsBucket(item) { return getType(item) === NODE_TYPES.BUCKET; } function nodeIsEntries(item) { return getType(item) === NODE_TYPES.ENTRIES; } function nodeIsMapEntry(item) { return GripEntryRep.supportsObject(getValue(item)); } function nodeHasChildren(item) { return Array.isArray(item.contents); } function nodeHasValue(item) { return item && item.contents && item.contents.hasOwnProperty("value"); } function nodeHasGetterValue(item) { return item && item.contents && item.contents.hasOwnProperty("getterValue"); } function nodeIsObject(item) { const value = getValue(item); return value && value.type === "object"; } function nodeIsArrayLike(item) { const value = getValue(item); return GripArrayRep.supportsObject(value) || ArrayRep.supportsObject(value); } function nodeIsFunction(item) { const value = getValue(item); return value && value.class === "Function"; } function nodeIsOptimizedOut(item) { const value = getValue(item); return !nodeHasChildren(item) && value && value.optimizedOut; } function nodeIsUninitializedBinding(item) { const value = getValue(item); return value && value.uninitialized; } // Used to check if an item represents a binding that exists in a sourcemap's // original file content, but does not match up with a binding found in the // generated code. function nodeIsUnmappedBinding(item) { const value = getValue(item); return value && value.unmapped; } // Used to check if an item represents a binding that exists in the debugger's // parser result, but does not match up with a binding returned by the // devtools server. function nodeIsUnscopedBinding(item) { const value = getValue(item); return value && value.unscoped; } function nodeIsMissingArguments(item) { const value = getValue(item); return !nodeHasChildren(item) && value && value.missingArguments; } function nodeHasProperties(item) { return !nodeHasChildren(item) && nodeIsObject(item); } function nodeIsPrimitive(item) { return ( nodeIsBigInt(item) || (!nodeHasChildren(item) && !nodeHasProperties(item) && !nodeIsEntries(item) && !nodeIsMapEntry(item) && !nodeHasAccessors(item) && !nodeIsBucket(item) && !nodeIsLongString(item)) ); } function nodeIsDefaultProperties(item) { return getType(item) === NODE_TYPES.DEFAULT_PROPERTIES; } function isDefaultWindowProperty(name) { return WINDOW_PROPERTIES.includes(name); } function nodeIsPromise(item) { const value = getValue(item); if (!value) { return false; } return value.class == "Promise"; } function nodeIsProxy(item) { const value = getValue(item); if (!value) { return false; } return value.class == "Proxy"; } function nodeIsPrototype(item) { return getType(item) === NODE_TYPES.PROTOTYPE; } function nodeIsWindow(item) { const value = getValue(item); if (!value) { return false; } return value.class == "Window"; } function nodeIsGetter(item) { return getType(item) === NODE_TYPES.GET; } function nodeIsSetter(item) { return getType(item) === NODE_TYPES.SET; } function nodeIsBlock(item) { return getType(item) === NODE_TYPES.BLOCK; } function nodeIsError(item) { return ErrorRep.supportsObject(getValue(item)); } function nodeIsLongString(item) { return isLongString(getValue(item)); } function nodeIsBigInt(item) { return BigIntRep.supportsObject(getValue(item)); } function nodeHasFullText(item) { const value = getValue(item); return nodeIsLongString(item) && value.hasOwnProperty("fullText"); } function nodeHasGetter(item) { const getter = getNodeGetter(item); return getter && getter.type !== "undefined"; } function nodeHasSetter(item) { const setter = getNodeSetter(item); return setter && setter.type !== "undefined"; } function nodeHasAccessors(item) { return nodeHasGetter(item) || nodeHasSetter(item); } function nodeSupportsNumericalBucketing(item) { // We exclude elements with entries since it's the node // itself that can have buckets. return ( (nodeIsArrayLike(item) && !nodeHasEntries(item)) || nodeIsEntries(item) || nodeIsBucket(item) ); } function nodeHasEntries(item) { const value = getValue(item); if (!value) { return false; } const className = value.class; return ( className === "Map" || className === "Set" || className === "WeakMap" || className === "WeakSet" || className === "Storage" || className === "URLSearchParams" || className === "Headers" || className === "FormData" || className === "MIDIInputMap" || className === "MIDIOutputMap" ); } function nodeNeedsNumericalBuckets(item) { return ( nodeSupportsNumericalBucketing(item) && getNumericalPropertiesCount(item) > MAX_NUMERICAL_PROPERTIES ); } function makeNodesForPromiseProperties(loadedProps, item) { const { reason, value, state } = loadedProps.promiseState; const properties = []; if (state) { properties.push( createNode({ parent: item, name: "", contents: { value: state }, type: NODE_TYPES.PROMISE_STATE, }) ); } if (reason) { properties.push( createNode({ parent: item, name: "", contents: { value: reason.getGrip ? reason.getGrip() : reason, front: reason.getGrip ? reason : null, }, type: NODE_TYPES.PROMISE_REASON, }) ); } if (value) { properties.push( createNode({ parent: item, name: "", contents: { value: value.getGrip ? value.getGrip() : value, front: value.getGrip ? value : null, }, type: NODE_TYPES.PROMISE_VALUE, }) ); } return properties; } function makeNodesForProxyProperties(loadedProps, item) { const { proxyHandler, proxyTarget } = loadedProps; const isProxyHandlerFront = proxyHandler && proxyHandler.getGrip; const proxyHandlerGrip = isProxyHandlerFront ? proxyHandler.getGrip() : proxyHandler; const proxyHandlerFront = isProxyHandlerFront ? proxyHandler : null; const isProxyTargetFront = proxyTarget && proxyTarget.getGrip; const proxyTargetGrip = isProxyTargetFront ? proxyTarget.getGrip() : proxyTarget; const proxyTargetFront = isProxyTargetFront ? proxyTarget : null; return [ createNode({ parent: item, name: "", contents: { value: proxyTargetGrip, front: proxyTargetFront }, type: NODE_TYPES.PROXY_TARGET, }), createNode({ parent: item, name: "", contents: { value: proxyHandlerGrip, front: proxyHandlerFront }, type: NODE_TYPES.PROXY_HANDLER, }), ]; } function makeNodesForEntries(item) { const nodeName = ""; return createNode({ parent: item, name: nodeName, contents: null, type: NODE_TYPES.ENTRIES, }); } function makeNodesForMapEntry(item) { const nodeValue = getValue(item); if (!nodeValue || !nodeValue.preview) { return []; } const { key, value } = nodeValue.preview; const isKeyFront = key && key.getGrip; const keyGrip = isKeyFront ? key.getGrip() : key; const keyFront = isKeyFront ? key : null; const isValueFront = value && value.getGrip; const valueGrip = isValueFront ? value.getGrip() : value; const valueFront = isValueFront ? value : null; return [ createNode({ parent: item, name: "", contents: { value: keyGrip, front: keyFront }, type: NODE_TYPES.MAP_ENTRY_KEY, }), createNode({ parent: item, name: "", contents: { value: valueGrip, front: valueFront }, type: NODE_TYPES.MAP_ENTRY_VALUE, }), ]; } function getNodeGetter(item) { return item && item.contents ? item.contents.get : undefined; } function getNodeSetter(item) { return item && item.contents ? item.contents.set : undefined; } function sortProperties(properties) { return properties.sort((a, b) => { // Sort numbers in ascending order and sort strings lexicographically const aInt = parseInt(a, 10); const bInt = parseInt(b, 10); if (isNaN(aInt) || isNaN(bInt)) { return a > b ? 1 : -1; } return aInt - bInt; }); } function makeNumericalBuckets(parent) { const numProperties = getNumericalPropertiesCount(parent); // We want to have at most a hundred slices. const bucketSize = 10 ** Math.max(2, Math.ceil(Math.log10(numProperties)) - 2); const numBuckets = Math.ceil(numProperties / bucketSize); const buckets = []; for (let i = 1; i <= numBuckets; i++) { const minKey = (i - 1) * bucketSize; const maxKey = Math.min(i * bucketSize - 1, numProperties - 1); const startIndex = nodeIsBucket(parent) ? parent.meta.startIndex : 0; const minIndex = startIndex + minKey; const maxIndex = startIndex + maxKey; const bucketName = `[${minIndex}…${maxIndex}]`; buckets.push( createNode({ parent, name: bucketName, contents: null, type: NODE_TYPES.BUCKET, meta: { startIndex: minIndex, endIndex: maxIndex, }, }) ); } return buckets; } function makeDefaultPropsBucket(propertiesNames, parent, ownProperties) { const userPropertiesNames = []; const defaultProperties = []; propertiesNames.forEach(name => { if (isDefaultWindowProperty(name)) { defaultProperties.push(name); } else { userPropertiesNames.push(name); } }); const nodes = makeNodesForOwnProps( userPropertiesNames, parent, ownProperties ); if (defaultProperties.length > 0) { const defaultPropertiesNode = createNode({ parent, name: "", contents: null, type: NODE_TYPES.DEFAULT_PROPERTIES, }); const defaultNodes = makeNodesForOwnProps( defaultProperties, defaultPropertiesNode, ownProperties ); nodes.push(setNodeChildren(defaultPropertiesNode, defaultNodes)); } return nodes; } function makeNodesForOwnProps(propertiesNames, parent, ownProperties) { return propertiesNames.map(name => { const property = ownProperties[name]; let propertyValue = property; if (property && property.hasOwnProperty("getterValue")) { propertyValue = property.getterValue; } else if (property && property.hasOwnProperty("value")) { propertyValue = property.value; } // propertyValue can be a front (LongString or Object) or a primitive grip. const isFront = propertyValue && propertyValue.getGrip; const front = isFront ? propertyValue : null; const grip = isFront ? front.getGrip() : propertyValue; return createNode({ parent, name: maybeEscapePropertyName(name), propertyName: name, contents: { ...(property || {}), value: grip, front, }, }); }); } function makeNodesForProperties(objProps, parent) { const { ownProperties = {}, ownSymbols, privateProperties, prototype, safeGetterValues, } = objProps; const parentValue = getValue(parent); const allProperties = { ...ownProperties, ...safeGetterValues }; // Ignore properties that are neither non-concrete nor getters/setters. const propertiesNames = sortProperties(Object.keys(allProperties)).filter( name => { if (!allProperties[name]) { return false; } const properties = Object.getOwnPropertyNames(allProperties[name]); return properties.some(property => ["value", "getterValue", "get", "set"].includes(property) ); } ); const isParentNodeWindow = parentValue && parentValue.class == "Window"; const nodes = isParentNodeWindow ? makeDefaultPropsBucket(propertiesNames, parent, allProperties) : makeNodesForOwnProps(propertiesNames, parent, allProperties); if (Array.isArray(ownSymbols)) { ownSymbols.forEach((ownSymbol, index) => { const descriptorValue = ownSymbol?.descriptor?.value; const hasGrip = descriptorValue?.getGrip; const symbolGrip = hasGrip ? descriptorValue.getGrip() : descriptorValue; const symbolFront = hasGrip ? ownSymbol.descriptor.value : null; nodes.push( createNode({ parent, name: ownSymbol.name, path: `symbol-${index}`, contents: { value: symbolGrip, front: symbolFront, }, }) ); }, this); } if (Array.isArray(privateProperties)) { privateProperties.forEach((privateProperty, index) => { const descriptorValue = privateProperty?.descriptor?.value; const hasGrip = descriptorValue?.getGrip; const privatePropertyGrip = hasGrip ? descriptorValue.getGrip() : descriptorValue; const privatePropertyFront = hasGrip ? privateProperty.descriptor.value : null; nodes.push( createNode({ parent, name: privateProperty.name, path: `private-${index}`, contents: { value: privatePropertyGrip, front: privatePropertyFront, }, }) ); }, this); } if (nodeIsPromise(parent)) { nodes.push(...makeNodesForPromiseProperties(objProps, parent)); } if (nodeHasEntries(parent)) { nodes.push(makeNodesForEntries(parent)); } // Add accessor nodes if needed const defaultPropertiesNode = isParentNodeWindow ? nodes.find(node => nodeIsDefaultProperties(node)) : null; for (const name of propertiesNames) { const property = allProperties[name]; const isDefaultProperty = isParentNodeWindow && defaultPropertiesNode && isDefaultWindowProperty(name); const parentNode = isDefaultProperty ? defaultPropertiesNode : parent; const parentContentsArray = isDefaultProperty && defaultPropertiesNode ? defaultPropertiesNode.contents : nodes; if (property.get && property.get.type !== "undefined") { parentContentsArray.push( createGetterNode({ parent: parentNode, property, name, }) ); } if (property.set && property.set.type !== "undefined") { parentContentsArray.push( createSetterNode({ parent: parentNode, property, name, }) ); } } // Add the prototype if it exists and is not null if (prototype && prototype.type !== "null") { nodes.push(makeNodeForPrototype(objProps, parent)); } return nodes; } function setNodeFullText(loadedProps, node) { if (nodeHasFullText(node) || !nodeIsLongString(node)) { return node; } const { fullText } = loadedProps; if (nodeHasValue(node)) { node.contents.value.fullText = fullText; } else if (nodeHasGetterValue(node)) { node.contents.getterValue.fullText = fullText; } return node; } function makeNodeForPrototype(objProps, parent) { const { prototype } = objProps || {}; // Add the prototype if it exists and is not null if (prototype && prototype.type !== "null") { return createNode({ parent, name: "", contents: { value: prototype.getGrip ? prototype.getGrip() : prototype, front: prototype.getGrip ? prototype : null, }, type: NODE_TYPES.PROTOTYPE, }); } return null; } function createNode(options) { const { parent, name, propertyName, path, contents, type = NODE_TYPES.GRIP, meta, } = options; if (contents === undefined) { return null; } // The path is important to uniquely identify the item in the entire // tree. This helps debugging & optimizes React's rendering of large // lists. The path will be separated by property name. return { parent, name, // `name` can be escaped; propertyName contains the original property name. propertyName, path: createPath(parent && parent.path, path || name), contents, type, meta, }; } function createGetterNode({ parent, property, name }) { const isFront = property.get && property.get.getGrip; const grip = isFront ? property.get.getGrip() : property.get; const front = isFront ? property.get : null; return createNode({ parent, name: ``, contents: { value: grip, front }, type: NODE_TYPES.GET, }); } function createSetterNode({ parent, property, name }) { const isFront = property.set && property.set.getGrip; const grip = isFront ? property.set.getGrip() : property.set; const front = isFront ? property.set : null; return createNode({ parent, name: ``, contents: { value: grip, front }, type: NODE_TYPES.SET, }); } function setNodeChildren(node, children) { node.contents = children; return node; } function getEvaluatedItem(item, evaluations) { if (!evaluations.has(item.path)) { return item; } const evaluation = evaluations.get(item.path); const isFront = evaluation && evaluation.getterValue && evaluation.getterValue.getGrip; const contents = isFront ? { getterValue: evaluation.getterValue.getGrip(), front: evaluation.getterValue, } : evaluations.get(item.path); return { ...item, contents, }; } function getChildrenWithEvaluations(options) { const { item, loadedProperties, cachedNodes, evaluations } = options; const children = getChildren({ loadedProperties, cachedNodes, item, }); if (Array.isArray(children)) { return children.map(i => getEvaluatedItem(i, evaluations)); } if (children) { return getEvaluatedItem(children, evaluations); } return []; } function getChildren(options) { const { cachedNodes, item, loadedProperties = new Map() } = options; const key = item.path; if (cachedNodes && cachedNodes.has(key)) { return cachedNodes.get(key); } const loadedProps = loadedProperties.get(key); const hasLoadedProps = loadedProperties.has(key); // Because we are dynamically creating the tree as the user // expands it (not precalculated tree structure), we cache child // arrays. This not only helps performance, but is necessary // because the expanded state depends on instances of nodes // being the same across renders. If we didn't do this, each // node would be a new instance every render. // If the node needs properties, we only add children to // the cache if the properties are loaded. const addToCache = children => { if (cachedNodes) { cachedNodes.set(item.path, children); } return children; }; // Nodes can either have children already, or be an object with // properties that we need to go and fetch. if (nodeHasChildren(item)) { return addToCache(item.contents); } if (nodeIsMapEntry(item)) { return addToCache(makeNodesForMapEntry(item)); } if (nodeIsProxy(item) && hasLoadedProps) { return addToCache(makeNodesForProxyProperties(loadedProps, item)); } if (nodeIsLongString(item) && hasLoadedProps) { // Set longString object's fullText to fetched one. return addToCache(setNodeFullText(loadedProps, item)); } if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) { // Even if we have numerical buckets, we should have loaded non indexed // properties. const bucketNodes = makeNumericalBuckets(item); return addToCache( bucketNodes.concat(makeNodesForProperties(loadedProps, item)) ); } if (!nodeIsEntries(item) && !nodeIsBucket(item) && !nodeHasProperties(item)) { return []; } if (!hasLoadedProps) { return []; } return addToCache(makeNodesForProperties(loadedProps, item)); } // Builds an expression that resolves to the value of the item in question // e.g. `b` in { a: { b: 2 } } resolves to `a.b` function getPathExpression(item) { if (item && item.parent) { const parent = nodeIsBucket(item.parent) ? item.parent.parent : item.parent; return `${getPathExpression(parent)}.${item.name}`; } return item.name; } function getParent(item) { return item.parent; } function getNumericalPropertiesCount(item) { if (nodeIsBucket(item)) { return item.meta.endIndex - item.meta.startIndex + 1; } const value = getValue(getClosestGripNode(item)); if (!value) { return 0; } if (GripArrayRep.supportsObject(value)) { return GripArrayRep.getLength(value); } if (GripMap.supportsObject(value)) { return GripMap.getLength(value); } // TODO: We can also have numerical properties on Objects, but at the // moment we don't have a way to distinguish them from non-indexed properties, // as they are all computed in a ownPropertiesLength property. return 0; } function getClosestGripNode(item) { const type = getType(item); if ( type !== NODE_TYPES.BUCKET && type !== NODE_TYPES.DEFAULT_PROPERTIES && type !== NODE_TYPES.ENTRIES ) { return item; } const parent = getParent(item); if (!parent) { return null; } return getClosestGripNode(parent); } function getClosestNonBucketNode(item) { const type = getType(item); if (type !== NODE_TYPES.BUCKET) { return item; } const parent = getParent(item); if (!parent) { return null; } return getClosestNonBucketNode(parent); } function getParentGripNode(item) { const parentNode = getParent(item); if (!parentNode) { return null; } return getClosestGripNode(parentNode); } function getParentGripValue(item) { const parentGripNode = getParentGripNode(item); if (!parentGripNode) { return null; } return getValue(parentGripNode); } function getParentFront(item) { const parentGripNode = getParentGripNode(item); if (!parentGripNode) { return null; } return getFront(parentGripNode); } function getNonPrototypeParentGripValue(item) { const parentGripNode = getParentGripNode(item); if (!parentGripNode) { return null; } if (getType(parentGripNode) === NODE_TYPES.PROTOTYPE) { return getNonPrototypeParentGripValue(parentGripNode); } return getValue(parentGripNode); } function createPath(parentPath, path) { return parentPath ? `${parentPath}◦${path}` : path; } module.exports = { createNode, createGetterNode, createSetterNode, getActor, getChildren, getChildrenWithEvaluations, getClosestGripNode, getClosestNonBucketNode, getEvaluatedItem, getFront, getPathExpression, getParent, getParentFront, getParentGripValue, getNonPrototypeParentGripValue, getNumericalPropertiesCount, getValue, makeNodesForEntries, makeNodesForPromiseProperties, makeNodesForProperties, makeNumericalBuckets, nodeHasAccessors, nodeHasChildren, nodeHasEntries, nodeHasProperties, nodeHasGetter, nodeHasSetter, nodeIsBlock, nodeIsBucket, nodeIsDefaultProperties, nodeIsEntries, nodeIsError, nodeIsLongString, nodeHasFullText, nodeIsFunction, nodeIsGetter, nodeIsMapEntry, nodeIsMissingArguments, nodeIsObject, nodeIsOptimizedOut, nodeIsPrimitive, nodeIsPromise, nodeIsPrototype, nodeIsProxy, nodeIsSetter, nodeIsUninitializedBinding, nodeIsUnmappedBinding, nodeIsUnscopedBinding, nodeIsWindow, nodeNeedsNumericalBuckets, nodeSupportsNumericalBucketing, setNodeChildren, sortProperties, NODE_TYPES, };