Skip to content

Commit 33d7a80

Browse files
committed
Preserve property descriptors
This preserves property descriptors of plain object properties using `Object.defineProperties`. For backwards compatibility, it still always uses a value, not a getter.
1 parent e3fb668 commit 33d7a80

File tree

3 files changed

+181
-42
lines changed

3 files changed

+181
-42
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Used as input
2+
// { preserveReferences: true }
3+
export default (() => {
4+
const $1 = {},
5+
$0 = {
6+
string: 'Hello'
7+
}
8+
return ($0['assignment'] = Object.defineProperties($0, {
9+
configurable: {
10+
value: $0,
11+
configurable: true
12+
},
13+
enumerable: {
14+
value: $1,
15+
enumerable: true
16+
},
17+
writable: {
18+
value: $1,
19+
writable: true
20+
}
21+
}))
22+
})()
23+
24+
// -------------------------------------------------------------------------------------------------
25+
26+
// Default output
27+
// { preserveReferences: false }
28+
// Recursive references are not supported without preserveReferences
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Used as input
2+
// { preserveReferences: true }
3+
export default Object.defineProperties(
4+
{
5+
string: 'Hello'
6+
},
7+
{
8+
configurable: {
9+
value: 'Only configurable',
10+
configurable: true
11+
},
12+
enumerable: {
13+
value: 'Only enumerable',
14+
enumerable: true
15+
},
16+
writable: {
17+
value: 'Only writable',
18+
writable: true
19+
}
20+
}
21+
)
22+
23+
// -------------------------------------------------------------------------------------------------
24+
25+
// Default output
26+
// { preserveReferences: false }
27+
const withoutPreserveReferences = Object.defineProperties(
28+
{
29+
string: 'Hello'
30+
},
31+
{
32+
configurable: {
33+
value: 'Only configurable',
34+
configurable: true
35+
},
36+
enumerable: {
37+
value: 'Only enumerable',
38+
enumerable: true
39+
},
40+
writable: {
41+
value: 'Only writable',
42+
writable: true
43+
}
44+
}
45+
)

src/estree-util-value-to-estree.ts

