Safe navigation in destructuring

This commit is contained in:
Ron Buckton
2017-02-23 17:10:56 -08:00
parent 9c0046c057
commit f8f3ba75e8
3 changed files with 221 additions and 36 deletions
+114 -8
View File
@@ -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 };
+2 -1
View File
@@ -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,
+105 -27
View File
@@ -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<Node> {
switch (node.kind) {
case SyntaxKind.ObjectLiteralExpression:
return visitEachChild(node, convertNullPropagatingReferenceToRefInAssignmentPattern, context);
case SyntaxKind.PropertyAssignment:
return updatePropertyAssignment(
<PropertyAssignment>node,
(<PropertyAssignment>node).name,
visitNode((<PropertyAssignment>node).initializer, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression));
case SyntaxKind.SpreadAssignment:
return updateSpreadAssignment(
<SpreadAssignment>node,
visitNode((<SpreadAssignment>node).expression, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression));
case SyntaxKind.ArrayLiteralExpression:
return visitEachChild(node, convertNullPropagatingReferenceToRefInAssignmentPattern, context);
case SyntaxKind.SpreadElement:
return updateSpread(
<SpreadElement>node,
visitNode((<SpreadElement>node).expression, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression));
case SyntaxKind.BinaryExpression:
return updateBinary(
<BinaryExpression>node,
visitNode((<BinaryExpression>node).left, convertNullPropagatingReferenceToRefInAssignmentPattern, isExpression),
(<BinaryExpression>node).right);
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
if (node.flags & NodeFlags.PropagateNull) {
const ref = createRef(<PropertyAccessExpression | ElementAccessExpression>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<T extends Node, U>(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<T extends UnaryExpression>(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(