diff options
Diffstat (limited to 'devtools/client/shared/components/object-inspector/utils')
6 files changed, 1504 insertions, 0 deletions
diff --git a/devtools/client/shared/components/object-inspector/utils/client.js b/devtools/client/shared/components/object-inspector/utils/client.js new file mode 100644 index 0000000000..eaa42be05a --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/client.js @@ -0,0 +1,124 @@ +/* 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 <http://mozilla.org/MPL/2.0/>. */ + +const { + getValue, + nodeHasFullText, +} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js"); + +async function enumIndexedProperties(objectFront, start, end) { + try { + const iterator = await objectFront.enumProperties({ + ignoreNonIndexedProperties: true, + }); + const response = await iteratorSlice(iterator, start, end); + return response; + } catch (e) { + console.error("Error in enumIndexedProperties", e); + return {}; + } +} + +async function enumNonIndexedProperties(objectFront, start, end) { + try { + const iterator = await objectFront.enumProperties({ + ignoreIndexedProperties: true, + }); + const response = await iteratorSlice(iterator, start, end); + return response; + } catch (e) { + console.error("Error in enumNonIndexedProperties", e); + return {}; + } +} + +async function enumEntries(objectFront, start, end) { + try { + const iterator = await objectFront.enumEntries(); + const response = await iteratorSlice(iterator, start, end); + return response; + } catch (e) { + console.error("Error in enumEntries", e); + return {}; + } +} + +async function enumSymbols(objectFront, start, end) { + try { + const iterator = await objectFront.enumSymbols(); + const response = await iteratorSlice(iterator, start, end); + return response; + } catch (e) { + console.error("Error in enumSymbols", e); + return {}; + } +} + +async function enumPrivateProperties(objectFront, start, end) { + try { + const iterator = await objectFront.enumPrivateProperties(); + const response = await iteratorSlice(iterator, start, end); + return response; + } catch (e) { + console.error("Error in enumPrivateProperties", e); + return {}; + } +} + +async function getPrototype(objectFront) { + if (typeof objectFront.getPrototype !== "function") { + console.error("objectFront.getPrototype is not a function"); + return Promise.resolve({}); + } + return objectFront.getPrototype(); +} + +async function getFullText(longStringFront, item) { + const { initial, fullText, length } = getValue(item); + // Return fullText property if it exists so that it can be added to the + // loadedProperties map. + if (nodeHasFullText(item)) { + return { fullText }; + } + + try { + const substring = await longStringFront.substring(initial.length, length); + return { + fullText: initial + substring, + }; + } catch (e) { + console.error("LongStringFront.substring", e); + throw e; + } +} + +async function getPromiseState(objectFront) { + return objectFront.getPromiseState(); +} + +async function getProxySlots(objectFront) { + return objectFront.getProxySlots(); +} + +function iteratorSlice(iterator, start, end) { + start = start || 0; + const count = end ? end - start + 1 : iterator.count; + + if (count === 0) { + return Promise.resolve({}); + } + return iterator.slice(start, count); +} + +module.exports = { + enumEntries, + enumIndexedProperties, + enumNonIndexedProperties, + enumPrivateProperties, + enumSymbols, + getPrototype, + getFullText, + getPromiseState, + getProxySlots, +}; diff --git a/devtools/client/shared/components/object-inspector/utils/index.js b/devtools/client/shared/components/object-inspector/utils/index.js new file mode 100644 index 0000000000..13b3fd0049 --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/index.js @@ -0,0 +1,52 @@ +/* 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 <http://mozilla.org/MPL/2.0/>. */ + +const client = require("resource://devtools/client/shared/components/object-inspector/utils/client.js"); +const loadProperties = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js"); +const node = require("resource://devtools/client/shared/components/object-inspector/utils/node.js"); +const { nodeIsError, nodeIsPrimitive } = node; +const selection = require("resource://devtools/client/shared/components/object-inspector/utils/selection.js"); + +const { + MODE, +} = require("resource://devtools/client/shared/components/reps/reps/constants.js"); +const { + REPS: { Rep, Grip }, +} = require("resource://devtools/client/shared/components/reps/reps/rep.js"); + +function shouldRenderRootsInReps(roots, props = {}) { + if (roots.length !== 1) { + return false; + } + + const root = roots[0]; + const name = root && root.name; + + return ( + (name === null || typeof name === "undefined") && + (nodeIsPrimitive(root) || + (root?.contents?.value?.useCustomFormatter === true && + Array.isArray(root?.contents?.value?.header)) || + (nodeIsError(root) && props?.customFormat === true)) + ); +} + +function renderRep(item, props) { + return Rep({ + ...props, + front: item.contents.front, + object: node.getValue(item), + mode: props.mode || MODE.TINY, + defaultRep: Grip, + }); +} + +module.exports = { + client, + loadProperties, + node, + renderRep, + selection, + shouldRenderRootsInReps, +}; diff --git a/devtools/client/shared/components/object-inspector/utils/load-properties.js b/devtools/client/shared/components/object-inspector/utils/load-properties.js new file mode 100644 index 0000000000..42525e54f1 --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/load-properties.js @@ -0,0 +1,260 @@ +/* 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 <http://mozilla.org/MPL/2.0/>. */ + +const { + enumEntries, + enumIndexedProperties, + enumNonIndexedProperties, + enumPrivateProperties, + enumSymbols, + getPrototype, + getFullText, + getPromiseState, + getProxySlots, +} = require("resource://devtools/client/shared/components/object-inspector/utils/client.js"); + +const { + getClosestGripNode, + getClosestNonBucketNode, + getFront, + getValue, + nodeHasAccessors, + nodeHasProperties, + nodeIsBucket, + nodeIsDefaultProperties, + nodeIsEntries, + nodeIsMapEntry, + nodeIsPrimitive, + nodeIsPromise, + nodeIsProxy, + nodeNeedsNumericalBuckets, + nodeIsLongString, +} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js"); + +function loadItemProperties(item, client, loadedProperties, threadActorID) { + const gripItem = getClosestGripNode(item); + const value = getValue(gripItem); + let front = getFront(gripItem); + + if (!front && value && client && client.getFrontByID) { + front = client.getFrontByID(value.actor); + } + + const getObjectFront = function() { + if (!front) { + front = client.createObjectFront( + value, + client.getFrontByID(threadActorID) + ); + } + + return front; + }; + + const [start, end] = item.meta + ? [item.meta.startIndex, item.meta.endIndex] + : []; + + const promises = []; + + if (shouldLoadItemIndexedProperties(item, loadedProperties)) { + promises.push(enumIndexedProperties(getObjectFront(), start, end)); + } + + if (shouldLoadItemNonIndexedProperties(item, loadedProperties)) { + promises.push(enumNonIndexedProperties(getObjectFront(), start, end)); + } + + if (shouldLoadItemEntries(item, loadedProperties)) { + promises.push(enumEntries(getObjectFront(), start, end)); + } + + if (shouldLoadItemPrototype(item, loadedProperties)) { + promises.push(getPrototype(getObjectFront())); + } + + if (shouldLoadItemPrivateProperties(item, loadedProperties)) { + promises.push(enumPrivateProperties(getObjectFront(), start, end)); + } + + if (shouldLoadItemSymbols(item, loadedProperties)) { + promises.push(enumSymbols(getObjectFront(), start, end)); + } + + if (shouldLoadItemFullText(item, loadedProperties)) { + const longStringFront = front || client.createLongStringFront(value); + promises.push(getFullText(longStringFront, item)); + } + + if (shouldLoadItemPromiseState(item, loadedProperties)) { + promises.push(getPromiseState(getObjectFront())); + } + + if (shouldLoadItemProxySlots(item, loadedProperties)) { + promises.push(getProxySlots(getObjectFront())); + } + + return Promise.all(promises).then(mergeResponses); +} + +function mergeResponses(responses) { + const data = {}; + + for (const response of responses) { + if (response.hasOwnProperty("ownProperties")) { + data.ownProperties = { ...data.ownProperties, ...response.ownProperties }; + } + + if (response.privateProperties && response.privateProperties.length > 0) { + data.privateProperties = response.privateProperties; + } + + if (response.ownSymbols && response.ownSymbols.length > 0) { + data.ownSymbols = response.ownSymbols; + } + + if (response.prototype) { + data.prototype = response.prototype; + } + + if (response.fullText) { + data.fullText = response.fullText; + } + + if (response.promiseState) { + data.promiseState = response.promiseState; + } + + if (response.proxyTarget && response.proxyHandler) { + data.proxyTarget = response.proxyTarget; + data.proxyHandler = response.proxyHandler; + } + } + + return data; +} + +function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) { + const gripItem = getClosestGripNode(item); + const value = getValue(gripItem); + + return ( + value && + nodeHasProperties(gripItem) && + !loadedProperties.has(item.path) && + !nodeIsProxy(item) && + !nodeNeedsNumericalBuckets(item) && + !nodeIsEntries(getClosestNonBucketNode(item)) && + // The data is loaded when expanding the window node. + !nodeIsDefaultProperties(item) + ); +} + +function shouldLoadItemNonIndexedProperties( + item, + loadedProperties = new Map() +) { + const gripItem = getClosestGripNode(item); + const value = getValue(gripItem); + + return ( + value && + nodeHasProperties(gripItem) && + !loadedProperties.has(item.path) && + !nodeIsProxy(item) && + !nodeIsEntries(getClosestNonBucketNode(item)) && + !nodeIsBucket(item) && + // The data is loaded when expanding the window node. + !nodeIsDefaultProperties(item) + ); +} + +function shouldLoadItemEntries(item, loadedProperties = new Map()) { + const gripItem = getClosestGripNode(item); + const value = getValue(gripItem); + + return ( + value && + nodeIsEntries(getClosestNonBucketNode(item)) && + !loadedProperties.has(item.path) && + !nodeNeedsNumericalBuckets(item) + ); +} + +function shouldLoadItemPrototype(item, loadedProperties = new Map()) { + const value = getValue(item); + + return ( + value && + !loadedProperties.has(item.path) && + !nodeIsBucket(item) && + !nodeIsMapEntry(item) && + !nodeIsEntries(item) && + !nodeIsDefaultProperties(item) && + !nodeHasAccessors(item) && + !nodeIsPrimitive(item) && + !nodeIsLongString(item) && + !nodeIsProxy(item) + ); +} + +function shouldLoadItemSymbols(item, loadedProperties = new Map()) { + const value = getValue(item); + + return ( + value && + !loadedProperties.has(item.path) && + !nodeIsBucket(item) && + !nodeIsMapEntry(item) && + !nodeIsEntries(item) && + !nodeIsDefaultProperties(item) && + !nodeHasAccessors(item) && + !nodeIsPrimitive(item) && + !nodeIsLongString(item) && + !nodeIsProxy(item) + ); +} + +function shouldLoadItemPrivateProperties(item, loadedProperties = new Map()) { + const value = getValue(item); + + return ( + value && + value?.preview?.privatePropertiesLength && + !loadedProperties.has(item.path) && + !nodeIsBucket(item) && + !nodeIsMapEntry(item) && + !nodeIsEntries(item) && + !nodeIsDefaultProperties(item) && + !nodeHasAccessors(item) && + !nodeIsPrimitive(item) && + !nodeIsLongString(item) && + !nodeIsProxy(item) + ); +} + +function shouldLoadItemFullText(item, loadedProperties = new Map()) { + return !loadedProperties.has(item.path) && nodeIsLongString(item); +} + +function shouldLoadItemPromiseState(item, loadedProperties = new Map()) { + return !loadedProperties.has(item.path) && nodeIsPromise(item); +} + +function shouldLoadItemProxySlots(item, loadedProperties = new Map()) { + return !loadedProperties.has(item.path) && nodeIsProxy(item); +} + +module.exports = { + loadItemProperties, + mergeResponses, + shouldLoadItemEntries, + shouldLoadItemIndexedProperties, + shouldLoadItemNonIndexedProperties, + shouldLoadItemPrototype, + shouldLoadItemSymbols, + shouldLoadItemFullText, + shouldLoadItemPromiseState, + shouldLoadItemProxySlots, +}; diff --git a/devtools/client/shared/components/object-inspector/utils/moz.build b/devtools/client/shared/components/object-inspector/utils/moz.build new file mode 100644 index 0000000000..1301b2aca6 --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "client.js", + "index.js", + "load-properties.js", + "node.js", + "selection.js", +) diff --git a/devtools/client/shared/components/object-inspector/utils/node.js b/devtools/client/shared/components/object-inspector/utils/node.js new file mode 100644 index 0000000000..7b4d1fb0ce --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/node.js @@ -0,0 +1,1039 @@ +/* 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 <http://mozilla.org/MPL/2.0/>. */ + +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("<default properties>"), + ENTRIES: Symbol("<entries>"), + GET: Symbol("<get>"), + GRIP: Symbol("GRIP"), + MAP_ENTRY_KEY: Symbol("<key>"), + MAP_ENTRY_VALUE: Symbol("<value>"), + PROMISE_REASON: Symbol("<reason>"), + PROMISE_STATE: Symbol("<state>"), + PROMISE_VALUE: Symbol("<value>"), + PROXY_HANDLER: Symbol("<handler>"), + PROXY_TARGET: Symbol("<target>"), + SET: Symbol("<set>"), + PROTOTYPE: Symbol("<prototype>"), + 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 <entries> 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: "<state>", + contents: { value: state }, + type: NODE_TYPES.PROMISE_STATE, + }) + ); + } + + if (reason) { + properties.push( + createNode({ + parent: item, + name: "<reason>", + 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: "<value>", + 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: "<target>", + contents: { value: proxyTargetGrip, front: proxyTargetFront }, + type: NODE_TYPES.PROXY_TARGET, + }), + createNode({ + parent: item, + name: "<handler>", + contents: { value: proxyHandlerGrip, front: proxyHandlerFront }, + type: NODE_TYPES.PROXY_HANDLER, + }), + ]; +} + +function makeNodesForEntries(item) { + const nodeName = "<entries>"; + + 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: "<key>", + contents: { value: keyGrip, front: keyFront }, + type: NODE_TYPES.MAP_ENTRY_KEY, + }), + createNode({ + parent: item, + name: "<value>", + 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: "<default properties>", + 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: "<prototype>", + 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: `<get ${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: `<set ${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, +}; diff --git a/devtools/client/shared/components/object-inspector/utils/selection.js b/devtools/client/shared/components/object-inspector/utils/selection.js new file mode 100644 index 0000000000..fdcca7ff6b --- /dev/null +++ b/devtools/client/shared/components/object-inspector/utils/selection.js @@ -0,0 +1,16 @@ +/* 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 <http://mozilla.org/MPL/2.0/>. */ + +function documentHasSelection(doc = document) { + const selection = doc.defaultView.getSelection(); + if (!selection) { + return false; + } + + return selection.type === "Range"; +} + +module.exports = { + documentHasSelection, +}; |