Lines changed: 108 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
type ArrayExpression,
33
type Expression,
44
type Identifier,
5+
type ObjectExpression,
56
type Property,
67
type SimpleLiteral,
78
type VariableDeclarator
@@ -36,14 +37,14 @@ function literal(value: SimpleLiteral['value']): SimpleLiteral {
3637
*
3738
* @param object
3839
* The object to call the method on.
39-
* @param property
40+
* @param name
4041
* The name of the method to call.
4142
* @param args
4243
* Arguments to pass to the function call
4344
* @returns
4445
* The call expression node.
4546
*/
46-
function methodCall(object: Expression, property: string, args: Expression[]): Expression {
47+
function methodCall(object: Expression, name: string, args: Expression[]): Expression {
4748
return {
4849
type: 'CallExpression',
4950
optional: false,
@@ -52,7 +53,7 @@ function methodCall(object: Expression, property: string, args: Expression[]): E
5253
computed: false,
5354
optional: false,
5455
object,
55-
property: identifier(property)
56+
property: identifier(name)
5657
},
5758
arguments: args
5859
}
@@ -284,6 +285,59 @@ function replaceAssignment(expression: Expression, assignment: Expression | unde
284285
return assignment
285286
}
286287

288+
/**
289+
* Create an ESTree epxression to represent a symbol. Global and well-known symbols are supported.
290+
*
291+
* @param symbol
292+
* THe symbol to represent.
293+
* @returns
294+
* An ESTree expression to represent the symbol.
295+
*/
296+
function symbolToEstree(symbol: symbol): Expression {
297+
const name = wellKnownSymbols.get(symbol)
298+
if (name) {
299+
return {
300+
type: 'MemberExpression',
301+
computed: false,
302+
optional: false,
303+
object: identifier('Symbol'),
304+
property: identifier(name)
305+
}
306+
}
307+
308+
if (symbol.description && symbol === Symbol.for(symbol.description)) {
309+
return methodCall(identifier('Symbol'), 'for', [literal(symbol.description)])
310+
}
311+
312+
throw new TypeError(`Only global symbols are supported, got: ${String(symbol)}`, {
313+
cause: symbol
314+
})
315+
}
316+
317+
/**
318+
* Create an ESTree property from a key and a value expression.
319+
*
320+
* @param key
321+
* The property key value
322+
* @param value
323+
* The property value as an ESTree expression.
324+
* @returns
325+
* The ESTree properry node.
326+
*/
327+
function property(key: string | symbol, value: Expression): Property {
328+
const computed = typeof key !== 'string'
329+
330+
return {
331+
type: 'Property',
332+
method: false,
333+
shorthand: false,
334+
computed,
335+
kind: 'init',
336+
key: computed ? symbolToEstree(key) : literal(key),
337+
value
338+
}
339+
}
340+
287341
/**
288342
* Convert a value to an ESTree node.
289343
*
@@ -408,22 +462,7 @@ export function valueToEstree(value: unknown, options: Options = {}): Expression
408462
}
409463

410464
if (typeof val === 'symbol') {
411-
const name = wellKnownSymbols.get(val)
412-
if (name) {
413-
return {
414-
type: 'MemberExpression',
415-
computed: false,
416-
optional: false,
417-
object: identifier('Symbol'),
418-
property: identifier(name)
419-
}
420-
}
421-
422-
if (val.description && val === Symbol.for(val.description)) {
423-
return methodCall(identifier('Symbol'), 'for', [literal(val.description)])
424-
}
425-
426-
throw new TypeError(`Only global symbols are supported, got: ${String(val)}`, { cause: val })
465+
return symbolToEstree(val)
427466
}
428467

429468
const context = collectedContexts.get(val)
@@ -588,24 +627,34 @@ export function valueToEstree(value: unknown, options: Options = {}): Expression
588627

589628
const properties: Property[] = []
590629
if (Object.getPrototypeOf(val) == null) {
591-
properties.push({
592-
type: 'Property',
593-
method: false,
594-
shorthand: false,
595-
computed: false,
596-
kind: 'init',
597-
key: identifier('__proto__'),
598-
value: literal(null)
599-
})
630+
properties.push(property('__proto__', literal(null)))
600631
}
601632

602633
const object = val as Record<string | symbol, unknown>
634+
const propertyDescriptors: Property[] = []
603635
for (const key of Reflect.ownKeys(val)) {
604-
const computed = typeof key !== 'string'
605-
const keyExpression = generate(key)
636+
// TODO [>=4] Throw an error for getters.
606637
const child = object[key]
638+
const { configurable, enumerable, writable } = Object.getOwnPropertyDescriptor(val, key)!
607639
const childContext = collectedContexts.get(child)
608-
if (
640+
if (!configurable || !enumerable || !writable) {
641+
const propertyDescriptor = [property('value', generate(child))]
642+
if (configurable) {
643+
propertyDescriptor.push(property('configurable', literal(true)))
644+
}
645+
if (enumerable) {
646+
propertyDescriptor.push(property('enumerable', literal(true)))
647+
}
648+
if (writable) {
649+
propertyDescriptor.push(property('writable', literal(true)))
650+
}
651+
propertyDescriptors.push(
652+
property(key, {
653+
type: 'ObjectExpression',
654+
properties: propertyDescriptor
655+
})
656+
)
657+
} else if (
609658
context &&
610659
childContext &&
611660
namedContexts.indexOf(childContext) >= namedContexts.indexOf(context)
@@ -618,27 +667,44 @@ export function valueToEstree(value: unknown, options: Options = {}): Expression
618667
computed: true,
619668
optional: false,
620669
object: identifier(context.name!),
621-
property: keyExpression
670+
property: generate(key)
622671
},
623672
right: childContext.assignment || generate(child)
624673
}
625674
} else {
626-
properties.push({
627-
type: 'Property',
628-
method: false,
629-
shorthand: false,
630-
computed,
631-
kind: 'init',
632-
key: keyExpression,
633-
value: generate(child)
634-
})
675+
properties.push(property(key, generate(child)))
635676
}
636677
}
637678

638-
return {
679+
const objectExpression: ObjectExpression = {
639680
type: 'ObjectExpression',
640681
properties
641682
}
683+
684+
if (propertyDescriptors.length) {
685+
if (!context) {
686+
return methodCall(identifier('Object'), 'defineProperties', [
687+
objectExpression,
688+
{
689+
type: 'ObjectExpression',
690+
properties: propertyDescriptors
691+
}
692+
])
693+
}
694+
695+
context.assignment = replaceAssignment(
696+
methodCall(identifier('Object'), 'defineProperties', [
697+
identifier(context.name!),
698+
{
699+
type: 'ObjectExpression',
700+
properties: propertyDescriptors
701+
}
702+
]),
703+
context.assignment
704+
)
705+
}
706+
707+
return objectExpression
642708
}
643709

644710
analyze(value)

0 commit comments

Comments
 (0)