Skip to content

Commit bcbe1d7

Browse files
authored
Merge pull request microsoft#41094 from microsoft/destructuringEvaluationOrder
Fix destructuring evaluation order for initializers
2 parents eb6ddf6 + 2b7e790 commit bcbe1d7

File tree

69 files changed

+6123
-5076
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+6123
-5076
lines changed

src/compiler/binder.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ namespace ts {
218218
// or if compiler options contain alwaysStrict.
219219
let inStrictMode: boolean;
220220

221+
// If we are binding an assignment pattern, we will bind certain expressions differently.
222+
let inAssignmentPattern = false;
223+
221224
let symbolCount = 0;
222225

223226
let Symbol: new (flags: SymbolFlags, name: __String) => Symbol;
@@ -275,6 +278,7 @@ namespace ts {
275278
currentExceptionTarget = undefined;
276279
activeLabelList = undefined;
277280
hasExplicitReturn = false;
281+
inAssignmentPattern = false;
278282
emitFlags = NodeFlags.None;
279283
}
280284

@@ -733,9 +737,14 @@ namespace ts {
733737
}
734738

735739
function bindChildren(node: Node): void {
740+
const saveInAssignmentPattern = inAssignmentPattern;
741+
// Most nodes aren't valid in an assignment pattern, so we clear the value here
742+
// and set it before we descend into nodes that could actually be part of an assignment pattern.
743+
inAssignmentPattern = false;
736744
if (checkUnreachable(node)) {
737745
bindEachChild(node);
738746
bindJSDoc(node);
747+
inAssignmentPattern = saveInAssignmentPattern;
739748
return;
740749
}
741750
if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && !options.allowUnreachableCode) {
@@ -791,6 +800,13 @@ namespace ts {
791800
bindPostfixUnaryExpressionFlow(<PostfixUnaryExpression>node);
792801
break;
793802
case SyntaxKind.BinaryExpression:
803+
if (isDestructuringAssignment(node)) {
804+
// Carry over whether we are in an assignment pattern to
805+
// binary expressions that could actually be an initializer
806+
inAssignmentPattern = saveInAssignmentPattern;
807+
bindDestructuringAssignmentFlow(node);
808+
return;
809+
}
794810
bindBinaryExpressionFlow(<BinaryExpression>node);
795811
break;
796812
case SyntaxKind.DeleteExpression:
@@ -827,11 +843,23 @@ namespace ts {
827843
case SyntaxKind.ModuleBlock:
828844
bindEachFunctionsFirst((node as Block).statements);
829845
break;
846+
case SyntaxKind.BindingElement:
847+
bindBindingElementFlow(<BindingElement>node);
848+
break;
849+
case SyntaxKind.ObjectLiteralExpression:
850+
case SyntaxKind.ArrayLiteralExpression:
851+
case SyntaxKind.PropertyAssignment:
852+
case SyntaxKind.SpreadElement:
853+
// Carry over whether we are in an assignment pattern of Object and Array literals
854+
// as well as their children that are valid assignment targets.
855+
inAssignmentPattern = saveInAssignmentPattern;
856+
// falls through
830857
default:
831858
bindEachChild(node);
832859
break;
833860
}
834861
bindJSDoc(node);
862+
inAssignmentPattern = saveInAssignmentPattern;
835863
}
836864

837865
function isNarrowingExpression(expr: Expression): boolean {
@@ -1455,6 +1483,24 @@ namespace ts {
14551483
}
14561484
}
14571485

1486+
function bindDestructuringAssignmentFlow(node: DestructuringAssignment) {
1487+
if (inAssignmentPattern) {
1488+
inAssignmentPattern = false;
1489+
bind(node.operatorToken);
1490+
bind(node.right);
1491+
inAssignmentPattern = true;
1492+
bind(node.left);
1493+
}
1494+
else {
1495+
inAssignmentPattern = true;
1496+
bind(node.left);
1497+
inAssignmentPattern = false;
1498+
bind(node.operatorToken);
1499+
bind(node.right);
1500+
}
1501+
bindAssignmentTargetFlow(node.left);
1502+
}
1503+
14581504
const enum BindBinaryExpressionFlowState {
14591505
BindThenBindChildren,
14601506
MaybeBindLeft,
@@ -1571,7 +1617,7 @@ namespace ts {
15711617
* If `node` is a BinaryExpression, adds it to the local work stack, otherwise recursively binds it
15721618
*/
15731619
function maybeBind(node: Node) {
1574-
if (node && isBinaryExpression(node)) {
1620+
if (node && isBinaryExpression(node) && !isDestructuringAssignment(node)) {
15751621
stackIndex++;
15761622
workStacks.expr[stackIndex] = node;
15771623
workStacks.state[stackIndex] = BindBinaryExpressionFlowState.BindThenBindChildren;
@@ -1626,6 +1672,25 @@ namespace ts {
16261672
}
16271673
}
16281674

1675+
function bindBindingElementFlow(node: BindingElement) {
1676+
if (isBindingPattern(node.name)) {
1677+
// When evaluating a binding pattern, the initializer is evaluated before the binding pattern, per:
1678+
// - https://tc39.es/ecma262/#sec-destructuring-binding-patterns-runtime-semantics-iteratorbindinginitialization
1679+
// - `BindingElement: BindingPattern Initializer?`
1680+
// - https://tc39.es/ecma262/#sec-runtime-semantics-keyedbindinginitialization
1681+
// - `BindingElement: BindingPattern Initializer?`
1682+
bindEach(node.decorators);
1683+
bindEach(node.modifiers);
1684+
bind(node.dotDotDotToken);
1685+
bind(node.propertyName);
1686+
bind(node.initializer);
1687+
bind(node.name);
1688+
}
1689+
else {
1690+
bindEachChild(node);
1691+
}
1692+
}
1693+
16291694
function bindJSDocTypeAlias(node: JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag) {
16301695
setParent(node.tagName, node);
16311696
if (node.kind !== SyntaxKind.JSDocEnumTag && node.fullName) {

src/compiler/factory/nodeFactory.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2663,12 +2663,14 @@ namespace ts {
26632663
node.transformFlags |=
26642664
TransformFlags.ContainsES2015 |
26652665
TransformFlags.ContainsES2018 |
2666-
TransformFlags.ContainsDestructuringAssignment;
2666+
TransformFlags.ContainsDestructuringAssignment |
2667+
propagateAssignmentPatternFlags(node.left);
26672668
}
26682669
else if (isArrayLiteralExpression(node.left)) {
26692670
node.transformFlags |=
26702671
TransformFlags.ContainsES2015 |
2671-
TransformFlags.ContainsDestructuringAssignment;
2672+
TransformFlags.ContainsDestructuringAssignment |
2673+
propagateAssignmentPatternFlags(node.left);
26722674
}
26732675
}
26742676
else if (operatorKind === SyntaxKind.AsteriskAsteriskToken || operatorKind === SyntaxKind.AsteriskAsteriskEqualsToken) {
@@ -2680,6 +2682,27 @@ namespace ts {
26802682
return node;
26812683
}
26822684

2685+
function propagateAssignmentPatternFlags(node: AssignmentPattern): TransformFlags {
2686+
if (node.transformFlags & TransformFlags.ContainsObjectRestOrSpread) return TransformFlags.ContainsObjectRestOrSpread;
2687+
if (node.transformFlags & TransformFlags.ContainsES2018) {
2688+
// check for nested spread assignments, otherwise '{ x: { a, ...b } = foo } = c'
2689+
// will not be correctly interpreted by the ES2018 transformer
2690+
for (const element of getElementsOfBindingOrAssignmentPattern(node)) {
2691+
const target = getTargetOfBindingOrAssignmentElement(element);
2692+
if (target && isAssignmentPattern(target)) {
2693+
if (target.transformFlags & TransformFlags.ContainsObjectRestOrSpread) {
2694+
return TransformFlags.ContainsObjectRestOrSpread;
2695+
}
2696+
if (target.transformFlags & TransformFlags.ContainsES2018) {
2697+
const flags = propagateAssignmentPatternFlags(target);
2698+
if (flags) return flags;
2699+
}
2700+
}
2701+
}
2702+
}
2703+
return TransformFlags.None;
2704+
}
2705+
26832706
// @api
26842707
function updateBinaryExpression(node: BinaryExpression, left: Expression, operator: BinaryOperatorToken, right: Expression) {
26852708
return node.left !== left

src/compiler/transformers/destructuring.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace ts {
55
level: FlattenLevel;
66
downlevelIteration: boolean;
77
hoistTempVariables: boolean;
8+
hasTransformedPriorElement?: boolean; // indicates whether we've transformed a prior declaration
89
emitExpression: (value: Expression) => void;
910
emitBindingOrAssignment: (target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node | undefined) => void;
1011
createArrayBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ArrayBindingOrAssignmentPattern;
@@ -265,18 +266,27 @@ namespace ts {
265266
value: Expression | undefined,
266267
location: TextRange,
267268
skipInitializer?: boolean) {
269+
const bindingTarget = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217
268270
if (!skipInitializer) {
269271
const initializer = visitNode(getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, isExpression);
270272
if (initializer) {
271273
// Combine value and initializer
272-
value = value ? createDefaultValueCheck(flattenContext, value, initializer, location) : initializer;
274+
if (value) {
275+
value = createDefaultValueCheck(flattenContext, value, initializer, location);
276+
// If 'value' is not a simple expression, it could contain side-effecting code that should evaluate before an object or array binding pattern.
277+
if (!isSimpleInlineableExpression(initializer) && isBindingOrAssignmentPattern(bindingTarget)) {
278+
value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location);
279+
}
280+
}
281+
else {
282+
value = initializer;
283+
}
273284
}
274285
else if (!value) {
275286
// Use 'void 0' in absence of value and initializer
276287
value = flattenContext.context.factory.createVoidZero();
277288
}
278289
}
279-
const bindingTarget = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217
280290
if (isObjectBindingOrAssignmentPattern(bindingTarget)) {
281291
flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location);
282292
}
@@ -393,7 +403,8 @@ namespace ts {
393403
if (flattenContext.level >= FlattenLevel.ObjectRest) {
394404
// If an array pattern contains an ObjectRest, we must cache the result so that we
395405
// can perform the ObjectRest destructuring in a different declaration
396-
if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread) {
406+
if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread || flattenContext.hasTransformedPriorElement && !isSimpleBindingOrAssignmentElement(element)) {
407+
flattenContext.hasTransformedPriorElement = true;
397408
const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined);
398409
if (flattenContext.hoistTempVariables) {
399410
flattenContext.context.hoistVariableDeclaration(temp);
@@ -428,6 +439,17 @@ namespace ts {
428439
}
429440
}
430441

442+
function isSimpleBindingOrAssignmentElement(element: BindingOrAssignmentElement): boolean {
443+
const target = getTargetOfBindingOrAssignmentElement(element);
444+
if (!target || isOmittedExpression(target)) return true;
445+
const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(element);
446+
if (propertyName && !isPropertyNameLiteral(propertyName)) return false;
447+
const initializer = getInitializerOfBindingOrAssignmentElement(element);
448+
if (initializer && !isSimpleInlineableExpression(initializer)) return false;
449+
if (isBindingOrAssignmentPattern(target)) return every(getElementsOfBindingOrAssignmentPattern(target), isSimpleBindingOrAssignmentElement);
450+
return isIdentifier(target);
451+
}
452+
431453
/**
432454
* Creates an expression used to provide a default value if a value is `undefined` at runtime.
433455
*

0 commit comments

Comments
 (0)