/* 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,
};