diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 51830c50bfe..b21b9140a87 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2610,6 +2610,10 @@ namespace ts { transformFlags |= TransformFlags.AssertTypeScript; } + if (node.flags & NodeFlags.PropagateNull) { + transformFlags |= TransformFlags.AssertESNext; + } + if (subtreeFlags & TransformFlags.ContainsSpread || isSuperOrSuperProperty(expression, expressionKind)) { // If the this node contains a SpreadExpression, or is a super call, then it is an ES6 @@ -2641,11 +2645,17 @@ namespace ts { if (node.typeArguments) { transformFlags |= TransformFlags.AssertTypeScript; } + + if (node.flags & NodeFlags.PropagateNull) { + transformFlags |= TransformFlags.AssertESNext; + } + if (subtreeFlags & TransformFlags.ContainsSpread) { // If the this node contains a SpreadElementExpression then it is an ES6 // node. transformFlags |= TransformFlags.AssertES2015; } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; return transformFlags & ~TransformFlags.ArrayLiteralOrCallOrNewExcludes; } @@ -3049,6 +3059,10 @@ namespace ts { const expression = node.expression; const expressionKind = expression.kind; + if (node.flags & NodeFlags.PropagateNull) { + transformFlags |= TransformFlags.AssertESNext; + } + // If a PropertyAccessExpression starts with a super keyword, then it is // ES6 syntax, and requires a lexical `this` binding. if (expressionKind === SyntaxKind.SuperKeyword) { @@ -3354,7 +3368,6 @@ namespace ts { break; case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.NewExpression: excludeFlags = TransformFlags.ArrayLiteralOrCallOrNewExcludes; if (subtreeFlags & TransformFlags.ContainsSpread) { // If the this node contains a SpreadExpression, then it is an ES6 @@ -3364,6 +3377,13 @@ namespace ts { break; + case SyntaxKind.ElementAccessExpression: + if (node.flags & NodeFlags.PropagateNull) { + transformFlags |= TransformFlags.AssertESNext; + } + + break; + case SyntaxKind.DoStatement: case SyntaxKind.WhileStatement: case SyntaxKind.ForStatement: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 12b55aa818b..788a61dad28 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13109,6 +13109,7 @@ namespace ts { } function checkPropertyAccessExpression(node: PropertyAccessExpression) { + checkGrammarNullPropagation(node); return checkPropertyAccessExpressionOrQualifiedName(node, node.expression, node.name); } @@ -13276,6 +13277,8 @@ namespace ts { } function checkIndexedAccess(node: ElementAccessExpression): Type { + checkGrammarNullPropagation(node); + const objectType = checkNonNullExpression(node.expression); const indexExpression = node.argumentExpression; @@ -14678,7 +14681,7 @@ namespace ts { */ function checkCallExpression(node: CallExpression | NewExpression): Type { // Grammar checking; stop grammar-checking if checkGrammarTypeArguments return true - checkGrammarTypeArguments(node, node.typeArguments) || checkGrammarArguments(node, node.arguments); + checkGrammarNullPropagation(node) || checkGrammarTypeArguments(node, node.typeArguments) || checkGrammarArguments(node, node.arguments); const signature = getResolvedSignature(node); @@ -22386,6 +22389,18 @@ namespace ts { return checkGrammarDecorators(node) || checkGrammarModifiers(node) || checkGrammarIndexSignatureParameters(node); } + function checkGrammarNullPropagation(node: CallExpression | NewExpression | PropertyAccessExpression | ElementAccessExpression) { + if (node.flags & NodeFlags.PropagateNull && node.expression.kind === SyntaxKind.SuperKeyword) { + const sourceFile = getSourceFileOfNode(node); + const start = skipTrivia(sourceFile.text, node.expression.end); + if (node.kind === SyntaxKind.PropertyAccessExpression) { + return grammarErrorAtPos(sourceFile, start, 2, Diagnostics._0_expected, tokenToString(SyntaxKind.DotToken)); + } + + return grammarErrorAtPos(sourceFile, start, 2, Diagnostics.Unexpected_token); + } + } + function checkGrammarForAtLeastOneTypeArgument(node: Node, typeArguments: NodeArray): boolean { if (typeArguments && typeArguments.length === 0) { const sourceFile = getSourceFileOfNode(node); diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 06472db58b2..c00fff3683e 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1092,12 +1092,13 @@ namespace ts { } function emitPropertyAccessExpression(node: PropertyAccessExpression) { + const propagatesNull = node.flags & NodeFlags.PropagateNull; let indentBeforeDot = false; let indentAfterDot = false; if (!(getEmitFlags(node) & EmitFlags.NoIndentation)) { const dotRangeStart = node.expression.end; - const dotRangeEnd = skipTrivia(currentSourceFile.text, node.expression.end) + 1; - const dotToken = { kind: SyntaxKind.DotToken, pos: dotRangeStart, end: dotRangeEnd }; + const dotRangeEnd = skipTrivia(currentSourceFile.text, node.expression.end) + (propagatesNull ? 2 : 1); + const dotToken = { kind: propagatesNull ? SyntaxKind.QuestionDotToken : SyntaxKind.DotToken, pos: dotRangeStart, end: dotRangeEnd }; indentBeforeDot = needsIndentation(node, node.expression, dotToken); indentAfterDot = needsIndentation(node, dotToken, node.name); } @@ -1105,8 +1106,8 @@ namespace ts { emitExpression(node.expression); increaseIndentIf(indentBeforeDot); - const shouldEmitDotDot = !indentBeforeDot && needsDotDotForPropertyAccess(node.expression); - write(shouldEmitDotDot ? ".." : "."); + const shouldEmitDotDot = !propagatesNull && !indentBeforeDot && needsDotDotForPropertyAccess(node.expression); + write(shouldEmitDotDot ? ".." : propagatesNull ? "?." : "."); increaseIndentIf(indentAfterDot); emit(node.name); @@ -1135,13 +1136,16 @@ namespace ts { function emitElementAccessExpression(node: ElementAccessExpression) { emitExpression(node.expression); - write("["); + write(node.flags & NodeFlags.PropagateNull ? "?.[" : "["); emitExpression(node.argumentExpression); write("]"); } function emitCallExpression(node: CallExpression) { emitExpression(node.expression); + if (node.flags & NodeFlags.PropagateNull) { + write("?."); + } emitTypeArguments(node, node.typeArguments); emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments); } @@ -1149,6 +1153,9 @@ namespace ts { function emitNewExpression(node: NewExpression) { write("new "); emitExpression(node.expression); + if (node.flags & NodeFlags.PropagateNull) { + write("?."); + } emitTypeArguments(node, node.typeArguments); emitExpressionList(node, node.arguments, ListFormat.NewExpressionArguments); } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 223bf53237d..14aa08ce254 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -440,8 +440,9 @@ namespace ts { : node; } - export function createPropertyAccess(expression: Expression, name: string | Identifier) { + export function createPropertyAccess(expression: Expression, name: string | Identifier, flags?: NodeFlags) { const node = createSynthesizedNode(SyntaxKind.PropertyAccessExpression); + node.flags |= flags; node.expression = parenthesizeForAccess(expression); node.name = asName(name); setEmitFlags(node, EmitFlags.NoIndentation); @@ -453,12 +454,13 @@ namespace ts { // instead of using the default from createPropertyAccess return node.expression !== expression || node.name !== name - ? updateNode(setEmitFlags(createPropertyAccess(expression, name), getEmitFlags(node)), node) + ? updateNode(setEmitFlags(createPropertyAccess(expression, name, node.flags), getEmitFlags(node)), node) : node; } - export function createElementAccess(expression: Expression, index: number | Expression) { + export function createElementAccess(expression: Expression, index: number | Expression, flags?: NodeFlags) { const node = createSynthesizedNode(SyntaxKind.ElementAccessExpression); + node.flags |= flags; node.expression = parenthesizeForAccess(expression); node.argumentExpression = asExpression(index); return node; @@ -467,12 +469,13 @@ namespace ts { export function updateElementAccess(node: ElementAccessExpression, expression: Expression, argumentExpression: Expression) { return node.expression !== expression || node.argumentExpression !== argumentExpression - ? updateNode(createElementAccess(expression, argumentExpression), node) + ? updateNode(createElementAccess(expression, argumentExpression, node.flags), node) : node; } - export function createCall(expression: Expression, typeArguments: TypeNode[] | undefined, argumentsArray: Expression[]) { + export function createCall(expression: Expression, typeArguments: TypeNode[] | undefined, argumentsArray: Expression[], flags?: NodeFlags) { const node = createSynthesizedNode(SyntaxKind.CallExpression); + node.flags |= flags; node.expression = parenthesizeForAccess(expression); node.typeArguments = asNodeArray(typeArguments); node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); @@ -483,12 +486,13 @@ namespace ts { return expression !== node.expression || typeArguments !== node.typeArguments || argumentsArray !== node.arguments - ? updateNode(createCall(expression, typeArguments, argumentsArray), node) + ? updateNode(createCall(expression, typeArguments, argumentsArray, node.flags), node) : node; } - export function createNew(expression: Expression, typeArguments: TypeNode[] | undefined, argumentsArray: Expression[] | undefined) { + export function createNew(expression: Expression, typeArguments: TypeNode[] | undefined, argumentsArray: Expression[] | undefined, flags?: NodeFlags) { const node = createSynthesizedNode(SyntaxKind.NewExpression); + node.flags |= flags; node.expression = parenthesizeForNew(expression); node.typeArguments = asNodeArray(typeArguments); node.arguments = argumentsArray ? parenthesizeListElements(createNodeArray(argumentsArray)) : undefined; @@ -499,7 +503,7 @@ namespace ts { return node.expression !== expression || node.typeArguments !== typeArguments || node.arguments !== argumentsArray - ? updateNode(createNew(expression, typeArguments, argumentsArray), node) + ? updateNode(createNew(expression, typeArguments, argumentsArray, node.flags), node) : node; } @@ -1740,6 +1744,10 @@ namespace ts { return createBinary(left, SyntaxKind.EqualsToken, right); } + export function createEquality(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.EqualsEqualsToken, right); + } + export function createStrictEquality(left: Expression, right: Expression) { return createBinary(left, SyntaxKind.EqualsEqualsEqualsToken, right); } @@ -2449,7 +2457,8 @@ namespace ts { ), (callee).expression ), - (callee).name + (callee).name, + callee.flags & NodeFlags.PropagateNull ); setTextRange(target, callee); } @@ -2472,7 +2481,8 @@ namespace ts { ), (callee).expression ), - (callee).argumentExpression + (callee).argumentExpression, + callee.flags & NodeFlags.PropagateNull ); setTextRange(target, callee); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 308d3a317f9..75b337095b4 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -3748,7 +3748,10 @@ namespace ts { function parseSuperExpression(): MemberExpression { const expression = parseTokenNode(); - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.DotToken || token() === SyntaxKind.OpenBracketToken) { + if (token() === SyntaxKind.OpenParenToken || + token() === SyntaxKind.DotToken || + token() === SyntaxKind.OpenBracketToken || + token() === SyntaxKind.QuestionDotToken) { return expression; } @@ -4004,11 +4007,32 @@ namespace ts { return finishNode(node); } + function isNullPropagatingCallOrNewExpression() { + return token() === SyntaxKind.QuestionDotToken + && lookAhead(nextTokenIsOpenParenOrLessThanToken); + } + + function nextTokenIsOpenParenOrLessThanToken() { + nextToken(); + return token() === SyntaxKind.OpenParenToken + || token() === SyntaxKind.LessThanToken; + } + function parseMemberExpressionRest(expression: LeftHandSideExpression): MemberExpression { while (true) { + if (isNullPropagatingCallOrNewExpression()) { + // In a null-propagating call or new expression, we defer parsing `.?` to parseCallExpressionRest. + return expression; + } + const dotToken = parseOptionalToken(SyntaxKind.DotToken); - if (dotToken) { + const questionDotToken = !dotToken && parseOptionalToken(SyntaxKind.QuestionDotToken); + if (dotToken || (questionDotToken && token() !== SyntaxKind.OpenBracketToken)) { const propertyAccess = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); + if (questionDotToken) { + propertyAccess.flags |= NodeFlags.PropagateNull; + } + propertyAccess.expression = expression; propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true); expression = finishNode(propertyAccess); @@ -4024,8 +4048,13 @@ namespace ts { } // when in the [Decorator] context, we do not parse ElementAccess as it could be part of a ComputedPropertyName - if (!inDecoratorContext() && parseOptional(SyntaxKind.OpenBracketToken)) { + // however, `?.[` is unambiguously *not* a ComputedPropertyName. + if ((questionDotToken || !inDecoratorContext()) && parseOptional(SyntaxKind.OpenBracketToken)) { const indexedAccess = createNode(SyntaxKind.ElementAccessExpression, expression.pos); + if (questionDotToken) { + indexedAccess.flags |= NodeFlags.PropagateNull; + } + indexedAccess.expression = expression; // It's not uncommon for a user to write: "new Type[]". @@ -4060,32 +4089,34 @@ namespace ts { function parseCallExpressionRest(expression: LeftHandSideExpression): LeftHandSideExpression { while (true) { expression = parseMemberExpressionRest(expression); + const questionDot = parseOptionalToken(SyntaxKind.QuestionDotToken); + let typeArguments: NodeArray; if (token() === SyntaxKind.LessThanToken) { // See if this is the start of a generic invocation. If so, consume it and // keep checking for postfix expressions. Otherwise, it's just a '<' that's // part of an arithmetic expression. Break out so we consume it higher in the // stack. - const typeArguments = tryParse(parseTypeArgumentsInExpression); + // If we have seen `?.<` then this is definately a call expression. + typeArguments = questionDot + ? parseTypeArgumentsInExpression() + : tryParse(parseTypeArgumentsInExpression); if (!typeArguments) { return expression; } - - const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); - callExpr.expression = expression; - callExpr.typeArguments = typeArguments; - callExpr.arguments = parseArgumentList(); - expression = finishNode(callExpr); - continue; } - else if (token() === SyntaxKind.OpenParenToken) { - const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); - callExpr.expression = expression; - callExpr.arguments = parseArgumentList(); - expression = finishNode(callExpr); - continue; + else if (token() !== SyntaxKind.OpenParenToken) { + return expression; } - return expression; + const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); + if (questionDot) { + callExpr.flags |= NodeFlags.PropagateNull; + } + + callExpr.expression = expression; + callExpr.typeArguments = typeArguments; + callExpr.arguments = parseArgumentList(); + expression = finishNode(callExpr); } } @@ -4121,6 +4152,7 @@ namespace ts { // list. So we definitely want to treat this as a type arg list. case SyntaxKind.DotToken: // foo. + case SyntaxKind.QuestionDotToken: // foo?. case SyntaxKind.CloseParenToken: // foo) case SyntaxKind.CloseBracketToken: // foo] case SyntaxKind.ColonToken: // foo: @@ -4365,6 +4397,10 @@ namespace ts { const node = createNode(SyntaxKind.NewExpression, fullStart); node.expression = parseMemberExpressionOrHigher(); + if (parseOptional(SyntaxKind.QuestionDotToken)) { + node.flags |= NodeFlags.PropagateNull; + } + node.typeArguments = tryParse(parseTypeArgumentsInExpression); if (node.typeArguments || token() === SyntaxKind.OpenParenToken) { node.arguments = parseArgumentList(); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index fb91b0ff5b7..41fe860eb61 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -167,6 +167,7 @@ namespace ts { "&&": SyntaxKind.AmpersandAmpersandToken, "||": SyntaxKind.BarBarToken, "?": SyntaxKind.QuestionToken, + "?.": SyntaxKind.QuestionDotToken, ":": SyntaxKind.ColonToken, "=": SyntaxKind.EqualsToken, "+=": SyntaxKind.PlusEqualsToken, @@ -1529,6 +1530,9 @@ namespace ts { pos++; return token = SyntaxKind.GreaterThanToken; case CharacterCodes.question: + if (text.charCodeAt(pos + 1) === CharacterCodes.dot && !isDigit(text.charCodeAt(pos + 2))) { + return pos += 2, token = SyntaxKind.QuestionDotToken; + } pos++; return token = SyntaxKind.QuestionToken; case CharacterCodes.openBracket: diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 971dddd20ea..2b652a82f0f 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -26,6 +26,7 @@ namespace ts { const previousOnSubstituteNode = context.onSubstituteNode; context.onSubstituteNode = onSubstituteNode; + const nullPropagatingExpressions = createMap(); let enabledSubstitutions: ESNextSubstitutionFlags; let enclosingFunctionFlags: FunctionFlags; let enclosingSuperContainerFlags: NodeCheckFlags = 0; @@ -39,6 +40,7 @@ namespace ts { const visited = visitEachChild(node, visitor, context); addEmitHelpers(visited, context.readEmitHelpers()); + nullPropagatingExpressions.clear(); return visited; } @@ -101,6 +103,20 @@ namespace ts { return visitExpressionStatement(node as ExpressionStatement); case SyntaxKind.ParenthesizedExpression: return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue); + case SyntaxKind.CallExpression: + return visitCallExpression(node as CallExpression); + case SyntaxKind.NewExpression: + return visitNewExpression(node as NewExpression); + case SyntaxKind.PropertyAccessExpression: + return visitPropertyAccess(node as PropertyAccessExpression); + case SyntaxKind.ElementAccessExpression: + return visitElementAccess(node as ElementAccessExpression); + case SyntaxKind.DeleteExpression: + return visitDelete(node as DeleteExpression); + case SyntaxKind.PrefixUnaryExpression: + return visitPrefix(node as PrefixUnaryExpression); + case SyntaxKind.PostfixUnaryExpression: + return visitPostfix(node as PostfixUnaryExpression); default: return visitEachChild(node, visitor, context); } @@ -230,7 +246,20 @@ namespace ts { visitNode(node.right, noDestructuringValue ? visitorNoDestructuringValue : visitor, isExpression) ); } - return visitEachChild(node, visitor, context); + + 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) + ); + } + } + + return updateBinary(node, left, right); } /** @@ -699,6 +728,310 @@ namespace ts { return statements; } + function isNullPropagatingExpression(node: CallExpression | NewExpression | PropertyAccessExpression | ElementAccessExpression) { + return (node.flags & NodeFlags.PropagateNull) !== 0 + && 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 { + if (node.flags & NodeFlags.PropagateNull) { + if (isIdentifier(nullableExpression)) { + return setTextRange( + createConditional( + createEquality(nullableExpression, createNull()), + createVoidZero(), + finishExpression(node, nullableExpression, data) + ), + node + ); + } + else { + const temp = createTempVariable(hoistVariableDeclaration); + return setTextRange( + createConditional( + createEquality( + createAssignment(temp, nullableExpression), + createNull() + ), + createVoidZero(), + finishExpression(node, temp, data) + ), + node + ); + } + } + return finishExpression(node, nullableExpression, data); + } + + function visitCallExpression(node: CallExpression): Expression { + if (isNullPropagatingExpression(node)) { + // null propagation in call: + // x?.() -> x == null ? void 0 : x() + // (x)?.() -> (_a = (x)) == null ? void 0 : _a() + // x.y?.() -> (_a = x.y) == null ? void 0 : _a.call(x); + // x[y]?.() -> (_a = x[y]) == null ? void 0 : _a.call(x); + // (x.y)?.() -> (_a = x.y) == null ? void 0 : _a.call(x); + // (x[y])?.() -> (_a = x[y]) == null ? void 0 : _a.call(x); + const { target, thisArg } = createCallBinding(node.expression, hoistVariableDeclaration); + 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 + ); + } + + function finishNullableCallExpression(node: CallExpression, target: Expression, thisArg: Expression) { + if (!isVoidZero(thisArg)) { + return setOriginalNode( + createFunctionCall( + target, + thisArg, + visitNodes(node.arguments, visitor, isExpression), + node + ), + node + ); + } + return setOriginalNode( + setTextRange( + createCall( + target, + /*typeArguments*/ undefined, + visitNodes(node.arguments, visitor, isExpression) + ), + node + ), + node + ); + } + + 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(); + // new (x)?.() -> (_a = (x)) == 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(); + // 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); + } + + 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); + } + + function finishNullableNewExpression(node: NewExpression, expression: Expression) { + return setOriginalNode( + setTextRange( + createNew( + expression, + /*typeArguments*/ undefined, + visitNodes(node.arguments, visitor, isExpression) + ), + node + ), + node + ); + } + + 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); + } + + 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 + ); + } + + function finishNullablePropertyAccess(node: PropertyAccessExpression, expression: Expression) { + return setOriginalNode( + setTextRange( + createPropertyAccess( + expression, + node.name + ), + node + ), + node + ); + } + + 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); + } + + 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 + ); + } + + function finishNullableElementAccess(node: ElementAccessExpression, expression: Expression) { + return setOriginalNode( + setTextRange( + createElementAccess( + expression, + visitNode(node.argumentExpression, visitor, isExpression) + ), + node + ), + node + ); + } + + 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 + ); + } + return updateDelete(node, expression); + } + + function visitPrefix(node: PrefixUnaryExpression) { + const operand = visitNode(node.operand, visitor, isExpression); + const nilReference = getNilReference(operand); + if (nilReference) { + return updateNilReference( + nilReference, + updatePrefix(node, nilReference.whenFalse) + ); + } + return updatePrefix(node, operand); + } + + function visitPostfix(node: PostfixUnaryExpression) { + const operand = visitNode(node.operand, visitor, isExpression); + const nilReference = getNilReference(operand); + if (nilReference) { + return updateNilReference( + nilReference, + updatePostfix(node, nilReference.whenFalse) + ); + } + return updatePostfix(node, operand); + } + function enableSubstitutionForAsyncMethodsWithSuper() { if ((enabledSubstitutions & ESNextSubstitutionFlags.AsyncMethodsWithSuper) === 0) { enabledSubstitutions |= ESNextSubstitutionFlags.AsyncMethodsWithSuper; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index de42d5d9745..1045b35caf8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -82,6 +82,7 @@ DotDotDotToken, SemicolonToken, CommaToken, + QuestionDotToken, LessThanToken, LessThanSlashToken, GreaterThanToken, @@ -435,20 +436,21 @@ NestedNamespace = 1 << 2, // Namespace declaration Synthesized = 1 << 3, // Node was synthesized during transformation Namespace = 1 << 4, // Namespace declaration - ExportContext = 1 << 5, // Export context (initialized by binding) - ContainsThis = 1 << 6, // Interface contains references to "this" - HasImplicitReturn = 1 << 7, // If function implicitly returns on one of codepaths (initialized by binding) - HasExplicitReturn = 1 << 8, // If function has explicit reachable return on one of codepaths (initialized by binding) - GlobalAugmentation = 1 << 9, // Set if module declaration is an augmentation for the global scope - HasAsyncFunctions = 1 << 10, // If the file has async functions (initialized by binding) - DisallowInContext = 1 << 11, // If node was parsed in a context where 'in-expressions' are not allowed - YieldContext = 1 << 12, // If node was parsed in the 'yield' context created when parsing a generator - DecoratorContext = 1 << 13, // If node was parsed as part of a decorator - AwaitContext = 1 << 14, // If node was parsed in the 'await' context created when parsing an async function - ThisNodeHasError = 1 << 15, // If the parser encountered an error when parsing the code that created this node - JavaScriptFile = 1 << 16, // If node was parsed in a JavaScript - ThisNodeOrAnySubNodesHasError = 1 << 17, // If this node or any of its children had an error - HasAggregatedChildData = 1 << 18, // If we've computed data from children and cached it in this node + PropagateNull = 1 << 5, // Expression + ExportContext = 1 << 6, // Export context (initialized by binding) + ContainsThis = 1 << 7, // Interface contains references to "this" + HasImplicitReturn = 1 << 8, // If function implicitly returns on one of codepaths (initialized by binding) + HasExplicitReturn = 1 << 9, // If function has explicit reachable return on one of codepaths (initialized by binding) + GlobalAugmentation = 1 << 10, // Set if module declaration is an augmentation for the global scope + HasAsyncFunctions = 1 << 11, // If the file has async functions (initialized by binding) + DisallowInContext = 1 << 12, // If node was parsed in a context where 'in-expressions' are not allowed + YieldContext = 1 << 13, // If node was parsed in the 'yield' context created when parsing a generator + DecoratorContext = 1 << 14, // If node was parsed as part of a decorator + AwaitContext = 1 << 15, // If node was parsed in the 'await' context created when parsing an async function + ThisNodeHasError = 1 << 16, // If the parser encountered an error when parsing the code that created this node + JavaScriptFile = 1 << 17, // If node was parsed in a JavaScript + ThisNodeOrAnySubNodesHasError = 1 << 18, // If this node or any of its children had an error + HasAggregatedChildData = 1 << 19, // If we've computed data from children and cached it in this node BlockScoped = Let | Const, diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 66bf693d831..8263cd16d12 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3583,10 +3583,6 @@ namespace ts { return node.kind === SyntaxKind.Identifier; } - export function isVoidExpression(node: Node): node is VoidExpression { - return node.kind === SyntaxKind.VoidExpression; - } - export function isGeneratedIdentifier(node: Node): node is GeneratedIdentifier { // Using `>` here catches both `GeneratedIdentifierKind.None` and `undefined`. return isIdentifier(node) && node.autoGenerateKind > GeneratedIdentifierKind.None; @@ -3897,6 +3893,16 @@ namespace ts { return isExpressionKind(skipPartiallyEmittedExpressions(node).kind); } + export function isVoidExpression(node: Node): node is VoidExpression { + return node.kind === SyntaxKind.VoidExpression; + } + + export function isVoidZero(node: Node): boolean { + return isVoidExpression(node) + && node.expression.kind === SyntaxKind.NumericLiteral + && (node.expression).text === "0"; + } + export function isAssertionExpression(node: Node): node is AssertionExpression { const kind = node.kind; return kind === SyntaxKind.TypeAssertionExpression