Fix destructuring evaluation order for initializers

This commit is contained in:
Ron Buckton
2020-10-13 17:14:35 -07:00
parent 83d02a5f05
commit 7393dba6bd
40 changed files with 599 additions and 376 deletions

View File

@@ -5,6 +5,7 @@ namespace ts {
level: FlattenLevel;
downlevelIteration: boolean;
hoistTempVariables: boolean;
hasTransformedPriorElement?: boolean; // indicates whether we've transformed a prior declaration
emitExpression: (value: Expression) => void;
emitBindingOrAssignment: (target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node | undefined) => void;
createArrayBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ArrayBindingOrAssignmentPattern;
@@ -265,18 +266,27 @@ namespace ts {
value: Expression | undefined,
location: TextRange,
skipInitializer?: boolean) {
const bindingTarget = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217
if (!skipInitializer) {
const initializer = visitNode(getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, isExpression);
if (initializer) {
// Combine value and initializer
value = value ? createDefaultValueCheck(flattenContext, value, initializer, location) : initializer;
if (value) {
value = createDefaultValueCheck(flattenContext, value, initializer, location);
// If 'value' is not a simple expression, it could contain side-effecting code that should evaluate before an object or array binding pattern.
if (!isSimpleInlineableExpression(initializer) && isBindingOrAssignmentPattern(bindingTarget)) {
value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location);
}
}
else {
value = initializer;
}
}
else if (!value) {
// Use 'void 0' in absence of value and initializer
value = flattenContext.context.factory.createVoidZero();
}
}
const bindingTarget = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217
if (isObjectBindingOrAssignmentPattern(bindingTarget)) {
flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location);
}
@@ -393,7 +403,8 @@ namespace ts {
if (flattenContext.level >= FlattenLevel.ObjectRest) {
// If an array pattern contains an ObjectRest, we must cache the result so that we
// can perform the ObjectRest destructuring in a different declaration
if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread) {
if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread || flattenContext.hasTransformedPriorElement && !isSimpleBindingOrAssignmentElement(element)) {
flattenContext.hasTransformedPriorElement = true;
const temp = flattenContext.context.factory.createTempVariable(/*recordTempVariable*/ undefined);
if (flattenContext.hoistTempVariables) {
flattenContext.context.hoistVariableDeclaration(temp);
@@ -428,6 +439,17 @@ namespace ts {
}
}
function isSimpleBindingOrAssignmentElement(element: BindingOrAssignmentElement): boolean {
const target = getTargetOfBindingOrAssignmentElement(element);
if (!target || isOmittedExpression(target)) return true;
const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(element);
if (propertyName && !isPropertyNameLiteral(propertyName)) return false;
const initializer = getInitializerOfBindingOrAssignmentElement(element);
if (initializer && !isSimpleInlineableExpression(initializer)) return false;
if (isBindingOrAssignmentPattern(target)) return every(getElementsOfBindingOrAssignmentPattern(target), isSimpleBindingOrAssignmentElement);
return isIdentifier(target);
}
/**
* Creates an expression used to provide a default value if a value is `undefined` at runtime.
*

View File

@@ -89,6 +89,7 @@
"unittests/evaluation/asyncArrow.ts",
"unittests/evaluation/asyncGenerator.ts",
"unittests/evaluation/awaiter.ts",
"unittests/evaluation/destructuring.ts",
"unittests/evaluation/forAwaitOf.ts",
"unittests/evaluation/forOf.ts",
"unittests/evaluation/optionalCall.ts",

View File

@@ -0,0 +1,39 @@
describe("unittests:: evaluation:: destructuring", () => {
// https://github.com/microsoft/TypeScript/issues/39205
describe("correct order for array destructuring evaluation and initializers", () => {
it("when element is undefined", () => {
const result = evaluator.evaluateTypeScript(`
export const output: any[] = [];
const order = (n: any): any => output.push(n);
let [{ [order(1)]: x } = order(0)] = [];
`, { target: ts.ScriptTarget.ES5 });
assert.deepEqual(result.output, [0, 1]);
});
it("when element is defined", async () => {
const result = evaluator.evaluateTypeScript(`
export const output: any[] = [];
const order = (n: any): any => output.push(n);
let [{ [order(1)]: x } = order(0)] = [{}];
`, { target: ts.ScriptTarget.ES5 });
assert.deepEqual(result.output, [1]);
});
});
describe("correct order for array destructuring evaluation and initializers with spread", () => {
it("ES5", () => {
const result = evaluator.evaluateTypeScript(`
export const output: any[] = [];
const order = (n: any): any => output.push(n);
let { [order(0)]: { [order(2)]: z } = order(1), ...w } = {} as any;
`, { target: ts.ScriptTarget.ES5 });
assert.deepEqual(result.output, [0, 1, 2]);
});
it("ES2015", () => {
const result = evaluator.evaluateTypeScript(`
export const output: any[] = [];
const order = (n: any): any => output.push(n);
let { [order(0)]: { [order(2)]: z } = order(1), ...w } = {} as any;
`, { target: ts.ScriptTarget.ES2015 });
assert.deepEqual(result.output, [0, 1, 2]);
});
});
});