From 8babde0b9839fbd500a3089abd33754f478a05d2 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Fri, 11 Nov 2016 17:23:42 -0800 Subject: [PATCH] WIP Clean up destructuring --- src/compiler/core.ts | 7 + src/compiler/factory.ts | 14 +- src/compiler/transformers/destructuring.ts | 758 ++++++++++++++++++++- src/compiler/transformers/esnext.ts | 2 +- src/compiler/types.ts | 10 +- src/compiler/utilities.ts | 26 +- src/compiler/visitor.ts | 30 + 7 files changed, 814 insertions(+), 33 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8175730159b..a21477a40d6 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -811,6 +811,13 @@ namespace ts { } } + export function appendProperty(map: Map, key: string | number, value: T): Map { + if (key === undefined || value === undefined) return map; + if (map === undefined) map = createMap(); + map[key] = value; + return map; + } + export function assign, T2, T3>(t: T1, arg1: T2, arg2: T3): T1 & T2 & T3; export function assign, T2>(t: T1, arg1: T2): T1 & T2; export function assign>(t: T1, ...args: any[]): any; diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index e9935668676..6674ffc7263 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -238,9 +238,9 @@ namespace ts { return node; } - export function updateParameter(node: ParameterDeclaration, decorators: Decorator[], modifiers: Modifier[], name: BindingName, type: TypeNode, initializer: Expression) { - if (node.decorators !== decorators || node.modifiers !== modifiers || node.name !== name || node.type !== type || node.initializer !== initializer) { - return updateNode(createParameter(decorators, modifiers, node.dotDotDotToken, name, node.questionToken, type, initializer, /*location*/ node, /*flags*/ node.flags), node); + export function updateParameter(node: ParameterDeclaration, decorators: Decorator[], modifiers: Modifier[], dotDotDotToken: DotDotDotToken, name: BindingName, type: TypeNode, initializer: Expression) { + if (node.decorators !== decorators || node.modifiers !== modifiers || node.dotDotDotToken !== dotDotDotToken || node.name !== name || node.type !== type || node.initializer !== initializer) { + return updateNode(createParameter(decorators, modifiers, dotDotDotToken, name, node.questionToken, type, initializer, /*location*/ node, /*flags*/ node.flags), node); } return node; @@ -378,9 +378,9 @@ namespace ts { return node; } - export function updateBindingElement(node: BindingElement, propertyName: PropertyName, name: BindingName, initializer: Expression) { - if (node.propertyName !== propertyName || node.name !== name || node.initializer !== initializer) { - return updateNode(createBindingElement(propertyName, node.dotDotDotToken, name, initializer, node), node); + export function updateBindingElement(node: BindingElement, dotDotDotToken: DotDotDotToken, propertyName: PropertyName, name: BindingName, initializer: Expression) { + if (node.propertyName !== propertyName || node.dotDotDotToken !== dotDotDotToken || node.name !== name || node.initializer !== initializer) { + return updateNode(createBindingElement(propertyName, dotDotDotToken, name, initializer, node), node); } return node; } @@ -1635,7 +1635,7 @@ namespace ts { // flag and setting a parent node. const react = createIdentifier(reactNamespace || "React"); react.flags &= ~NodeFlags.Synthesized; - // Set the parent that is in parse tree + // Set the parent that is in parse tree // this makes sure that parent chain is intact for checker to traverse complete scope tree react.parent = getParseTreeNode(parent); return react; diff --git a/src/compiler/transformers/destructuring.ts b/src/compiler/transformers/destructuring.ts index bba09710898..b0cb3f9d239 100644 --- a/src/compiler/transformers/destructuring.ts +++ b/src/compiler/transformers/destructuring.ts @@ -3,6 +3,18 @@ /*@internal*/ namespace ts { + type EffectiveBindingElement = VariableDeclaration | ParameterDeclaration | BindingElement | ObjectLiteralElementLike | Expression; + type EffectiveObjectBindingPattern = ObjectBindingPattern | ObjectLiteralExpression; + type EffectiveArrayBindingPattern = ArrayBindingPattern | ArrayLiteralExpression; + type EffectiveBindingPattern = EffectiveObjectBindingPattern | EffectiveArrayBindingPattern; + type EffectiveBindingTarget = EffectiveBindingPattern | Expression; + type EffectiveRestIndicator = DotDotDotToken | SpreadElement | SpreadAssignment; + + export const enum FlattenLevel { + All, + ObjectRestDestructuringOnly, + } + /** * Flattens a destructuring assignment expression. * @@ -40,7 +52,7 @@ namespace ts { // // The source map location for the assignment should point to the entire binary // expression. - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment, visitor); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment, visitor); } else if (nodeIsSynthesized(node)) { // Generally, the source map location for a destructuring assignment is the root @@ -52,7 +64,7 @@ namespace ts { location = value; } - flattenDestructuring(node, value, location, emitAssignment, emitTempVariableAssignment, recordTempVariable, emitRestAssignment, transformRest, visitor); + flattenDestructuringOld(node, value, location, emitAssignment, emitTempVariableAssignment, recordTempVariable, emitRestAssignment, transformRest, visitor); if (needsValue) { expressions.push(value); @@ -98,7 +110,7 @@ namespace ts { transformRest?: boolean) { const declarations: VariableDeclaration[] = []; - flattenDestructuring(node, value, node, emitAssignment, emitTempVariableAssignment, noop, emitRestAssignment, transformRest, visitor); + flattenDestructuringOld(node, value, node, emitAssignment, emitTempVariableAssignment, noop, emitRestAssignment, transformRest, visitor); return declarations; @@ -140,7 +152,7 @@ namespace ts { const declarations: VariableDeclaration[] = []; let pendingAssignments: Expression[]; - flattenDestructuring(node, value, node, emitAssignment, emitTempVariableAssignment, recordTempVariable, emitRestAssignment, transformRest, visitor); + flattenDestructuringOld(node, value, node, emitAssignment, emitTempVariableAssignment, recordTempVariable, emitRestAssignment, transformRest, visitor); return declarations; @@ -201,7 +213,7 @@ namespace ts { const pendingAssignments: Expression[] = []; - flattenDestructuring(node, /*value*/ undefined, node, emitAssignment, emitTempVariableAssignment, noop, emitRestAssignment, /*transformRest*/ false, visitor); + flattenDestructuringOld(node, /*value*/ undefined, node, emitAssignment, emitTempVariableAssignment, noop, emitRestAssignment, /*transformRest*/ false, visitor); const expression = inlineExpressions(pendingAssignments); aggregateTransformFlags(expression); @@ -238,7 +250,7 @@ namespace ts { } } - function flattenDestructuring( + function flattenDestructuringOld( root: VariableDeclaration | ParameterDeclaration | BindingElement | BinaryExpression, value: Expression, location: TextRange, @@ -305,7 +317,7 @@ namespace ts { // For anything but a single element destructuring we need to generate a temporary // to ensure value is evaluated exactly once. // When doing so we want to hightlight the passed in source map node since thats the one needing this temp assignment - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); } let bindingElements: ObjectLiteralElementLike[] = []; @@ -361,7 +373,7 @@ namespace ts { // For anything but a single element destructuring we need to generate a temporary // to ensure value is evaluated exactly once. // When doing so we want to highlight the passed-in source map node since thats the one needing this temp assignment - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); } const expressions: Expression[] = []; @@ -393,7 +405,7 @@ namespace ts { // For anything but a single element destructuring we need to generate a temporary // to ensure value is evaluated exactly once. // When doing so we want to highlight the passed-in source map node since thats the one needing this temp assignment - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); } for (let i = 0; i < numElements; i++) { @@ -454,7 +466,7 @@ namespace ts { // to ensure value is evaluated exactly once. Additionally, if we have zero elements // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, // so in that case, we'll intentionally create that temporary. - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ numElements !== 0, target, emitTempVariableAssignment); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ numElements !== 0, target, emitTempVariableAssignment); } if (name.kind === SyntaxKind.ArrayBindingPattern) { emitArrayBindingElement(name as ArrayBindingPattern, value); @@ -558,7 +570,7 @@ namespace ts { } function createDefaultValueCheck(value: Expression, defaultValue: Expression, location: TextRange): Expression { - value = ensureIdentifier(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); + value = ensureIdentifierOld(value, /*reuseIdentifierExpressions*/ true, location, emitTempVariableAssignment); return createConditional( createStrictEquality(value, createVoidZero()), createToken(SyntaxKind.QuestionToken), @@ -579,7 +591,7 @@ namespace ts { if (isComputedPropertyName(propertyName)) { return createElementAccess( expression, - ensureIdentifier(propertyName.expression, /*reuseIdentifierExpressions*/ false, /*location*/ propertyName, emitTempVariableAssignment) + ensureIdentifierOld(propertyName.expression, /*reuseIdentifierExpressions*/ false, /*location*/ propertyName, emitTempVariableAssignment) ); } else if (isLiteralExpression(propertyName)) { @@ -612,7 +624,7 @@ namespace ts { * @param emitTempVariableAssignment A callback used to emit a temporary variable. * @param visitor An optional callback used to visit the value. */ - function ensureIdentifier( + function ensureIdentifierOld( value: Expression, reuseIdentifierExpressions: boolean, location: TextRange, @@ -630,4 +642,724 @@ namespace ts { return emitTempVariableAssignment(value, location); } } + + interface FlattenHost { + context: TransformationContext; + level: FlattenLevel; + recordTempVariablesInLine: boolean; + emitAssignment: (target: EffectiveBindingTarget, value: Expression, location: TextRange, original: Node) => void; + emitArrayAssignment: (elements: EffectiveBindingElement[], value: Expression, location: TextRange, original: Node) => void; + emitObjectAssignment: (elements: EffectiveBindingElement[], value: Expression, location: TextRange, original: Node) => void; + emitExpression: (value: Expression) => void; + } + + export function flattenDestructuringToDeclarations( + context: TransformationContext, + node: VariableDeclaration | ParameterDeclaration, + boundValue: Expression | undefined, + recordTempVariablesInLine: boolean, + level: FlattenLevel): VisitResult { + + const pendingDeclarations: { pendingExpressions?: Expression[], name: BindingName, value: Expression, location?: TextRange, original?: Node; }[] = []; + let pendingExpressions: Expression[]; + let declaration: VariableDeclaration; + let declarations: VariableDeclaration[]; + const host: FlattenHost = { + context, + level, + recordTempVariablesInLine, + emitExpression, + emitAssignment, + emitArrayAssignment, + emitObjectAssignment + }; + + flattenEffectiveBindingElement(host, node, boundValue, node); + + if (pendingExpressions) { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + if (recordTempVariablesInLine) { + const value = inlineExpressions(pendingExpressions); + pendingExpressions = undefined; + emitAssignment(temp, value, /*location*/ undefined, /*original*/ undefined); + } + else { + context.hoistVariableDeclaration(temp); + const pendingDeclaration = lastOrUndefined(pendingDeclarations); + pendingDeclaration.pendingExpressions = append( + pendingDeclaration.pendingExpressions, + createAssignment(temp, pendingDeclaration.value) + ); + addRange(pendingDeclaration.pendingExpressions, pendingExpressions); + pendingDeclaration.value = temp; + } + } + + for (const { pendingExpressions, name, value, location, original } of pendingDeclarations) { + const variable = createVariableDeclaration( + name, + /*type*/ undefined, + pendingExpressions ? inlineExpressions(append(pendingExpressions, value)) : value, + location); + variable.original = original; + if (isIdentifier(name)) { + setEmitFlags(variable, EmitFlags.NoNestedSourceMaps); + } + aggregateTransformFlags(variable); + if (!declaration) { + declaration = variable; + } + else if (!declarations) { + declarations = [declaration, variable]; + } + else { + declarations.push(variable); + } + } + + return declarations || declaration; + + function emitExpression(value: Expression) { + pendingExpressions = append(pendingExpressions, value); + } + + function emitAssignment(target: EffectiveBindingTarget, value: Expression, location: TextRange, original: Node) { + Debug.assertNode(target, isBindingName); + pendingDeclarations.push({ pendingExpressions, name: target, value, location, original }); + if (pendingExpressions) { + value = inlineExpressions(append(pendingExpressions, value)); + pendingExpressions = undefined; + } + } + + function emitArrayAssignment(elements: EffectiveBindingElement[], value: Expression, location: TextRange, original: Node) { + Debug.assertEachNode(elements, isArrayBindingElement); + emitAssignment(createArrayBindingPattern(elements), value, location, original); + } + + function emitObjectAssignment(elements: EffectiveBindingElement[], value: Expression, location: TextRange, original: Node) { + Debug.assertEachNode(elements, isBindingElement); + emitAssignment(createObjectBindingPattern(elements), value, location, original); + } + } + + function flattenEffectiveBindingElement( + host: FlattenHost, + bindingElement: EffectiveBindingElement, + boundValue: Expression | undefined, + location: TextRange, + skipInitializer?: boolean) { + if (!skipInitializer) { + const initializer = getInitializerOfEffectiveBindingElement(bindingElement); + if (initializer) { + // Combine value and initializer + boundValue = boundValue ? createDefaultValueCheck(host, boundValue, initializer, location) : initializer; + } + else if (!boundValue) { + // Use 'void 0' in absence of value and initializer + boundValue = createVoidZero(); + } + } + const bindingTarget = getTargetOfEffectiveBindingElement(bindingElement); + if (!isEffectiveBindingPattern(bindingTarget)) { + host.emitAssignment(bindingTarget, boundValue, location, /*original*/ bindingElement); + } + else { + const elements = getElementsOfEffectiveBindingPattern(bindingTarget); + const numElements = elements.length; + if (numElements !== 1) { + // For anything other than a single-element destructuring we need to generate a temporary + // to ensure value is evaluated exactly once. Additionally, if we have zero elements + // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, + // so in that case, we'll intentionally create that temporary. + const reuseIdentifierExpressions = !isDeclarationBindingElement(bindingElement) || numElements !== 0; + boundValue = ensureIdentifier(host, boundValue, reuseIdentifierExpressions, location); + } + if (isEffectiveObjectBindingPattern(bindingTarget)) { + flattenEffectiveObjectBindingElements(host, bindingTarget, elements, boundValue, location); + } + else { + flattenEffectiveArrayBindingElements(host, bindingTarget, elements, boundValue, location); + } + } + } + + function flattenEffectiveObjectBindingElements(host: FlattenHost, bindingTarget: EffectiveObjectBindingPattern, elements: EffectiveBindingElement[], boundValue: Expression, location: TextRange) { + let bindingElements: EffectiveBindingElement[]; + const numElements = elements.length; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + if (!getEffectiveRestIndicator(element)) { + if (host.level >= FlattenLevel.ObjectRestDestructuringOnly && !(element.transformFlags & TransformFlags.ContainsSpreadExpression)) { + bindingElements = append(bindingElements, element); + } + else { + if (bindingElements) { + host.emitObjectAssignment(bindingElements, boundValue, location, bindingTarget); + bindingElements = undefined; + } + const propertyName = getEffectivePropertyNameOfEffectiveBindingElement(element); + const value = createDestructuringPropertyAccess(host, boundValue, propertyName); + flattenEffectiveBindingElement(host, element, value, /*location*/ element); + } + } + else if (i === numElements - 1) { + if (bindingElements) { + host.emitObjectAssignment(bindingElements, boundValue, location, bindingTarget); + bindingElements = undefined; + } + const value = createRestCall(boundValue, elements, bindingTarget); + flattenEffectiveBindingElement(host, element, value, element); + } + } + if (bindingElements) { + host.emitObjectAssignment(bindingElements, boundValue, location, bindingTarget); + } + } + + function flattenEffectiveArrayBindingElements(host: FlattenHost, bindingTarget: EffectiveArrayBindingPattern, elements: EffectiveBindingElement[], boundValue: Expression, location: TextRange) { + let bindingElements: EffectiveBindingElement[]; + let spreadContainingElements: EffectiveBindingElement[]; + let tempNames: Map; + const numElements = elements.length; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + if (isOmittedExpression(element)) { + continue; + } + else if (host.level >= FlattenLevel.ObjectRestDestructuringOnly) { + if (element.transformFlags & TransformFlags.ContainsSpreadExpression) { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + tempNames = appendProperty(tempNames, getNodeId(element), temp); + spreadContainingElements = append(spreadContainingElements, element); + bindingElements = append(bindingElements, temp); + } + else { + bindingElements = append(bindingElements, element); + } + } + else if (!getEffectiveRestIndicator(element)) { + const value = createElementAccess(boundValue, i); + flattenEffectiveBindingElement(host, element, value, /*location*/ element); + } + else if (i === numElements - 1) { + const value = createArraySlice(boundValue, i); + flattenEffectiveBindingElement(host, element, value, /*location*/ element); + } + } + if (bindingElements) { + host.emitArrayAssignment(bindingElements, boundValue, location, bindingTarget); + } + if (spreadContainingElements) { + for (const element of spreadContainingElements) { + flattenEffectiveBindingElement(host, element, tempNames[getNodeId(element)], element); + } + } + } + + /** + * Creates an expression used to provide a default value if a value is `undefined` at runtime. + */ + function createDefaultValueCheck(host: FlattenHost, value: Expression, defaultValue: Expression, location: TextRange): Expression { + value = ensureIdentifier(host, value, /*reuseIdentifierExpressions*/ true, location); + return createConditional( + createStrictEquality(value, createVoidZero()), + createToken(SyntaxKind.QuestionToken), + defaultValue, + createToken(SyntaxKind.ColonToken), + value + ); + } + + /** + * Creates either a PropertyAccessExpression or an ElementAccessExpression for the + * right-hand side of a transformed destructuring assignment. + * + * @param expression The right-hand expression that is the source of the property. + * @param propertyName The destructuring property name. + * @param emitTempVariableAssignment A callback used to emit a temporary variable. + */ + function createDestructuringPropertyAccess(host: FlattenHost, expression: Expression, propertyName: PropertyName): LeftHandSideExpression { + if (isComputedPropertyName(propertyName)) { + const argumentExpression = ensureIdentifier(host, propertyName.expression, /*reuseIdentifierExpressions*/ false, /*location*/ propertyName); + return createElementAccess(expression, argumentExpression); + } + else if (isLiteralExpression(propertyName)) { + const argumentExpression = getSynthesizedClone(propertyName); + argumentExpression.text = unescapeIdentifier(argumentExpression.text); + return createElementAccess(expression, argumentExpression); + } + else if (isGeneratedIdentifier(propertyName)) { + const name = getSynthesizedClone(propertyName); + name.text = unescapeIdentifier(name.text); + return createPropertyAccess(expression, name); + } + else { + const name = createIdentifier(unescapeIdentifier(propertyName.text)); + return createPropertyAccess(expression, name); + } + } + + /** + * Ensures that there exists a declared identifier whose value holds the given expression. + * This function is useful to ensure that the expression's value can be read from in subsequent expressions. + * Unless 'reuseIdentifierExpressions' is false, 'value' will be returned if it is just an identifier. + * + * @param value the expression whose value needs to be bound. + * @param reuseIdentifierExpressions true if identifier expressions can simply be returned; + * false if it is necessary to always emit an identifier. + * @param emitTempVariableAssignment A callback used to emit a temporary variable. + * @param location The location to use for source maps and comments. + */ + function ensureIdentifier( + host: FlattenHost, + value: Expression, + reuseIdentifierExpressions: boolean, + location: TextRange) { + + if (isIdentifier(value) && reuseIdentifierExpressions) { + return value; + } + else { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + if (host.recordTempVariablesInLine) { + host.emitAssignment(temp, value, location, /*original*/ undefined); + } + else { + host.context.hoistVariableDeclaration(temp); + host.emitExpression(createAssignment(temp, value, location)); + } + return temp; + } + } + + /** + * Determines whether the EffectiveBindingElement is a declaration + */ + function isDeclarationBindingElement(bindingElement: EffectiveBindingElement): bindingElement is VariableDeclaration | ParameterDeclaration | BindingElement { + switch (bindingElement.kind) { + case SyntaxKind.VariableDeclaration: + case SyntaxKind.Parameter: + case SyntaxKind.BindingElement: + return true; + } + + return false; + } + + /** + * Gets the initializer of an EffectiveBindingElement. + */ + function getInitializerOfEffectiveBindingElement(bindingElement: EffectiveBindingElement): Expression | undefined { + if (isDeclarationBindingElement(bindingElement)) { + // `1` in `let { a = 1 } = ...` + // `1` in `let { a: b = 1 } = ...` + // `1` in `let { a: {b} = 1 } = ...` + // `1` in `let { a: [b] = 1 } = ...` + // `1` in `let [a = 1] = ...` + // `1` in `let [{a} = 1] = ...` + // `1` in `let [[a] = 1] = ...` + return bindingElement.initializer; + } + + if (isPropertyAssignment(bindingElement)) { + // `1` in `({ a: b = 1 } = ...)` + // `1` in `({ a: {b} = 1 } = ...)` + // `1` in `({ a: [b] = 1 } = ...)` + return isAssignmentExpression(bindingElement.initializer, /*excludeCompoundAssignment*/ true) + ? bindingElement.initializer.right + : undefined; + } + + if (isShorthandPropertyAssignment(bindingElement)) { + // `1` in `({ a = 1 } = ...)` + return bindingElement.objectAssignmentInitializer; + } + + if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { + // `1` in `[a = 1] = ...` + // `1` in `[{a} = 1] = ...` + // `1` in `[[a] = 1] = ...` + return bindingElement.right; + } + + if (isSpreadExpression(bindingElement) || isPartiallyEmittedExpression(bindingElement)) { + // Recovery consistent with existing emit. + return getInitializerOfEffectiveBindingElement(bindingElement.expression); + } + } + + /** + * Gets the name of an EffectiveBindingElement. + */ + function getTargetOfEffectiveBindingElement(bindingElement: EffectiveBindingElement): EffectiveBindingTarget { + if (isDeclarationBindingElement(bindingElement)) { + // `a` in `let { a } = ...` + // `a` in `let { a = 1 } = ...` + // `b` in `let { a: b } = ...` + // `b` in `let { a: b = 1 } = ...` + // `a` in `let { ...a } = ...` + // `{b}` in `let { a: {b} } = ...` + // `{b}` in `let { a: {b} = 1 } = ...` + // `[b]` in `let { a: [b] } = ...` + // `[b]` in `let { a: [b] = 1 } = ...` + // `a` in `let [a] = ...` + // `a` in `let [a = 1] = ...` + // `a` in `let [...a] = ...` + // `{a}` in `let [{a}] = ...` + // `{a}` in `let [{a} = 1] = ...` + // `[a]` in `let [[a]] = ...` + // `[a]` in `let [[a] = 1] = ...` + return bindingElement.name; + } + + if (isObjectLiteralElementLike(bindingElement)) { + switch (bindingElement.kind) { + case SyntaxKind.PropertyAssignment: + // `b` in `({ a: b } = ...)` + // `b` in `({ a: b = 1 } = ...)` + // `{b}` in `({ a: {b} } = ...)` + // `{b}` in `({ a: {b} = 1 } = ...)` + // `[b]` in `({ a: [b] } = ...)` + // `[b]` in `({ a: [b] = 1 } = ...)` + // `b.c` in `({ a: b.c } = ...)` + // `b.c` in `({ a: b.c = 1 } = ...)` + // `b[0]` in `({ a: b[0] } = ...)` + // `b[0]` in `({ a: b[0] = 1 } = ...)` + return getTargetOfEffectiveBindingElement(bindingElement.initializer); + + case SyntaxKind.ShorthandPropertyAssignment: + // `a` in `({ a } = ...)` + // `a` in `({ a = 1 } = ...)` + return bindingElement.name; + + case SyntaxKind.SpreadAssignment: + // `a` in `({ ...a } = ...)` + return getTargetOfEffectiveBindingElement(bindingElement.expression); + } + + // no target + return undefined; + } + + if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { + // `a` in `[a = 1] = ...` + // `{a}` in `[{a} = 1] = ...` + // `[a]` in `[[a] = 1] = ...` + // `a.b` in `[a.b = 1] = ...` + // `a[0]` in `[a[0] = 1] = ...` + return getTargetOfEffectiveBindingElement(bindingElement.left); + } + + if (isSpreadExpression(bindingElement) || isPartiallyEmittedExpression(bindingElement)) { + // `a` in `[...a] = ...` + return getTargetOfEffectiveBindingElement(bindingElement.expression); + } + + // `a` in `[a] = ...` + // `{a}` in `[{a}] = ...` + // `[a]` in `[[a]] = ...` + // `a.b` in `[a.b] = ...` + // `a[0]` in `[a[0]] = ...` + return bindingElement; + } + + /** + * Determines whether an EffectiveBindingElement is a rest element. + */ + function getEffectiveRestIndicator(bindingElement: EffectiveBindingElement): EffectiveRestIndicator { + switch (bindingElement.kind) { + case SyntaxKind.Parameter: + case SyntaxKind.BindingElement: + // `...` in `let [...a] = ...` + return (bindingElement).dotDotDotToken; + + case SyntaxKind.SpreadElement: + case SyntaxKind.SpreadAssignment: + // `...` in `[...a] = ...` + return bindingElement; + } + + return undefined; + } + + /** + * Gets the property name of a BindingElement-like element + */ + function getEffectivePropertyNameOfEffectiveBindingElement(bindingElement: EffectiveBindingElement) { + switch (bindingElement.kind) { + case SyntaxKind.BindingElement: + // `a` in `let { a: b } = ...` + // `[a]` in `let { [a]: b } = ...` + // `"a"` in `let { "a": b } = ...` + // `1` in `let { 1: b } = ...` + if ((bindingElement).propertyName) { + return (bindingElement).propertyName; + } + + break; + + case SyntaxKind.PropertyAssignment: + // `a` in `({ a: b } = ...)` + // `[a]` in `({ [a]: b } = ...)` + // `"a"` in `({ "a": b } = ...)` + // `1` in `({ 1: b } = ...)` + if ((bindingElement).name) { + return (bindingElement).name; + } + + break; + + case SyntaxKind.SpreadAssignment: + // `a` in `({ ...a } = ...)` + return (bindingElement).name; + } + + const target = getTargetOfEffectiveBindingElement(bindingElement); + if (target && isPropertyName(target)) { + return target; + } + + Debug.fail("Invalid property name for binding element."); + } + + /** + * Determines whether a node is BindingPattern-like + */ + function isEffectiveBindingPattern(node: EffectiveBindingTarget): node is EffectiveBindingPattern { + return isEffectiveObjectBindingPattern(node) + || isEffectiveArrayBindingPattern(node); + } + + /** + * Determines whether a node is ObjectBindingPattern-like + */ + function isEffectiveObjectBindingPattern(node: EffectiveBindingTarget): node is EffectiveObjectBindingPattern { + switch (node.kind) { + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ObjectLiteralExpression: + return true; + } + + return false; + } + + /** + * Determines whether a node is ArrayBindingPattern-like + */ + function isEffectiveArrayBindingPattern(node: EffectiveBindingTarget): node is EffectiveArrayBindingPattern { + switch (node.kind) { + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ArrayLiteralExpression: + return true; + } + + return false; + } + + /** + * Gets the elements of a BindingPattern-like name + */ + function getElementsOfEffectiveBindingPattern(name: EffectiveBindingPattern): EffectiveBindingElement[] { + switch (name.kind) { + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ArrayLiteralExpression: + // `a` in `{a}` + // `a` in `[a]` + return name.elements; + + case SyntaxKind.ObjectLiteralExpression: + // `a` in `{a}` + return name.properties; + } + } + + // function updateEffectiveBindingElement(bindingElement: EffectiveBindingElement, restIndicator: EffectiveRestIndicator, propertyName: PropertyName, target: EffectiveBindingTarget, initializer: Expression) { + // switch (bindingElement.kind) { + // case SyntaxKind.VariableDeclaration: + // Debug.assertNode(target, isBindingName); + // Debug.assertMissingNode(propertyName); + // return updateVariableDeclaration( + // bindingElement, + // target, + // /*type*/ undefined, + // initializer + // ); + + // case SyntaxKind.Parameter: + // Debug.assertOptionalToken(restIndicator, SyntaxKind.DotDotDotToken); + // Debug.assertMissingNode(propertyName); + // Debug.assertNode(target, isBindingName); + // return updateParameter( + // bindingElement, + // /*decorators*/ undefined, + // /*modifiers*/ undefined, + // restIndicator, + // target, + // /*type*/ undefined, + // initializer + // ); + + // case SyntaxKind.BindingElement: + // Debug.assertOptionalToken(restIndicator, SyntaxKind.DotDotDotToken); + // Debug.assertNode(target, isBindingName); + // return updateBindingElement( + // bindingElement, + // restIndicator, + // propertyName, + // target, + // initializer + // ); + + // case SyntaxKind.PropertyAssignment: + // case SyntaxKind.ShorthandPropertyAssignment: + // case SyntaxKind.SpreadAssignment: + // if (restIndicator) { + // return convertToSpreadAssignment(bindingElement, propertyName, target, initializer); + // } + // else if (propertyName) { + // return convertToPropertyAssignment(bindingElement, propertyName, target, initializer); + // } + // else { + // return convertToShorthandPropertyAssignment(bindingElement, target, initializer); + // } + + // case SyntaxKind.ArrayLiteralExpression: + // case SyntaxKind.BinaryExpression: + // case SyntaxKind.SpreadElement: + // case SyntaxKind.PropertyAccessExpression: + // case SyntaxKind.ElementAccessExpression: + // Debug.assertMissingNode(propertyName); + // if (restIndicator) { + // return convertToSpreadElement(bindingElement, target, initializer); + // } + // else { + + // } + // } + // } + + // function convertToSpreadAssignment(node: PropertyAssignment | ShorthandPropertyAssignment | SpreadAssignment, propertyName: PropertyName, target: EffectiveBindingTarget, initializer: Expression) { + // Debug.assertMissingNode(propertyName); + // Debug.assertNode(target, isIdentifier); + // Debug.assertMissingNode(initializer); + // if (node.kind === SyntaxKind.SpreadAssignment) { + // return updateSpreadAssignment(node, target); + // } + // return setOriginalNode( + // createSpreadAssignment( + // target, + // node + // ), + // node + // ); + // } + + // function convertToPropertyAssignment(node: PropertyAssignment | ShorthandPropertyAssignment | SpreadAssignment, propertyName: PropertyName, target: EffectiveBindingTarget, initializer: Expression) { + // Debug.assertNode(target, isExpression); + // if (node.kind === SyntaxKind.PropertyAssignment) { + // return updatePropertyAssignment( + // node, + // propertyName, + // initializer ? + // isAssignmentExpression(node.initializer, /*excludeCompoundAssignment*/ true) + // ? updateBinary(node.initializer, target, initializer) + // : createAssignment(target, initializer) + // : target + // ); + // } + // return setOriginalNode( + // createPropertyAssignment( + // propertyName, + // initializer ? + // createAssignment(target, initializer) + // : target, + // node + // ), + // node + // ); + // } + + // function convertToShorthandPropertyAssignment(node: PropertyAssignment | ShorthandPropertyAssignment | SpreadAssignment, target: EffectiveBindingTarget, initializer: Expression) { + // Debug.assertNode(target, isIdentifier); + // if (node.kind === SyntaxKind.ShorthandPropertyAssignment) { + // return updateShorthandPropertyAssignment( + // node, + // target, + // initializer + // ); + // } + // return setOriginalNode( + // createShorthandPropertyAssignment( + // target, + // initializer, + // node + // ), + // node + // ); + // } + + // function convertToSpreadElement(node: Expression, target: EffectiveBindingTarget, initializer: Expression) { + // Debug.assertNode(target, isExpression); + // Debug.assertMissingNode(initializer); + // if (node.kind === SyntaxKind.SpreadElement) { + // return updateSpread( + // node, + // target); + // } + // return setOriginalNode( + // createSpread( + // target, + // node + // ), + // node + // ); + // } + + // function convertToElement(node: Expression, target: EffectiveBindingTarget, initializer: Expression) { + + // } + + function createOrUpdateVariableDeclaration(node: Node, name: BindingName, initializer: Expression, location: TextRange) { + if (node && node.kind === SyntaxKind.VariableDeclaration) { + return updateVariableDeclaration(node, name, /*type*/ undefined, initializer); + } + const variable = createVariableDeclaration(name, /*type*/ undefined, initializer, location); + return node ? setOriginalNode(variable, node) : variable; + } + + function createOrUpdateArrayBindingPattern(node: Node, elements: ArrayBindingElement[], location: TextRange) { + if (node && node.kind === SyntaxKind.ArrayBindingPattern) { + return updateArrayBindingPattern(node, elements); + } + const pattern = createArrayBindingPattern(elements, location); + return node ? setOriginalNode(pattern, node) : pattern; + } + + function createOrUpdateObjectBindingPattern(node: Node, elements: BindingElement[], location: TextRange) { + if (node && node.kind === SyntaxKind.ArrayBindingPattern) { + return updateObjectBindingPattern(node, elements); + } + const pattern = createObjectBindingPattern(elements, location); + return node ? setOriginalNode(pattern, node) : pattern; + } + + /** Given value: o, propName: p, pattern: { a, b, ...p } from the original statement + * `{ a, b, ...p } = o`, create `p = __rest(o, ["a", "b"]);`*/ + function createRestCall(value: Expression, elements: EffectiveBindingElement[], location: TextRange): Expression { + const propertyNames: LiteralExpression[] = []; + for (let i = 0; i < elements.length - 1; i++) { + if (isOmittedExpression(elements[i])) { + continue; + } + const str = createSynthesizedNode(SyntaxKind.StringLiteral); + str.pos = location.pos; + str.end = location.end; + str.text = getTextOfPropertyName(getEffectivePropertyNameOfEffectiveBindingElement(elements[i])); + propertyNames.push(str); + } + const args = createSynthesizedNodeArray([value, createArrayLiteral(propertyNames, location)]); + return createCall(createIdentifier("__rest"), undefined, args); + } } diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 1fad209bdb3..9013fabf584 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -105,7 +105,7 @@ namespace ts { * @param node A BinaryExpression node. */ function visitBinaryExpression(node: BinaryExpression): Expression { - if (isDestructuringAssignment(node) && node.left.transformFlags & TransformFlags.AssertESNext) { + if (isDestructuringAssignment(node) && node.left.transformFlags & TransformFlags.ContainsESNext) { return flattenDestructuringAssignment(context, node, /*needsDestructuringValue*/ true, hoistVariableDeclaration, visitor, /*transformRest*/ true); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c814a2b1d46..e8018083783 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1175,16 +1175,18 @@ namespace ts { right: Expression; } - export interface AssignmentExpression extends BinaryExpression { + export type AssignmentOperatorToken = Token; + + export interface AssignmentExpression extends BinaryExpression { left: LeftHandSideExpression; - operatorToken: Token; + operatorToken: TOperator; } - export interface ObjectDestructuringAssignment extends AssignmentExpression { + export interface ObjectDestructuringAssignment extends AssignmentExpression { left: ObjectLiteralExpression; } - export interface ArrayDestructuringAssignment extends AssignmentExpression { + export interface ArrayDestructuringAssignment extends AssignmentExpression { left: ArrayLiteralExpression; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c49c33b857f..f0a666e1531 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3125,19 +3125,21 @@ namespace ts { } } - export function isAssignmentExpression(node: Node): node is AssignmentExpression { + export function isAssignmentExpression(node: Node, excludeCompoundAssignment: true): node is AssignmentExpression; + export function isAssignmentExpression(node: Node, excludeCompoundAssignment?: false): node is AssignmentExpression; + export function isAssignmentExpression(node: Node, excludeCompoundAssignment?: boolean): node is AssignmentExpression { return isBinaryExpression(node) - && isAssignmentOperator(node.operatorToken.kind) + && (excludeCompoundAssignment + ? node.operatorToken.kind === SyntaxKind.EqualsToken + : isAssignmentOperator(node.operatorToken.kind)) && isLeftHandSideExpression(node.left); } export function isDestructuringAssignment(node: Node): node is DestructuringAssignment { - if (isBinaryExpression(node)) { - if (node.operatorToken.kind === SyntaxKind.EqualsToken) { - const kind = node.left.kind; - return kind === SyntaxKind.ObjectLiteralExpression - || kind === SyntaxKind.ArrayLiteralExpression; - } + if (isAssignmentExpression(node, /*excludeCompoundAssignment*/ true)) { + const kind = node.left.kind; + return kind === SyntaxKind.ObjectLiteralExpression + || kind === SyntaxKind.ArrayLiteralExpression; } return false; @@ -3918,6 +3920,14 @@ namespace ts { // Binding patterns + export function isArrayBindingPattern(node: Node): node is ArrayBindingPattern { + return node.kind === SyntaxKind.ArrayBindingPattern; + } + + export function isObjectBindingPattern(node: Node): node is ObjectBindingPattern { + return node.kind === SyntaxKind.ObjectBindingPattern; + } + export function isBindingPattern(node: Node): node is BindingPattern { if (node) { const kind = node.kind; diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index cc27d5c9ddf..ebb3ec01ed3 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -701,6 +701,7 @@ namespace ts { return updateParameter(node, visitNodes((node).decorators, visitor, isDecorator), visitNodes((node).modifiers, visitor, isModifier), + (node).dotDotDotToken, visitNode((node).name, visitor, isBindingName), visitNode((node).type, visitor, isTypeNode, /*optional*/ true), visitNode((node).initializer, visitor, isExpression, /*optional*/ true)); @@ -767,6 +768,7 @@ namespace ts { case SyntaxKind.BindingElement: return updateBindingElement(node, + (node).dotDotDotToken, visitNode((node).propertyName, visitor, isPropertyName, /*optional*/ true), visitNode((node).name, visitor, isBindingName), visitNode((node).initializer, visitor, isExpression, /*optional*/ true)); @@ -1287,6 +1289,13 @@ namespace ts { ? (node: Node, message?: string) => assert(false, message || "Unexpected node.", () => `Node ${formatSyntaxKind(node.kind)} was unexpected.`) : noop; + export const assertEachNode = shouldAssert(AssertionLevel.Normal) + ? (nodes: Node[], test: (node: Node) => boolean, message?: string) => assert( + test === undefined || every(nodes, test), + message || "Unexpected node.", + () => `Node array did not pass test '${getFunctionName(test)}'.`) + : noop; + export const assertNode = shouldAssert(AssertionLevel.Normal) ? (node: Node, test: (node: Node) => boolean, message?: string) => assert( test === undefined || test(node), @@ -1294,6 +1303,27 @@ namespace ts { () => `Node ${formatSyntaxKind(node.kind)} did not pass test '${getFunctionName(test)}'.`) : noop; + export const assertOptionalNode = shouldAssert(AssertionLevel.Normal) + ? (node: Node, test: (node: Node) => boolean, message?: string) => assert( + test === undefined || node === undefined || test(node), + message || "Unexpected node.", + () => `Node ${formatSyntaxKind(node.kind)} did not pass test '${getFunctionName(test)}'.`) + : noop; + + export const assertOptionalToken = shouldAssert(AssertionLevel.Normal) + ? (node: Node, kind: SyntaxKind, message?: string) => assert( + kind === undefined || node === undefined || node.kind === kind, + message || "Unexpected node.", + () => `Node ${formatSyntaxKind(node.kind)} was not a '${formatSyntaxKind(kind)}' token.`) + : noop; + + export const assertMissingNode = shouldAssert(AssertionLevel.Normal) + ? (node: Node, message?: string) => assert( + node === undefined, + message || "Unexpected node.", + () => `Node ${formatSyntaxKind(node.kind)} was unexpected'.`) + : noop; + function getFunctionName(func: Function) { if (typeof func !== "function") { return "";