672 lines
22 KiB
JavaScript
672 lines
22 KiB
JavaScript
/**
|
||
* Create an ESTree identifier node for a given name.
|
||
*
|
||
* @param name
|
||
* The name of the identifier.
|
||
* @returns
|
||
* The identifier node.
|
||
*/
|
||
function identifier(name) {
|
||
return { type: 'Identifier', name };
|
||
}
|
||
/**
|
||
* Create an ESTree literal node for a given value.
|
||
*
|
||
* @param value
|
||
* The value for which to create a literal.
|
||
* @returns
|
||
* The literal node.
|
||
*/
|
||
function literal(value) {
|
||
return { type: 'Literal', value };
|
||
}
|
||
/**
|
||
* Create an ESTree call expression on an object member.
|
||
*
|
||
* @param object
|
||
* The object to call the method on.
|
||
* @param name
|
||
* The name of the method to call.
|
||
* @param args
|
||
* Arguments to pass to the function call
|
||
* @returns
|
||
* The call expression node.
|
||
*/
|
||
function methodCall(object, name, args) {
|
||
return {
|
||
type: 'CallExpression',
|
||
optional: false,
|
||
callee: {
|
||
type: 'MemberExpression',
|
||
computed: false,
|
||
optional: false,
|
||
object,
|
||
property: identifier(name)
|
||
},
|
||
arguments: args
|
||
};
|
||
}
|
||
/**
|
||
* Turn a number or bigint into an ESTree expression. This handles positive and negative numbers and
|
||
* bigints as well as special numbers.
|
||
*
|
||
* @param number
|
||
* The value to turn into an ESTree expression.
|
||
* @returns
|
||
* An expression that represents the given value.
|
||
*/
|
||
function processNumber(number) {
|
||
if (number < 0 || Object.is(number, -0)) {
|
||
return {
|
||
type: 'UnaryExpression',
|
||
operator: '-',
|
||
prefix: true,
|
||
argument: processNumber(-number)
|
||
};
|
||
}
|
||
if (typeof number === 'bigint') {
|
||
return { type: 'Literal', bigint: String(number) };
|
||
}
|
||
if (number === Number.POSITIVE_INFINITY || Number.isNaN(number)) {
|
||
return identifier(String(number));
|
||
}
|
||
return literal(number);
|
||
}
|
||
/**
|
||
* Process an array of numbers. This is a shortcut for iterables whose constructor takes an array of
|
||
* numbers as input.
|
||
*
|
||
* @param numbers
|
||
* The numbers to add to the array expression.
|
||
* @returns
|
||
* An ESTree array expression whose elements match the input numbers.
|
||
*/
|
||
function processNumberArray(numbers) {
|
||
return { type: 'ArrayExpression', elements: Array.from(numbers, processNumber) };
|
||
}
|
||
/**
|
||
* Check whether a value can be constructed from its string representation.
|
||
*
|
||
* @param value
|
||
* The value to check
|
||
* @returns
|
||
* Whether or not the value can be constructed from its string representation.
|
||
*/
|
||
function isStringReconstructable(value) {
|
||
return value instanceof URL || value instanceof URLSearchParams;
|
||
}
|
||
/**
|
||
* Check whether a value can be constructed from its `valueOf()` result.
|
||
*
|
||
* @param value
|
||
* The value to check
|
||
* @returns
|
||
* Whether or not the value can be constructed from its `valueOf()` result.
|
||
*/
|
||
function isValueReconstructable(value) {
|
||
return (value instanceof Boolean ||
|
||
value instanceof Date ||
|
||
value instanceof Number ||
|
||
value instanceof String);
|
||
}
|
||
const wellKnownSymbols = new Map();
|
||
for (const name of Reflect.ownKeys(Symbol)) {
|
||
const value = Symbol[name];
|
||
if (typeof value === 'symbol') {
|
||
wellKnownSymbols.set(value, name);
|
||
}
|
||
}
|
||
/**
|
||
* Check whether a value is a Temporal value.
|
||
*
|
||
* @param value
|
||
* The value to check
|
||
* @returns
|
||
* Whether or not the value is a Temporal value.
|
||
*/
|
||
function isTemporal(value) {
|
||
return (typeof Temporal !== 'undefined' &&
|
||
(value instanceof Temporal.Duration ||
|
||
value instanceof Temporal.Instant ||
|
||
value instanceof Temporal.PlainDate ||
|
||
value instanceof Temporal.PlainDateTime ||
|
||
value instanceof Temporal.PlainYearMonth ||
|
||
value instanceof Temporal.PlainMonthDay ||
|
||
value instanceof Temporal.PlainTime ||
|
||
value instanceof Temporal.ZonedDateTime));
|
||
}
|
||
/**
|
||
* Check whether a value is a typed array.
|
||
*
|
||
* @param value
|
||
* The value to check
|
||
* @returns
|
||
* Whether or not the value is a typed array.
|
||
*/
|
||
function isTypedArray(value) {
|
||
return (value instanceof BigInt64Array ||
|
||
value instanceof BigUint64Array ||
|
||
(typeof Float16Array !== 'undefined' && value instanceof Float16Array) ||
|
||
value instanceof Float32Array ||
|
||
value instanceof Float64Array ||
|
||
value instanceof Int8Array ||
|
||
value instanceof Int16Array ||
|
||
value instanceof Int32Array ||
|
||
value instanceof Uint8Array ||
|
||
value instanceof Uint8ClampedArray ||
|
||
value instanceof Uint16Array ||
|
||
value instanceof Uint32Array);
|
||
}
|
||
/**
|
||
* Compare two value contexts for sorting them based on reference count.
|
||
*
|
||
* @param a
|
||
* The first context to compare.
|
||
* @param b
|
||
* The second context to compare.
|
||
* @returns
|
||
* The count of context a minus the count of context b.
|
||
*/
|
||
function compareContexts(a, b) {
|
||
const aReferencedByB = a.referencedBy.has(b.value);
|
||
const bReferencedByA = b.referencedBy.has(a.value);
|
||
if (aReferencedByB) {
|
||
if (bReferencedByA) {
|
||
return a.count - b.count;
|
||
}
|
||
return -1;
|
||
}
|
||
if (bReferencedByA) {
|
||
return 1;
|
||
}
|
||
return a.count - b.count;
|
||
}
|
||
/**
|
||
* Replace the assigned right hand expression with the new expression.
|
||
*
|
||
* If there is no assignment expression, the original expression is returned. Otherwise the
|
||
* assignment is modified and returned.
|
||
*
|
||
* @param expression
|
||
* The expression to use for the assignment.
|
||
* @param assignment
|
||
* The existing assignmentexpression
|
||
* @returns
|
||
* The new expression.
|
||
*/
|
||
function replaceAssignment(expression, assignment) {
|
||
if (!assignment || assignment.type !== 'AssignmentExpression') {
|
||
return expression;
|
||
}
|
||
let node = assignment;
|
||
while (node.right.type === 'AssignmentExpression') {
|
||
node = node.right;
|
||
}
|
||
node.right = expression;
|
||
return assignment;
|
||
}
|
||
/**
|
||
* Create an ESTree epxression to represent a symbol. Global and well-known symbols are supported.
|
||
*
|
||
* @param symbol
|
||
* The symbol to represent.
|
||
* @returns
|
||
* An ESTree expression to represent the symbol.
|
||
*/
|
||
function symbolToEstree(symbol) {
|
||
const name = wellKnownSymbols.get(symbol);
|
||
if (name) {
|
||
return {
|
||
type: 'MemberExpression',
|
||
computed: false,
|
||
optional: false,
|
||
object: identifier('Symbol'),
|
||
property: identifier(name)
|
||
};
|
||
}
|
||
if (symbol.description && symbol === Symbol.for(symbol.description)) {
|
||
return methodCall(identifier('Symbol'), 'for', [literal(symbol.description)]);
|
||
}
|
||
throw new TypeError(`Only global symbols are supported, got: ${String(symbol)}`, {
|
||
cause: symbol
|
||
});
|
||
}
|
||
/**
|
||
* Create an ESTree property from a key and a value expression.
|
||
*
|
||
* @param key
|
||
* The property key value
|
||
* @param value
|
||
* The property value as an ESTree expression.
|
||
* @returns
|
||
* The ESTree properry node.
|
||
*/
|
||
function property(key, value) {
|
||
const isString = typeof key === 'string';
|
||
return {
|
||
type: 'Property',
|
||
method: false,
|
||
shorthand: false,
|
||
computed: key === '__proto__' || !isString,
|
||
kind: 'init',
|
||
key: isString ? literal(key) : symbolToEstree(key),
|
||
value
|
||
};
|
||
}
|
||
/**
|
||
* Convert a value to an ESTree node.
|
||
*
|
||
* @param value
|
||
* The value to convert.
|
||
* @param options
|
||
* Additional options to configure the output.
|
||
* @returns
|
||
* The ESTree node.
|
||
*/
|
||
export function valueToEstree(value, options = {}) {
|
||
const stack = [];
|
||
const collectedContexts = new Map();
|
||
const namedContexts = [];
|
||
const customTrees = new Map();
|
||
/**
|
||
* Analyze a value and collect all reference contexts.
|
||
*
|
||
* @param val
|
||
* The value to analyze.
|
||
*/
|
||
function analyze(val) {
|
||
if (typeof val !== 'object' && typeof val !== 'function') {
|
||
return;
|
||
}
|
||
if (val == null) {
|
||
return;
|
||
}
|
||
const context = collectedContexts.get(val);
|
||
if (context) {
|
||
if (options.preserveReferences) {
|
||
context.count += 1;
|
||
}
|
||
for (const ancestor of stack) {
|
||
context.referencedBy.add(ancestor);
|
||
}
|
||
if (stack.includes(val)) {
|
||
if (!options.preserveReferences) {
|
||
throw new Error(`Found circular reference: ${val}`, { cause: val });
|
||
}
|
||
const parent = stack.at(-1);
|
||
const parentContext = collectedContexts.get(parent);
|
||
parentContext.recursive = true;
|
||
context.recursive = true;
|
||
}
|
||
return;
|
||
}
|
||
collectedContexts.set(val, {
|
||
count: 1,
|
||
recursive: false,
|
||
referencedBy: new Set(stack),
|
||
value: val
|
||
});
|
||
const estree = options?.replacer?.(val);
|
||
if (estree) {
|
||
customTrees.set(val, estree);
|
||
return;
|
||
}
|
||
if (typeof val === 'function') {
|
||
throw new TypeError(`Unsupported value: ${val}`, { cause: val });
|
||
}
|
||
if (isTypedArray(val)) {
|
||
return;
|
||
}
|
||
if (isStringReconstructable(val)) {
|
||
return;
|
||
}
|
||
if (isValueReconstructable(val)) {
|
||
return;
|
||
}
|
||
if (value instanceof RegExp) {
|
||
return;
|
||
}
|
||
if (isTemporal(value)) {
|
||
return;
|
||
}
|
||
stack.push(val);
|
||
if (val instanceof Map) {
|
||
for (const pair of val) {
|
||
analyze(pair[0]);
|
||
analyze(pair[1]);
|
||
}
|
||
}
|
||
else if (Array.isArray(val) || val instanceof Set) {
|
||
for (const entry of val) {
|
||
analyze(entry);
|
||
}
|
||
}
|
||
else {
|
||
const proto = Object.getPrototypeOf(val);
|
||
if (proto != null && proto !== Object.prototype && !options.instanceAsObject) {
|
||
throw new TypeError(`Unsupported value: ${val}`, { cause: val });
|
||
}
|
||
for (const key of Reflect.ownKeys(val)) {
|
||
analyze(val[key]);
|
||
}
|
||
}
|
||
stack.pop();
|
||
}
|
||
/**
|
||
* Recursively generate the ESTree expression needed to reconstruct the value.
|
||
*
|
||
* @param val
|
||
* The value to process.
|
||
* @param isDeclaration
|
||
* Whether or not this is for a variable declaration.
|
||
* @returns
|
||
* The ESTree expression to reconstruct the value.
|
||
*/
|
||
function generate(val, isDeclaration) {
|
||
if (val === undefined) {
|
||
return identifier(String(val));
|
||
}
|
||
if (val == null || typeof val === 'string' || typeof val === 'boolean') {
|
||
return literal(val);
|
||
}
|
||
if (typeof val === 'bigint' || typeof val === 'number') {
|
||
return processNumber(val);
|
||
}
|
||
if (typeof val === 'symbol') {
|
||
return symbolToEstree(val);
|
||
}
|
||
const context = collectedContexts.get(val);
|
||
if (!isDeclaration && context?.name) {
|
||
return identifier(context.name);
|
||
}
|
||
const tree = customTrees.get(val);
|
||
if (tree) {
|
||
return tree;
|
||
}
|
||
if (isValueReconstructable(val)) {
|
||
return {
|
||
type: 'NewExpression',
|
||
callee: identifier(val.constructor.name),
|
||
arguments: [generate(val.valueOf())]
|
||
};
|
||
}
|
||
if (val instanceof RegExp) {
|
||
return {
|
||
type: 'Literal',
|
||
regex: { pattern: val.source, flags: val.flags }
|
||
};
|
||
}
|
||
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) {
|
||
return methodCall(identifier('Buffer'), 'from', [processNumberArray(val)]);
|
||
}
|
||
if (isTypedArray(val)) {
|
||
return {
|
||
type: 'NewExpression',
|
||
callee: identifier(val.constructor.name),
|
||
arguments: [processNumberArray(val)]
|
||
};
|
||
}
|
||
if (isStringReconstructable(val)) {
|
||
return {
|
||
type: 'NewExpression',
|
||
callee: identifier(val.constructor.name),
|
||
arguments: [literal(String(val))]
|
||
};
|
||
}
|
||
if (isTemporal(val)) {
|
||
return methodCall({
|
||
type: 'MemberExpression',
|
||
computed: false,
|
||
optional: false,
|
||
object: identifier('Temporal'),
|
||
property: identifier(val.constructor.name)
|
||
}, 'from', [literal(String(val))]);
|
||
}
|
||
if (Array.isArray(val)) {
|
||
const elements = Array.from({ length: val.length });
|
||
let trimmable;
|
||
for (let index = 0; index < val.length; index += 1) {
|
||
if (!(index in val)) {
|
||
elements[index] = null;
|
||
trimmable = undefined;
|
||
continue;
|
||
}
|
||
const child = val[index];
|
||
const childContext = collectedContexts.get(child);
|
||
if (context &&
|
||
childContext &&
|
||
namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) {
|
||
elements[index] = null;
|
||
trimmable ||= index;
|
||
childContext.assignment = {
|
||
type: 'AssignmentExpression',
|
||
operator: '=',
|
||
left: {
|
||
type: 'MemberExpression',
|
||
computed: true,
|
||
optional: false,
|
||
object: identifier(context.name),
|
||
property: literal(index)
|
||
},
|
||
right: childContext.assignment || identifier(childContext.name)
|
||
};
|
||
}
|
||
else {
|
||
elements[index] = generate(child);
|
||
trimmable = undefined;
|
||
}
|
||
}
|
||
if (trimmable != null) {
|
||
elements.splice(trimmable);
|
||
}
|
||
return {
|
||
type: 'ArrayExpression',
|
||
elements
|
||
};
|
||
}
|
||
if (val instanceof Set) {
|
||
const elements = [];
|
||
let finalizer;
|
||
for (const child of val) {
|
||
if (finalizer) {
|
||
finalizer = methodCall(finalizer, 'add', [generate(child)]);
|
||
}
|
||
else {
|
||
const childContext = collectedContexts.get(child);
|
||
if (context &&
|
||
childContext &&
|
||
namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) {
|
||
finalizer = methodCall(identifier(context.name), 'add', [generate(child)]);
|
||
}
|
||
else {
|
||
elements.push(generate(child));
|
||
}
|
||
}
|
||
}
|
||
if (context && finalizer) {
|
||
context.assignment = replaceAssignment(finalizer, context.assignment);
|
||
}
|
||
return {
|
||
type: 'NewExpression',
|
||
callee: identifier('Set'),
|
||
arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : []
|
||
};
|
||
}
|
||
if (val instanceof Map) {
|
||
const elements = [];
|
||
let finalizer;
|
||
for (const [key, item] of val) {
|
||
if (finalizer) {
|
||
finalizer = methodCall(finalizer, 'set', [generate(key), generate(item)]);
|
||
}
|
||
else {
|
||
const keyContext = collectedContexts.get(key);
|
||
const itemContext = collectedContexts.get(item);
|
||
if (context &&
|
||
((keyContext && namedContexts.indexOf(keyContext) >= namedContexts.indexOf(context)) ||
|
||
(itemContext && namedContexts.indexOf(itemContext) >= namedContexts.indexOf(context)))) {
|
||
finalizer = methodCall(identifier(context.name), 'set', [
|
||
generate(key),
|
||
generate(item)
|
||
]);
|
||
}
|
||
else {
|
||
elements.push({
|
||
type: 'ArrayExpression',
|
||
elements: [generate(key), generate(item)]
|
||
});
|
||
}
|
||
}
|
||
}
|
||
if (context && finalizer) {
|
||
context.assignment = replaceAssignment(finalizer, context.assignment);
|
||
}
|
||
return {
|
||
type: 'NewExpression',
|
||
callee: identifier('Map'),
|
||
arguments: elements.length ? [{ type: 'ArrayExpression', elements }] : []
|
||
};
|
||
}
|
||
const properties = [];
|
||
if (Object.getPrototypeOf(val) == null) {
|
||
properties.push({
|
||
type: 'Property',
|
||
method: false,
|
||
shorthand: false,
|
||
computed: false,
|
||
kind: 'init',
|
||
key: identifier('__proto__'),
|
||
value: literal(null)
|
||
});
|
||
}
|
||
const object = val;
|
||
const propertyDescriptors = [];
|
||
for (const key of Reflect.ownKeys(val)) {
|
||
// TODO [>=4] Throw an error for getters.
|
||
const child = object[key];
|
||
const { configurable, enumerable, writable } = Object.getOwnPropertyDescriptor(val, key);
|
||
const childContext = collectedContexts.get(child);
|
||
if (!configurable || !enumerable || !writable) {
|
||
const propertyDescriptor = [property('value', generate(child))];
|
||
if (configurable) {
|
||
propertyDescriptor.push(property('configurable', literal(true)));
|
||
}
|
||
if (enumerable) {
|
||
propertyDescriptor.push(property('enumerable', literal(true)));
|
||
}
|
||
if (writable) {
|
||
propertyDescriptor.push(property('writable', literal(true)));
|
||
}
|
||
propertyDescriptors.push([
|
||
key,
|
||
{ type: 'ObjectExpression', properties: propertyDescriptor }
|
||
]);
|
||
}
|
||
else if (context &&
|
||
childContext &&
|
||
namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)) {
|
||
if (key === '__proto__') {
|
||
propertyDescriptors.push([
|
||
key,
|
||
{
|
||
type: 'ObjectExpression',
|
||
properties: [
|
||
property('value', generate(child)),
|
||
property('configurable', literal(true)),
|
||
property('enumerable', literal(true)),
|
||
property('writable', literal(true))
|
||
]
|
||
}
|
||
]);
|
||
}
|
||
else {
|
||
childContext.assignment = {
|
||
type: 'AssignmentExpression',
|
||
operator: '=',
|
||
left: {
|
||
type: 'MemberExpression',
|
||
computed: true,
|
||
optional: false,
|
||
object: identifier(context.name),
|
||
property: generate(key)
|
||
},
|
||
right: childContext.assignment || generate(child)
|
||
};
|
||
}
|
||
}
|
||
else {
|
||
properties.push(property(key, generate(child)));
|
||
}
|
||
}
|
||
const objectExpression = {
|
||
type: 'ObjectExpression',
|
||
properties
|
||
};
|
||
if (propertyDescriptors.length) {
|
||
let name;
|
||
let args;
|
||
if (propertyDescriptors.length === 1) {
|
||
const [[key, expression]] = propertyDescriptors;
|
||
name = 'defineProperty';
|
||
args = [typeof key === 'string' ? literal(key) : symbolToEstree(key), expression];
|
||
}
|
||
else {
|
||
name = 'defineProperties';
|
||
args = [
|
||
{
|
||
type: 'ObjectExpression',
|
||
properties: propertyDescriptors.map(([key, expression]) => property(key, expression))
|
||
}
|
||
];
|
||
}
|
||
if (!context) {
|
||
return methodCall(identifier('Object'), name, [objectExpression, ...args]);
|
||
}
|
||
context.assignment = replaceAssignment(methodCall(identifier('Object'), name, [identifier(context.name), ...args]), context.assignment);
|
||
}
|
||
return objectExpression;
|
||
}
|
||
analyze(value);
|
||
for (const [val, context] of collectedContexts) {
|
||
if (context.recursive || context.count > 1) {
|
||
// Assign reused or recursive references to a variable.
|
||
context.name = `$${namedContexts.length}`;
|
||
namedContexts.push(context);
|
||
}
|
||
else {
|
||
// Otherwise don’t treat it as a reference.
|
||
collectedContexts.delete(val);
|
||
}
|
||
}
|
||
if (!namedContexts.length) {
|
||
return generate(value);
|
||
}
|
||
const params = namedContexts.sort(compareContexts).map((context) => ({
|
||
type: 'AssignmentPattern',
|
||
left: identifier(context.name),
|
||
right: generate(context.value, true)
|
||
}));
|
||
const rootContext = collectedContexts.get(value);
|
||
const finalizers = [];
|
||
for (const context of collectedContexts.values()) {
|
||
if (context !== rootContext && context.assignment) {
|
||
finalizers.push(context.assignment);
|
||
}
|
||
}
|
||
finalizers.push(rootContext ? rootContext.assignment || identifier(rootContext.name) : generate(value));
|
||
return {
|
||
type: 'CallExpression',
|
||
optional: false,
|
||
arguments: [],
|
||
callee: {
|
||
type: 'ArrowFunctionExpression',
|
||
expression: false,
|
||
params,
|
||
body: {
|
||
type: 'SequenceExpression',
|
||
expressions: finalizers
|
||
}
|
||
}
|
||
};
|
||
}
|
||
//# sourceMappingURL=estree-util-value-to-estree.js.map
|