From 9c0046c05708cd7e347a9e0d79e48fc369023a04 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 23 Feb 2017 14:32:24 -0800 Subject: [PATCH] Emit and check for safe navigation --- src/compiler/checker.ts | 130 ++++++++++---- src/compiler/factory.ts | 229 ++++++++++++++++-------- src/compiler/parser.ts | 10 +- src/compiler/transformers/esnext.ts | 228 +++++++---------------- src/compiler/transformers/generators.ts | 2 +- src/compiler/types.ts | 14 +- src/compiler/utilities.ts | 28 ++- 7 files changed, 350 insertions(+), 291 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 788a61dad28..f500dc79526 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8917,6 +8917,32 @@ namespace ts { return strictNullChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; } + function getNullPropagatingType(type: Type) { + return strictNullChecks && (getTypeFacts(type) & TypeFacts.EQUndefinedOrNull) === 0 + ? neverType + : undefinedType; + } + + function propagateNullType(type: Type, propagatingType: Type) { + return propagatingType === neverType ? type : getUnionType([type, propagatingType]); + } + + function propagateNullReturnType(signature: Signature, propagatingType: Type) { + if (propagatingType === neverType) { + return signature; + } + + const returnType = getReturnTypeOfSignature(signature); + const propagatingReturnType = propagateNullType(returnType, propagatingType); + if (returnType === propagatingReturnType) { + return signature; + } + + signature = cloneSignature(signature); + signature.resolvedReturnType = propagatingReturnType; + return signature; + } + /** * Return true if type was inferred from an object literal or written as an object type literal * with no call or construct signatures. @@ -13096,8 +13122,8 @@ namespace ts { } function checkNonNullType(type: Type, errorNode: Node): Type { - const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable; - if (kind) { + const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags); + if (kind & TypeFlags.Nullable) { error(errorNode, kind & TypeFlags.Undefined ? kind & TypeFlags.Null ? Diagnostics.Object_is_possibly_null_or_undefined : Diagnostics.Object_is_possibly_undefined : @@ -13156,7 +13182,11 @@ namespace ts { } function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) { - const type = checkNonNullExpression(left); + const propagateNull = node.flags & NodeFlags.PropagateNull; + const objectType = propagateNull ? checkExpression(left) : checkNonNullExpression(left); + const type = propagateNull ? getNonNullableType(objectType) : objectType; + const propagatingType = propagateNull ? getNullPropagatingType(objectType) : neverType; + if (isTypeAny(type) || type === silentNeverType) { return type; } @@ -13205,10 +13235,10 @@ namespace ts { if (node.kind !== SyntaxKind.PropertyAccessExpression || assignmentKind === AssignmentKind.Definite || !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) && !(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) { - return propType; + return propagateNullType(propType, propagatingType); } const flowType = getFlowTypeOfReference(node, propType, /*assumeInitialized*/ true, /*flowContainer*/ undefined); - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + return propagateNullType(assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType, propagatingType); } function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean { @@ -13279,7 +13309,10 @@ namespace ts { function checkIndexedAccess(node: ElementAccessExpression): Type { checkGrammarNullPropagation(node); - const objectType = checkNonNullExpression(node.expression); + const propagateNull = node.flags & NodeFlags.PropagateNull; + const objectType = propagateNull ? checkExpression(node.expression) : checkNonNullExpression(node.expression); + const type = propagateNull ? getNonNullableType(objectType) : objectType; + const propagatingType = propagateNull ? getNullPropagatingType(objectType) : neverType; const indexExpression = node.argumentExpression; if (!indexExpression) { @@ -13299,16 +13332,16 @@ namespace ts { const indexType = isForInVariableForNumericPropertyNames(indexExpression) ? numberType : checkExpression(indexExpression); - if (objectType === unknownType || objectType === silentNeverType) { - return objectType; + if (type === unknownType || type === silentNeverType) { + return type; } - if (isConstEnumObjectType(objectType) && indexExpression.kind !== SyntaxKind.StringLiteral) { + if (isConstEnumObjectType(type) && indexExpression.kind !== SyntaxKind.StringLiteral) { error(indexExpression, Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal); return unknownType; } - return checkIndexedAccessIndexType(getIndexedAccessType(objectType, indexType, node), node); + return propagateNullType(checkIndexedAccessIndexType(getIndexedAccessType(type, indexType, node), node), propagatingType); } function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean { @@ -14048,7 +14081,7 @@ namespace ts { } } - function resolveCall(node: CallLikeExpression, signatures: Signature[], candidatesOutArray: Signature[], headMessage?: DiagnosticMessage): Signature { + function resolveCall(node: CallLikeExpression, signatures: Signature[], candidatesOutArray: Signature[], propagatingType?: Type, headMessage?: DiagnosticMessage): Signature { const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression; const isDecorator = node.kind === SyntaxKind.Decorator; const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node); @@ -14154,7 +14187,7 @@ namespace ts { result = chooseOverload(candidates, assignableRelation, signatureHelpTrailingComma); } if (result) { - return result; + return propagateNullReturnType(result, propagatingType || neverType); } // No signatures were applicable. Now report errors based on the last applicable signature with @@ -14209,7 +14242,7 @@ namespace ts { if (candidate.typeParameters && typeArguments) { candidate = getSignatureInstantiation(candidate, map(typeArguments, getTypeFromTypeNode)); } - return candidate; + return propagateNullReturnType(candidate, propagatingType || neverType); } } } @@ -14309,12 +14342,15 @@ namespace ts { return resolveUntypedCall(node); } - const funcType = checkNonNullExpression(node.expression); - if (funcType === silentNeverType) { + + const propagateNull = node.flags & NodeFlags.PropagateNull; + const funcType = propagateNull ? checkExpression(node.expression) : checkNonNullExpression(node.expression); + const type = propagateNull ? getNonNullableType(funcType) : funcType; + if (type === silentNeverType) { return silentNeverSignature; } - const apparentType = getApparentType(funcType); + const apparentType = getApparentType(type); if (apparentType === unknownType) { // Another error has already been reported return resolveErrorCall(node); @@ -14330,10 +14366,10 @@ namespace ts { // TS 1.0 Spec: 4.12 // In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual // types are provided for the argument expressions, and the result is always of type Any. - if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, constructSignatures.length)) { + if (isUntypedFunctionCall(type, apparentType, callSignatures.length, constructSignatures.length)) { // The unknownType indicates that an error already occurred (and was reported). No // need to report another error in this case. - if (funcType !== unknownType && node.typeArguments) { + if (type !== unknownType && node.typeArguments) { error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); } return resolveUntypedCall(node); @@ -14343,14 +14379,16 @@ namespace ts { // with multiple call signatures. if (!callSignatures.length) { if (constructSignatures.length) { - error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); + error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(type)); } else { error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType)); } return resolveErrorCall(node); } - return resolveCall(node, callSignatures, candidatesOutArray); + + const propagatingType = propagateNull ? getNullPropagatingType(funcType) : neverType; + return resolveCall(node, callSignatures, candidatesOutArray, propagatingType); } /** @@ -14384,8 +14422,10 @@ namespace ts { } } - let expressionType = checkNonNullExpression(node.expression); - if (expressionType === silentNeverType) { + const propagateNull = node.flags & NodeFlags.PropagateNull; + const funcType = propagateNull ? checkExpression(node.expression) : checkNonNullExpression(node.expression); + const type = propagateNull ? getNonNullableType(funcType) : funcType; + if (type === silentNeverType) { return silentNeverSignature; } @@ -14394,8 +14434,8 @@ namespace ts { // function call, but using the construct signatures as the initial set of candidate // signatures for overload resolution. The result type of the function call becomes // the result type of the operation. - expressionType = getApparentType(expressionType); - if (expressionType === unknownType) { + const apparentType = getApparentType(type); + if (apparentType === unknownType) { // Another error has already been reported return resolveErrorCall(node); } @@ -14404,7 +14444,7 @@ namespace ts { // Note, only class declarations can be declared abstract. // In the case of a merged class-module or class-interface declaration, // only the class declaration node will have the Abstract flag set. - const valueDecl = expressionType.symbol && getClassLikeDeclarationOfSymbol(expressionType.symbol); + const valueDecl = apparentType.symbol && getClassLikeDeclarationOfSymbol(apparentType.symbol); if (valueDecl && getModifierFlags(valueDecl) & ModifierFlags.Abstract) { error(node, Diagnostics.Cannot_create_an_instance_of_the_abstract_class_0, declarationNameToString(valueDecl.name)); return resolveErrorCall(node); @@ -14413,7 +14453,7 @@ namespace ts { // TS 1.0 spec: 4.11 // If expressionType is of type Any, Args can be any argument // list and the result of the operation is of type Any. - if (isTypeAny(expressionType)) { + if (isTypeAny(apparentType)) { if (node.typeArguments) { error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); } @@ -14424,19 +14464,21 @@ namespace ts { // but we are not including construct signatures that may have been added to the Object or // Function interface, since they have none by default. This is a bit of a leap of faith // that the user will not add any. - const constructSignatures = getSignaturesOfType(expressionType, SignatureKind.Construct); + const constructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct); if (constructSignatures.length) { if (!isConstructorAccessible(node, constructSignatures[0])) { return resolveErrorCall(node); } - return resolveCall(node, constructSignatures, candidatesOutArray); + + const propagatingType = propagateNull ? getNullPropagatingType(funcType) : neverType; + return resolveCall(node, constructSignatures, candidatesOutArray, propagatingType); } // If expressionType's apparent type is an object type with no construct signatures but // one or more call signatures, the expression is processed as a function call. A compile-time // error occurs if the result of the function call is not Void. The type of the result of the // operation is Any. It is an error to have a Void this type. - const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call); + const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); if (callSignatures.length) { const signature = resolveCall(node, callSignatures, candidatesOutArray); if (getReturnTypeOfSignature(signature) !== voidType) { @@ -14566,7 +14608,7 @@ namespace ts { return resolveErrorCall(node); } - return resolveCall(node, callSignatures, candidatesOutArray, headMessage); + return resolveCall(node, callSignatures, candidatesOutArray, /*propagatingType*/ undefined, headMessage); } /** @@ -15405,13 +15447,15 @@ namespace ts { booleanType; case SyntaxKind.PlusPlusToken: case SyntaxKind.MinusMinusToken: - const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type); + const propagateNull = node.operand.flags & NodeFlags.PropagateNull; + const nonNullType = propagateNull ? getNonNullableType(operandType) : checkNonNullType(operandType, node.operand); + const propagatingType = propagateNull ? getNullPropagatingType(operandType) : neverType; + const ok = checkArithmeticOperandType(node.operand, nonNullType, Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type); if (ok) { // run check only if former checks succeeded to avoid reporting cascading errors checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access); } - return numberType; + return propagateNullType(numberType, propagatingType); } return unknownType; } @@ -15421,13 +15465,15 @@ namespace ts { if (operandType === silentNeverType) { return silentNeverType; } - const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type); + const propagateNull = node.operand.flags & NodeFlags.PropagateNull; + const nonNullType = propagateNull ? getNonNullableType(operandType) : checkNonNullType(operandType, node.operand); + const propagatingType = propagateNull ? getNullPropagatingType(operandType) : neverType; + const ok = checkArithmeticOperandType(node.operand, nonNullType, Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type); if (ok) { // run check only if former checks succeeded to avoid reporting cascading errors checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access); } - return numberType; + return propagateNullType(numberType, propagatingType); } // Return true if type might be of the given kind. A union or intersection type might be of a given @@ -15772,6 +15818,12 @@ namespace ts { } let leftType = checkExpression(left, contextualMapper); let rightType = checkExpression(right, contextualMapper); + let propagateNull = operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment && left.flags & NodeFlags.PropagateNull; + let propagatingType = propagateNull ? getNullPropagatingType(leftType) : neverType; + if (propagateNull) { + leftType = getNonNullableType(leftType); + } + switch (operator) { case SyntaxKind.AsteriskToken: case SyntaxKind.AsteriskAsteriskToken: @@ -15819,7 +15871,7 @@ namespace ts { } } - return numberType; + return propagateNullType(numberType, propagatingType); case SyntaxKind.PlusToken: case SyntaxKind.PlusEqualsToken: if (leftType === silentNeverType || rightType === silentNeverType) { @@ -15850,7 +15902,7 @@ namespace ts { // Symbols are not allowed at all in arithmetic expressions if (resultType && !checkForDisallowedESSymbolOperand(operator)) { - return resultType; + return propagateNullType(resultType, propagatingType); } } @@ -15862,7 +15914,7 @@ namespace ts { if (operator === SyntaxKind.PlusEqualsToken) { checkAssignmentOperator(resultType); } - return resultType; + return propagateNullType(resultType, propagatingType); case SyntaxKind.LessThanToken: case SyntaxKind.GreaterThanToken: case SyntaxKind.LessThanEqualsToken: diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 14aa08ce254..310730dcdba 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1739,7 +1739,7 @@ namespace ts { } export function createAssignment(left: ObjectLiteralExpression | ArrayLiteralExpression, right: Expression): DestructuringAssignment; - export function createAssignment(left: Expression, right: Expression): BinaryExpression; + export function createAssignment(left: Expression, right: Expression): AssignmentExpression; export function createAssignment(left: Expression, right: Expression) { return createBinary(left, SyntaxKind.EqualsToken, right); } @@ -1748,6 +1748,10 @@ namespace ts { return createBinary(left, SyntaxKind.EqualsEqualsToken, right); } + export function createInequality(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.ExclamationEqualsToken, right); + } + export function createStrictEquality(left: Expression, right: Expression) { return createBinary(left, SyntaxKind.EqualsEqualsEqualsToken, right); } @@ -2398,26 +2402,30 @@ namespace ts { return updated; } - export interface CallBinding { - target: LeftHandSideExpression; - thisArg: Expression; - } - - function shouldBeCapturedInTempVariable(node: Expression, cacheIdentifiers: boolean): boolean { - const target = skipParentheses(node); + /** + * Determines whether an expression should be captured in a temp variable as part of an + * emit transformation where side-effects could be observed. + * @param node The expression to be tested. + * @param captureIdentifiers A value that indicates whether identifiers should be captured + * as well. + */ + function shouldBeCapturedInTempVariable(node: Expression, captureIdentifiers?: boolean): boolean { + const target = skipOuterExpressions(node); switch (target.kind) { case SyntaxKind.Identifier: - return cacheIdentifiers; + return captureIdentifiers; case SyntaxKind.ThisKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: return false; + case SyntaxKind.VoidExpression: + return (node).expression.kind === SyntaxKind.NumericLiteral; case SyntaxKind.ArrayLiteralExpression: - const elements = (target).elements; - if (elements.length === 0) { - return false; - } - return true; + return (target).elements.length > 0; case SyntaxKind.ObjectLiteralExpression: return (target).properties.length > 0; default: @@ -2425,82 +2433,151 @@ namespace ts { } } - export function createCallBinding(expression: Expression, recordTempVariable: (temp: Identifier) => void, languageVersion?: ScriptTarget, cacheIdentifiers?: boolean): CallBinding { - const callee = skipOuterExpressions(expression, OuterExpressionKinds.All); + /** + * Captures an expression in a temp variable. + * @param expression The expression to capture. + * @param recordTempVariable A callback used to record new temp variables. + * @param expressions The array of expressions into which capturing assignments should be added. + */ + export function captureExpression(expression: Expression, recordTempVariable: (temp: Identifier) => void, expressions: Expression[]): Expression { + const temp = setTextRange(createTempVariable(recordTempVariable), expression); + expressions.push(setTextRange(createAssignment(temp, expression), expression)); + return temp; + } + + /** + * Captures an expression in a temp variable if it possibly contains side-effects. + * @param expression The expression to capture. + * @param recordTempVariable A callback used to record new temp variables. + * @param expressions The array of expressions into which capturing assignments should be added. + * @param captureIdentifiers A value indicating whether identifiers should be captured as well. + */ + export function captureExpressionIfNeeded(expression: Expression, recordTempVariable: (temp: Identifier) => void, expressions: Expression[], captureIdentifiers?: boolean): Expression { + if (shouldBeCapturedInTempVariable(expression, captureIdentifiers)) { + return captureExpression(expression, recordTempVariable, expressions); + } + return expression; + } + + // https://tc39.github.io/ecma262/#sec-reference-specification-type + export interface Reference { + baseValue: Expression; + reference: PropertyAccessExpression | ElementAccessExpression; + } + + /** + * Creates an approximation of an ECMAScript `Reference` specification type. + * @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. + */ + export function createReference(lhsReference: PropertyAccessExpression | ElementAccessExpression, recordTempVariable: (temp: Identifier) => void, expressions: Expression[]): Reference { + const baseValue = captureExpressionIfNeeded(lhsReference.expression, recordTempVariable, expressions); + let reference: PropertyAccessExpression | ElementAccessExpression; + if (isPropertyAccessExpression(lhsReference)) { + reference = setTextRange( + createPropertyAccess( + baseValue, + lhsReference.name + ), + lhsReference + ); + } + else { + reference = setTextRange( + createElementAccess( + baseValue, + lhsReference.argumentExpression + ), + lhsReference + ); + } + return { baseValue, reference }; + } + + export interface CallBinding { + target: LeftHandSideExpression; + thisArg: Expression; + } + + /** + * Returns a `CallBinding` for an expression that contains a reference to an expression to call + * and the `this` to be used for the call. + * @param expression The callee of a CallExpression. + * @param recordTempVariable A callback used to record new temp variables. + * @param languageVersion An optional `ScriptTarget` used to control the behavior of a `super` callee. + * @param captureIdentifiers A value indicating whether identifiers should be captured as well. + */ + export function createCallBinding(expression: Expression, recordTempVariable: (temp: Identifier) => void, languageVersion?: ScriptTarget, captureIdentifiers?: boolean): CallBinding { + const callee = skipOuterExpressions(expression); let thisArg: Expression; let target: LeftHandSideExpression; if (isSuperProperty(callee)) { thisArg = createThis(); target = callee; } - else if (callee.kind === SyntaxKind.SuperKeyword) { + else if (isSuper(callee)) { thisArg = createThis(); target = languageVersion < ScriptTarget.ES2015 ? setTextRange(createIdentifier("_super"), callee) - : callee; + : callee; } else if (getEmitFlags(callee) & EmitFlags.HelperName) { thisArg = createVoidZero(); target = parenthesizeForAccess(callee); } - else { - switch (callee.kind) { - case SyntaxKind.PropertyAccessExpression: { - if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { - // for `a.b()` target is `(_a = a).b` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createPropertyAccess( - setTextRange( - createAssignment( - thisArg, - (callee).expression - ), - (callee).expression + else if (isPropertyAccessExpression(callee)) { + if (shouldBeCapturedInTempVariable(callee.expression, captureIdentifiers)) { + // for `a.b()` target is `(_a = a).b` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = setTextRange( + createPropertyAccess( + setTextRange( + createAssignment( + thisArg, + callee.expression ), - (callee).name, - callee.flags & NodeFlags.PropagateNull - ); - setTextRange(target, callee); - } - else { - thisArg = (callee).expression; - target = callee; - } - break; - } - - case SyntaxKind.ElementAccessExpression: { - if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { - // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createElementAccess( - setTextRange( - createAssignment( - thisArg, - (callee).expression - ), - (callee).expression - ), - (callee).argumentExpression, - callee.flags & NodeFlags.PropagateNull - ); - setTextRange(target, callee); - } - else { - thisArg = (callee).expression; - target = callee; - } - - break; - } - - default: { - // for `a()` target is `a` and thisArg is `void 0` - thisArg = createVoidZero(); - target = parenthesizeForAccess(expression); - break; - } + callee.expression + ), + callee.name, + callee.flags & NodeFlags.PropagateNull + ), + callee + ); } + else { + thisArg = callee.expression; + target = callee; + } + } + else if (isElementAccessExpression(callee)) { + if (shouldBeCapturedInTempVariable(callee.expression, captureIdentifiers)) { + // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = setTextRange( + createElementAccess( + setTextRange( + createAssignment( + thisArg, + callee.expression + ), + callee.expression + ), + callee.argumentExpression, + callee.flags & NodeFlags.PropagateNull + ), + callee + ); + } + else { + thisArg = callee.expression; + target = callee; + } + } + else { + // for `a()` target is `a` and thisArg is `void 0` + thisArg = createVoidZero(); + target = parenthesizeForAccess(expression); } return { target, thisArg }; @@ -3271,8 +3348,8 @@ namespace ts { export function skipAssertions(node: Expression): Expression; export function skipAssertions(node: Node): Node; export function skipAssertions(node: Node): Node { - while (isAssertionExpression(node)) { - node = (node).expression; + while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) { + node = (node).expression; } return node; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 75b337095b4..815ef137beb 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -3509,7 +3509,7 @@ namespace ts { * 5) --UnaryExpression[?Yield] */ if (isUpdateExpression()) { - const incrementExpression = parseIncrementExpression(); + const incrementExpression = parseUpdateExpression(); return token() === SyntaxKind.AsteriskAsteriskToken ? parseBinaryExpressionRest(getBinaryOperatorPrecedence(), incrementExpression) : incrementExpression; @@ -3577,7 +3577,7 @@ namespace ts { return parseAwaitExpression(); } default: - return parseIncrementExpression(); + return parseUpdateExpression(); } } @@ -3617,9 +3617,9 @@ namespace ts { } /** - * Parse ES7 IncrementExpression. IncrementExpression is used instead of ES6's PostFixExpression. + * Parse ES7 UpdateExpression. UpdateExpression is used instead of ES6's PostFixExpression. * - * ES7 IncrementExpression[yield]: + * ES7 UpdateExpression[yield]: * 1) LeftHandSideExpression[?yield] * 2) LeftHandSideExpression[?yield] [[no LineTerminator here]]++ * 3) LeftHandSideExpression[?yield] [[no LineTerminator here]]-- @@ -3627,7 +3627,7 @@ namespace ts { * 5) --LeftHandSideExpression[?yield] * In TypeScript (2), (3) are parsed as PostfixUnaryExpression. (4), (5) are parsed as PrefixUnaryExpression */ - function parseIncrementExpression(): IncrementExpression { + function parseUpdateExpression(): UpdateExpression { if (token() === SyntaxKind.PlusPlusToken || token() === SyntaxKind.MinusMinusToken) { const node = createNode(SyntaxKind.PrefixUnaryExpression); node.operator = token(); diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 2b652a82f0f..baa3ac6d1c4 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -230,6 +230,9 @@ namespace ts { * @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, @@ -247,19 +250,30 @@ namespace ts { ); } - const left = visitNode(node.left, visitor, isExpression); - const right = visitNode(node.right, visitor, isExpression); if (isAssignmentExpression(node)) { - const nilReference = getNilReference(left); - if (nilReference) { - return updateNilReference( - nilReference, - updateBinary(node, nilReference.whenFalse, right) + const referenceExpression = skipOuterExpressions(node.left); + if (referenceExpression.flags & NodeFlags.PropagateNull && isPropertyAccessOrElementAccess(referenceExpression)) { + // x?.y = 1 -> _a = 1, x == null ? _a : x.y = _a + // x?.[y()] = 1 -> _a = y(), _b = 1, x == null ? _b : x[_a] = _b + // 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); + expressions.push( + setTextRange( + createConditional( + createEquality(baseValue, createNull()), + rhsValue, + updateBinary(node, reference, rhsValue) + ), + node + ) ); + return visitNode(inlineExpressions(expressions), visitor, isExpression); } } - - return updateBinary(node, left, right); + return visitEachChild(node, visitor, context); } /** @@ -733,38 +747,16 @@ namespace ts { && node.expression.kind !== SyntaxKind.SuperKeyword; } - function getNilReference(expression: Expression): ConditionalExpression { - expression = skipOuterExpressions(expression); - return isConditionalExpression(expression) - && isBinaryExpression(expression.condition) - && expression.condition.operatorToken.kind === SyntaxKind.EqualsEqualsToken - && expression.condition.right.kind === SyntaxKind.NullKeyword - && isVoidZero(expression.whenTrue) - ? expression - : undefined; - } - - function updateNilReference(expression: ConditionalExpression, whenNotNil: Expression) { - return setTextRange( - createConditional( - expression.condition, - expression.whenTrue, - whenNotNil - ), - whenNotNil - ); - } - - function propagateNull(finishExpression: (node: T, nullableExpression: Expression) => Expression, node: T, nullableExpression: Expression): Expression; - function propagateNull(finishExpression: (node: T, nullableExpression: Expression, data: U) => Expression, node: T, nullableExpression: Expression, data: U): Expression; - function propagateNull(finishExpression: (node: T, nullableExpression: Expression, data: U) => Expression, node: T, nullableExpression: Expression, data?: U): Expression { + function propagateNull(finishExpression: (node: T, nullableExpression: Expression) => Expression, node: T, referenceExpression: Expression): Expression; + function propagateNull(finishExpression: (node: T, nullableExpression: Expression, data: U) => Expression, node: T, referenceExpression: Expression, data: U): Expression; + function propagateNull(finishExpression: (node: T, nullableExpression: Expression, data: U) => Expression, node: T, referenceExpression: Expression, data?: U): Expression { if (node.flags & NodeFlags.PropagateNull) { - if (isIdentifier(nullableExpression)) { + if (isIdentifier(referenceExpression)) { return setTextRange( createConditional( - createEquality(nullableExpression, createNull()), + createEquality(referenceExpression, createNull()), createVoidZero(), - finishExpression(node, nullableExpression, data) + finishExpression(node, referenceExpression, data) ), node ); @@ -774,7 +766,7 @@ namespace ts { return setTextRange( createConditional( createEquality( - createAssignment(temp, nullableExpression), + createAssignment(temp, referenceExpression), createNull() ), createVoidZero(), @@ -784,7 +776,7 @@ namespace ts { ); } } - return finishExpression(node, nullableExpression, data); + return finishExpression(node, referenceExpression, data); } function visitCallExpression(node: CallExpression): Expression { @@ -800,30 +792,7 @@ namespace ts { return propagateNull(finishNullableCallExpression, node, visitNode(target, visitor, isExpression), visitNode(thisArg, visitor, isExpression)); } - const expression = visitNode(node.expression, visitor, isExpression); - const argumentsArray = visitNodes(node.arguments, visitor, isExpression); - const nilReference = getNilReference(expression); - if (nilReference) { - // NilReference shortcut in expression - // x?.y() -> x == null ? void 0 : x.y(); - // x?.[y]() -> x == null ? void 0 : x[y](); - return updateNilReference( - nilReference, - updateCall( - node, - nilReference.whenFalse, - /*typeArguments*/ undefined, - argumentsArray - ) - ); - } - - return updateCall( - node, - expression, - /*typeArguments*/ undefined, - argumentsArray - ); + return visitEachChild(node, visitor, context); } function finishNullableCallExpression(node: CallExpression, target: Expression, thisArg: Expression) { @@ -852,7 +821,6 @@ namespace ts { } function visitNewExpression(node: NewExpression): Expression { - const expression = visitNode(node.expression, visitor, isExpression); if (isNullPropagatingExpression(node)) { // null propagation in new: // new x?.() -> x == null ? void 0 : new x(); @@ -861,31 +829,10 @@ namespace ts { // new x[y]?.() -> (_a = x[y]) == null ? void 0 : new _a(); // new (x.y)?.() -> (_a = x.y) == null ? void 0 : new _a(); // new (x[y])?.() -> (_a = x[y]) == null ? void 0 : new _a(); - return propagateNull(finishNullableNewExpression, node, expression); + return propagateNull(finishNullableNewExpression, node, visitNode(node.expression, visitor, isExpression)); } - const argumentsArray = visitNodes(node.arguments, visitor, isExpression); - const nilReference = getNilReference(expression); - if (nilReference) { - // NilReference shortcut in expression - // new x?.y() -> x == null ? void 0 : new x.y(); - // new x?.[y]() -> x == null ? void 0 : new x[y](); - return updateNilReference( - nilReference, - updateNew( - node, - nilReference.whenTrue, - /*typeArguments*/ undefined, - argumentsArray - ) - ); - } - - return updateNew( - node, - expression, - /*typeArguments*/ undefined, - argumentsArray); + return visitEachChild(node, visitor, context); } function finishNullableNewExpression(node: NewExpression, expression: Expression) { @@ -903,35 +850,14 @@ namespace ts { } function visitPropertyAccess(node: PropertyAccessExpression): Expression { - const expression = visitNode(node.expression, visitor, isExpression); if (isNullPropagatingExpression(node)) { // null propagation in property access // x?.y -> x == null ? void 0 : x.y; // x.y?.z -> (_a = x.y) == null ? void 0 : _a.z; - return propagateNull(finishNullablePropertyAccess, node, expression); + return propagateNull(finishNullablePropertyAccess, node, visitNode(node.expression, visitor, isExpression)); } - const name = visitNode(node.name, visitor, isIdentifier); - const nilReference = getNilReference(expression); - if (nilReference) { - // NilReference shortcut in expression - // x?.y.z -> x == null ? void 0 : x.y.z; - // x.y?.z.a -> (_a = x.y) == null ? void 0 : _a.z.a; - return updateNilReference( - nilReference, - updatePropertyAccess( - node, - nilReference.whenFalse, - name - ) - ); - } - - return updatePropertyAccess( - node, - expression, - name - ); + return visitEachChild(node, visitor, context); } function finishNullablePropertyAccess(node: PropertyAccessExpression, expression: Expression) { @@ -948,35 +874,13 @@ namespace ts { } function visitElementAccess(node: ElementAccessExpression): Expression { - const expression = visitNode(node.expression, visitor, isExpression); if (isNullPropagatingExpression(node)) { // null propagation in element access // x?.[y] -> x == null ? void 0 : x.[y]; // x.y?.[z] -> (_a = x.y) == null ? void 0 : _a.[z]; - return propagateNull(finishNullableElementAccess, node, expression); + return propagateNull(finishNullableElementAccess, node, visitNode(node.expression, visitor, isExpression)); } - - const argumentExpression = visitNode(node.argumentExpression, visitor, isExpression); - const nilReference = getNilReference(expression); - if (nilReference) { - // NilReference shortcut in expression - // x?.y.[z] -> x == null ? void 0 : x.y.[z]; - // x.y?.z.[a] -> (_a = x.y) == null ? void 0 : _a.z.[a]; - return updateNilReference( - nilReference, - updateElementAccess( - node, - nilReference.whenFalse, - argumentExpression - ) - ); - } - - return updateElementAccess( - node, - expression, - argumentExpression - ); + return visitEachChild(node, visitor, context); } function finishNullableElementAccess(node: ElementAccessExpression, expression: Expression) { @@ -992,44 +896,44 @@ namespace ts { ); } - function visitDelete(node: DeleteExpression) { - const expression = visitNode(node.expression, visitor, isExpression); - const nilReference = getNilReference(expression); - if (nilReference) { - return setTextRange( - createConditional( - nilReference.condition, - createTrue(), - updateDelete(node, nilReference.whenFalse) - ), - node + 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); + expressions.push( + setTextRange( + createConditional( + createEquality(baseValue, createNull()), + createVoidZero(), + updateNode( + node, + reference + ) + ), + node + ) ); + return visitNode(inlineExpressions(expressions), visitor, isExpression); } - return updateDelete(node, expression); + return visitEachChild(node, visitor, context); + } + + function visitDelete(node: DeleteExpression) { + return visitUnaryMutationExpression(node, node.expression, updateDelete); } function visitPrefix(node: PrefixUnaryExpression) { - const operand = visitNode(node.operand, visitor, isExpression); - const nilReference = getNilReference(operand); - if (nilReference) { - return updateNilReference( - nilReference, - updatePrefix(node, nilReference.whenFalse) - ); + if (isPrefixOrPostfixUpdateExpression(node)) { + return visitUnaryMutationExpression(node, node.operand, updatePrefix); } - return updatePrefix(node, operand); + return visitEachChild(node, visitor, context); } function visitPostfix(node: PostfixUnaryExpression) { - const operand = visitNode(node.operand, visitor, isExpression); - const nilReference = getNilReference(operand); - if (nilReference) { - return updateNilReference( - nilReference, - updatePostfix(node, nilReference.whenFalse) - ); + if (isPrefixOrPostfixUpdateExpression(node)) { + return visitUnaryMutationExpression(node, node.operand, updatePostfix); } - return updatePostfix(node, operand); + return visitEachChild(node, visitor, context); } function enableSubstitutionForAsyncMethodsWithSuper() { diff --git a/src/compiler/transformers/generators.ts b/src/compiler/transformers/generators.ts index ff832188b5b..a4cb1e4f2d4 100644 --- a/src/compiler/transformers/generators.ts +++ b/src/compiler/transformers/generators.ts @@ -1120,7 +1120,7 @@ namespace ts { // .mark resumeLabel // _b.apply(_a, _c.concat([%sent%, 2])); - const { target, thisArg } = createCallBinding(node.expression, hoistVariableDeclaration, languageVersion, /*cacheIdentifiers*/ true); + const { target, thisArg } = createCallBinding(node.expression, hoistVariableDeclaration, languageVersion, /*captureIdentifiers*/ true); return setOriginalNode( createFunctionApply( cacheExpression(visitNode(target, visitor, isLeftHandSideExpression)), diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1045b35caf8..cc6d9859f93 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -980,8 +980,8 @@ _unaryExpressionBrand: any; } - export interface IncrementExpression extends UnaryExpression { - _incrementExpressionBrand: any; + export interface UpdateExpression extends UnaryExpression { + _updateExpressionBrand: any; } // see: https://tc39.github.io/ecma262/#prod-UpdateExpression @@ -995,25 +995,29 @@ | SyntaxKind.ExclamationToken ; - export interface PrefixUnaryExpression extends IncrementExpression { + export interface PrefixUnaryExpression extends UpdateExpression { kind: SyntaxKind.PrefixUnaryExpression; operator: PrefixUnaryOperator; operand: UnaryExpression; } + export interface PrefixUnaryUpdateExpression extends PrefixUnaryExpression { + operator: SyntaxKind.PlusPlusToken | SyntaxKind.MinusMinusToken; + } + // see: https://tc39.github.io/ecma262/#prod-UpdateExpression export type PostfixUnaryOperator = SyntaxKind.PlusPlusToken | SyntaxKind.MinusMinusToken ; - export interface PostfixUnaryExpression extends IncrementExpression { + export interface PostfixUnaryExpression extends UpdateExpression { kind: SyntaxKind.PostfixUnaryExpression; operand: LeftHandSideExpression; operator: PostfixUnaryOperator; } - export interface LeftHandSideExpression extends IncrementExpression { + export interface LeftHandSideExpression extends UpdateExpression { _leftHandSideExpressionBrand: any; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8263cd16d12..f9f537b30e8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1153,9 +1153,12 @@ namespace ts { * Determines whether a node is a property or element access expression for super. */ export function isSuperProperty(node: Node): node is SuperProperty { - const kind = node.kind; - return (kind === SyntaxKind.PropertyAccessExpression || kind === SyntaxKind.ElementAccessExpression) - && (node).expression.kind === SyntaxKind.SuperKeyword; + return isPropertyAccessOrElementAccess(node) + && isSuper(node.expression); + } + + export function isSuper(node: Node): node is SuperExpression { + return node.kind === SyntaxKind.SuperKeyword; } export function getEntityNameFromTypeNode(node: TypeNode): EntityNameOrEntityNameExpression { @@ -3104,6 +3107,19 @@ namespace ts { return false; } + export function isPrefixOrPostfixUpdateExpression(node: Node): node is PrefixUnaryUpdateExpression | PostfixUnaryExpression { + switch (node.kind) { + case SyntaxKind.PrefixUnaryExpression: + case SyntaxKind.PostfixUnaryExpression: + switch ((node).operator) { + case SyntaxKind.PlusPlusToken: + case SyntaxKind.MinusMinusToken: + return true; + } + } + return false; + } + // Returns false if this heritage clause element's expression contains something unsupported // (i.e. not a name or dotted name). export function isSupportedExpressionWithTypeArguments(node: ExpressionWithTypeArguments): boolean { @@ -3805,6 +3821,12 @@ namespace ts { return node.kind === SyntaxKind.ElementAccessExpression; } + export function isPropertyAccessOrElementAccess(node: Node): node is PropertyAccessExpression | ElementAccessExpression { + const kind = node.kind; + return kind === SyntaxKind.PropertyAccessExpression + || kind === SyntaxKind.ElementAccessExpression; + } + export function isBinaryExpression(node: Node): node is BinaryExpression { return node.kind === SyntaxKind.BinaryExpression; }