diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 310730dcdba..561d6426c23 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2402,6 +2402,109 @@ namespace ts { return updated; } + export const enum CreateRefFlags { + Readable = 1 << 0, + Writable = 1 << 1, + ReadWrite = Readable | Writable, + } + + /** + * Creates an expression that can be used to bind a reference to an identifier or property. + * + * For an identifier `x`, this creates an expression similar to the following: + * + * ({ + * get value() { return x; }, + * set value(_a) { x = _a; } + * }) + * + * For a property `x.y`, this creates an expression similar to the following: + * + * (_a = x, { + * get value() { return _a.y; }, + * set value(_b) { _a.y = _b; } + * }) + * + * For an element access `x[y()]`, this creates an expression similar to the following: + * + * (_a = x, _b = y(), { + * get value() { return _a[_b]; }, + * set value(_c) { _a[_b] = _c; } + * }) + */ + export function createRef(node: PropertyAccessExpression | ElementAccessExpression | Identifier, recordTempVariable: (node: Identifier) => void, flags?: CreateRefFlags) { + if (!flags) flags = CreateRefFlags.ReadWrite; + const paramName = flags & CreateRefFlags.Writable ? createTempVariable(/*recordTempVariable*/ undefined) : undefined; + let expressions: Expression[]; + let getValue: Expression; + let putValue: Expression; + if (isPropertyAccessOrElementAccess(node)) { + expressions = []; + const { reference } = createPropertyReference(node, recordTempVariable, expressions, /*captureArgumentExpression*/ true); + // Copy null propagation flags + if (node.flags & NodeFlags.PropagateNull) { + reference.flags |= NodeFlags.PropagateNull; + } + if (flags & CreateRefFlags.Readable) { + getValue = reference + } + if (flags & CreateRefFlags.Writable) { + putValue = setSourceMapRange(createAssignment(reference, paramName), node); + } + } + else { + if (flags & CreateRefFlags.Readable) { + getValue = node; + } + if (flags & CreateRefFlags.Writable) { + putValue = setSourceMapRange(createAssignment(node, paramName), node); + } + } + + const properties: ObjectLiteralElementLike[] = []; + if (getValue) { + properties.push( + setSourceMapRange( + createGetAccessor( + /*decorators*/ undefined, + /*modifiers*/ undefined, + "value", + /*parameters*/ [], + /*type*/ undefined, + createBlock([setSourceMapRange(createReturn(getValue), node)]) + ), + node + ) + ); + } + if (putValue) { + properties.push( + setSourceMapRange( + createSetAccessor( + /*decorators*/ undefined, + /*modifiers*/ undefined, + "value", + [createParameter(/*decorators*/ undefined, /*modifiers*/ undefined, /*dotDotDotToken*/ undefined, paramName)], + createBlock([setSourceMapRange(createStatement(putValue), node)]) + ), + node + ) + ); + } + const refExpression = setSourceMapRange( + setCommentRange( + createObjectLiteral(properties, /*multiLine*/ false), + node + ), + node + ); + if (expressions) { + expressions.push(refExpression); + return inlineExpressions(expressions); + } + return refExpression; + } + /** * Determines whether an expression should be captured in a temp variable as part of an * emit transformation where side-effects could be observed. @@ -2460,7 +2563,7 @@ namespace ts { } // https://tc39.github.io/ecma262/#sec-reference-specification-type - export interface Reference { + export interface PropertyReference { baseValue: Expression; reference: PropertyAccessExpression | ElementAccessExpression; } @@ -2470,26 +2573,29 @@ namespace ts { * @param lhsReference The property or element access expression from which to create the reference. * @param recordTempVariable A callback used to record new temp variables. * @param expressions The array of expressions into which capturing assignments should be added. + * @param captureArgumentExpression A value indicating whether to capture the argumentExpression of an ElementAccessExpression. */ - export function createReference(lhsReference: PropertyAccessExpression | ElementAccessExpression, recordTempVariable: (temp: Identifier) => void, expressions: Expression[]): Reference { - const baseValue = captureExpressionIfNeeded(lhsReference.expression, recordTempVariable, expressions); + export function createPropertyReference(referenceExpression: PropertyAccessExpression | ElementAccessExpression, recordTempVariable: (temp: Identifier) => void, expressions: Expression[], captureArgumentExpression?: boolean): PropertyReference { + const baseValue = captureExpressionIfNeeded(referenceExpression.expression, recordTempVariable, expressions); let reference: PropertyAccessExpression | ElementAccessExpression; - if (isPropertyAccessExpression(lhsReference)) { + if (isPropertyAccessExpression(referenceExpression)) { reference = setTextRange( createPropertyAccess( baseValue, - lhsReference.name + referenceExpression.name ), - lhsReference + referenceExpression ); } else { reference = setTextRange( createElementAccess( baseValue, - lhsReference.argumentExpression + captureArgumentExpression + ? captureExpressionIfNeeded(referenceExpression.argumentExpression, recordTempVariable, expressions) + : referenceExpression.argumentExpression ), - lhsReference + referenceExpression ); } return { baseValue, reference }; diff --git a/src/compiler/transformers/destructuring.ts b/src/compiler/transformers/destructuring.ts index bb129cd1187..bee4e68aadd 100644 --- a/src/compiler/transformers/destructuring.ts +++ b/src/compiler/transformers/destructuring.ts @@ -55,10 +55,11 @@ namespace ts { } let expressions: Expression[]; + const compilerOptions = context.getCompilerOptions(); const flattenContext: FlattenContext = { context, level, - downlevelIteration: context.getCompilerOptions().downlevelIteration, + downlevelIteration: compilerOptions.downlevelIteration, hoistTempVariables: true, emitExpression, emitBindingOrAssignment, diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index baa3ac6d1c4..8846afaec87 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -224,25 +224,95 @@ namespace ts { return visitEachChild(node, noDestructuringValue ? visitorNoDestructuringValue : visitor, context); } + function containsSingleNullPropagatingTarget(element: BindingOrAssignmentElement): boolean { + const target = getTargetOfBindingOrAssignmentElement(element); + if (isBindingOrAssignmentPattern(target)) { + const elements = getElementsOfBindingOrAssignmentPattern(target); + return elements.length === 1 && containsSingleNullPropagatingTarget(elements[0]); + } + return isPropertyAccessOrElementAccess(target) + && (target.flags & NodeFlags.PropagateNull) !== 0; + } + + function convertNullPropagatingReferenceToRefInAssignmentPattern(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.ObjectLiteralExpression: + return visitEachChild(node, convertNullPropagatingReferenceToRefInAssignmentPattern, context); + case SyntaxKind.PropertyAssignment: + return updatePropertyAssignment( + node, + (node).name, + visitNode((node).initializer, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression)); + case SyntaxKind.SpreadAssignment: + return updateSpreadAssignment( + node, + visitNode((node).expression, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression)); + case SyntaxKind.ArrayLiteralExpression: + return visitEachChild(node, convertNullPropagatingReferenceToRefInAssignmentPattern, context); + case SyntaxKind.SpreadElement: + return updateSpread( + node, + visitNode((node).expression, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression)); + case SyntaxKind.BinaryExpression: + return updateBinary( + node, + visitNode((node).left, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression), + (node).right); + + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + if (node.flags & NodeFlags.PropagateNull) { + const ref = createRef(node, hoistVariableDeclaration, CreateRefFlags.Writable); + return setTextRange(createPropertyAccess(ref, "value"), node); + } + + default: + return node; + } + } + /** * Visits a BinaryExpression that contains a destructuring assignment. * * @param node A BinaryExpression node. */ function visitBinaryExpression(node: BinaryExpression, noDestructuringValue: boolean): Expression { - // NOTE: null propagation is not currently handled in destructuring assignments: - // [x?.y] = [1] -> [_a] = _b = [1], x == null ? _a : x.y = _a - // (or)-> [{ set value(_) { x == null ? _ : x.y = _ }}.value] = [1] - if (isDestructuringAssignment(node) && node.left.transformFlags & TransformFlags.ContainsObjectRest) { - return flattenDestructuringAssignment( - node, - visitor, - context, - FlattenLevel.ObjectRest, - !noDestructuringValue - ); + if (isDestructuringAssignment(node)) { + if (languageVersion >= ScriptTarget.ES2015) { + // convert null propagating targets to `ref` bindings + // we don't need to do this for ES5 as the down-leveling handles the binding + node = visitNode(node, convertNullPropagatingReferenceToRefInAssignmentPattern, isBinaryExpression); + } + else { + // The ES5/3 removes the need for a `ref` binding, but we must ensure we + // capture the RHS. + if (containsSingleNullPropagatingTarget(node) && !isIdentifier(node.right)) { + const rhs = createTempVariable(hoistVariableDeclaration); + node = createComma( + createAssignment(rhs, node.right), + updateBinary( + node, + node.left, + rhs + ) + ) as BinaryExpression; + aggregateTransformFlags(node); + } + } } - else if (node.operatorToken.kind === SyntaxKind.CommaToken) { + + if (isDestructuringAssignment(node)) { + if (node.left.transformFlags & TransformFlags.ContainsObjectRest) { + return flattenDestructuringAssignment( + node, + visitor, + context, + FlattenLevel.ObjectRest, + !noDestructuringValue + ); + } + } + if (node.operatorToken.kind === SyntaxKind.CommaToken) { return updateBinary( node, visitNode(node.left, visitorNoDestructuringValue, isExpression), @@ -258,8 +328,10 @@ namespace ts { // x.y?.z = 1 -> _a = x.y, _b = 1, _a == null ? _b : _a.z = _b // x.y?.[z()] = 1 -> _a = x.y, _b = z(), _c = 1, _a == null ? _c : _a[_b] = _c const expressions: Expression[] = []; - const { baseValue, reference } = createReference(referenceExpression, hoistVariableDeclaration, expressions); - const rhsValue = captureExpression(node.right, hoistVariableDeclaration, expressions); + const { baseValue, reference } = createPropertyReference(referenceExpression, hoistVariableDeclaration, expressions); + const rhsValue = isGeneratedIdentifier(node.right) + ? node.right + : captureExpression(node.right, hoistVariableDeclaration, expressions); expressions.push( setTextRange( createConditional( @@ -752,25 +824,31 @@ namespace ts { function propagateNull(finishExpression: (node: T, nullableExpression: Expression, data: U) => Expression, node: T, referenceExpression: Expression, data?: U): Expression { if (node.flags & NodeFlags.PropagateNull) { if (isIdentifier(referenceExpression)) { - return setTextRange( - createConditional( - createEquality(referenceExpression, createNull()), - createVoidZero(), - finishExpression(node, referenceExpression, data) + return setOriginalNode( + setTextRange( + createConditional( + createEquality(referenceExpression, createNull()), + createVoidZero(), + finishExpression(node, referenceExpression, data) + ), + node ), node ); } else { const temp = createTempVariable(hoistVariableDeclaration); - return setTextRange( - createConditional( - createEquality( - createAssignment(temp, referenceExpression), - createNull() + return setOriginalNode( + setTextRange( + createConditional( + createEquality( + createAssignment(temp, referenceExpression), + createNull() + ), + createVoidZero(), + finishExpression(node, temp, data) ), - createVoidZero(), - finishExpression(node, temp, data) + node ), node ); @@ -899,7 +977,7 @@ namespace ts { function visitUnaryMutationExpression(node: T, referenceExpression: Expression, updateNode: (node: T, referenceExpression: Expression) => T): Expression { if (referenceExpression.flags & NodeFlags.PropagateNull && isPropertyAccessOrElementAccess(referenceExpression)) { const expressions: Expression[] = []; - const { baseValue, reference } = createReference(referenceExpression, hoistVariableDeclaration, expressions); + const { baseValue, reference } = createPropertyReference(referenceExpression, hoistVariableDeclaration, expressions); expressions.push( setTextRange( createConditional(