diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 70141c853ca..3aa20c6c0db 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2688,6 +2688,10 @@ namespace ts { transformFlags |= TransformFlags.AssertTypeScript; } + if (node.flags & NodeFlags.Optional) { + 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 @@ -3132,6 +3136,10 @@ namespace ts { const expression = node.expression; const expressionKind = expression.kind; + if (node.flags & NodeFlags.Optional) { + 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) { @@ -3449,7 +3457,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 @@ -3459,6 +3466,12 @@ namespace ts { break; + case SyntaxKind.ElementAccessExpression: + if (node.flags & NodeFlags.Optional) { + 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 604357511c6..9fb56f5db43 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -251,6 +251,7 @@ namespace ts { const unknownType = createIntrinsicType(TypeFlags.Any, "unknown"); const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined"); const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined | TypeFlags.ContainsWideningType, "undefined"); + const optionalType = createIntrinsicType(TypeFlags.Undefined | TypeFlags.Optional | TypeFlags.ContainsWideningType, "undefined"); const nullType = createIntrinsicType(TypeFlags.Null, "null"); const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null | TypeFlags.ContainsWideningType, "null"); const stringType = createIntrinsicType(TypeFlags.String, "string"); @@ -379,19 +380,21 @@ namespace ts { TypeofNEFunction = 1 << 12, // typeof x !== "function" TypeofNEHostObject = 1 << 13, // typeof x !== "xxx" EQUndefined = 1 << 14, // x === undefined - EQNull = 1 << 15, // x === null - EQUndefinedOrNull = 1 << 16, // x === undefined / x === null - NEUndefined = 1 << 17, // x !== undefined - NENull = 1 << 18, // x !== null - NEUndefinedOrNull = 1 << 19, // x != undefined / x != null - Truthy = 1 << 20, // x - Falsy = 1 << 21, // !x - Discriminatable = 1 << 22, // May have discriminant property - All = (1 << 23) - 1, + EQOptional = 1 << 15, + EQNull = 1 << 16, // x === null + EQUndefinedOrNull = 1 << 17, // x === undefined / x === null + NEUndefined = 1 << 18, // x !== undefined + NEOptional = 1 << 19, + NENull = 1 << 20, // x !== null + NEUndefinedOrNull = 1 << 21, // x != undefined / x != null + Truthy = 1 << 22, // x + Falsy = 1 << 23, // !x + Discriminatable = 1 << 24, // May have discriminant property + All = (1 << 25) - 1, // The following members encode facts about particular kinds of types for use in the getTypeFacts function. // The presence of a particular fact means that the given test is true for some (and possibly all) values // of that kind of type. - BaseStringStrictFacts = TypeofEQString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseStringStrictFacts = TypeofEQString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEOptional | NEUndefinedOrNull, BaseStringFacts = BaseStringStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, StringStrictFacts = BaseStringStrictFacts | Truthy | Falsy, StringFacts = BaseStringFacts | Truthy, @@ -399,7 +402,7 @@ namespace ts { EmptyStringFacts = BaseStringFacts, NonEmptyStringStrictFacts = BaseStringStrictFacts | Truthy, NonEmptyStringFacts = BaseStringFacts | Truthy, - BaseNumberStrictFacts = TypeofEQNumber | TypeofNEString | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseNumberStrictFacts = TypeofEQNumber | TypeofNEString | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEOptional | NEUndefinedOrNull, BaseNumberFacts = BaseNumberStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, NumberStrictFacts = BaseNumberStrictFacts | Truthy | Falsy, NumberFacts = BaseNumberFacts | Truthy, @@ -407,7 +410,7 @@ namespace ts { ZeroFacts = BaseNumberFacts, NonZeroStrictFacts = BaseNumberStrictFacts | Truthy, NonZeroFacts = BaseNumberFacts | Truthy, - BaseBooleanStrictFacts = TypeofEQBoolean | TypeofNEString | TypeofNENumber | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseBooleanStrictFacts = TypeofEQBoolean | TypeofNEString | TypeofNENumber | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEOptional | NEUndefinedOrNull, BaseBooleanFacts = BaseBooleanStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, BooleanStrictFacts = BaseBooleanStrictFacts | Truthy | Falsy, BooleanFacts = BaseBooleanFacts | Truthy, @@ -415,14 +418,15 @@ namespace ts { FalseFacts = BaseBooleanFacts, TrueStrictFacts = BaseBooleanStrictFacts | Truthy, TrueFacts = BaseBooleanFacts | Truthy, - SymbolStrictFacts = TypeofEQSymbol | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy, + SymbolStrictFacts = TypeofEQSymbol | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEOptional | NEUndefinedOrNull | Truthy, SymbolFacts = SymbolStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEUndefinedOrNull | Truthy | Discriminatable, + ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEOptional | NEUndefinedOrNull | Truthy | Discriminatable, ObjectFacts = ObjectStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy | Discriminatable, + FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEOptional | NEUndefinedOrNull | Truthy | Discriminatable, FunctionFacts = FunctionStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - UndefinedFacts = TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQUndefinedOrNull | NENull | Falsy, - NullFacts = TypeofEQObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | TypeofNEHostObject | EQNull | EQUndefinedOrNull | NEUndefined | Falsy, + UndefinedFacts = TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQUndefinedOrNull | NENull | NEOptional | Falsy, + OptionalFacts = TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQOptional | EQUndefinedOrNull | NENull | Falsy, + NullFacts = TypeofEQObject | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | TypeofNEHostObject | EQNull | EQUndefinedOrNull | NEUndefined | NEOptional | Falsy, } const typeofEQFacts = createMapFromTemplate({ @@ -10149,6 +10153,10 @@ namespace ts { return strictNullChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; } + function getNonOptionalType(type: Type): Type { + return strictNullChecks ? getTypeWithFacts(type, TypeFacts.NEOptional) : type; + } + /** * Return true if type was inferred from an object literal or written as an object type literal * with no call or construct signatures. @@ -10235,6 +10243,9 @@ namespace ts { function getWidenedType(type: Type): Type { if (type.flags & TypeFlags.RequiresWidening) { + if (type.flags & TypeFlags.Optional) { + return strictNullChecks ? undefinedType : anyType; + } if (type.flags & TypeFlags.Nullable) { return anyType; } @@ -11098,7 +11109,7 @@ namespace ts { strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; } if (flags & (TypeFlags.Void | TypeFlags.Undefined)) { - return TypeFacts.UndefinedFacts; + return flags & TypeFlags.Optional ? TypeFacts.OptionalFacts : TypeFacts.UndefinedFacts; } if (flags & TypeFlags.Null) { return TypeFacts.NullFacts; @@ -14697,7 +14708,14 @@ namespace ts { return checkNonNullType(checkExpression(node), node); } - function checkNonNullType(type: Type, errorNode: Node): Type { + function checkNonNullType(type: Type, errorNode: Node, optionality?: NodeFlags): Type { + if (optionality & NodeFlags.OptionalExpression) { + type = getNonNullableType(type); + } + else if (optionality & NodeFlags.OptionalChain) { + type = getNonOptionalType(type); + } + const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable; if (kind) { error(errorNode, kind & TypeFlags.Undefined ? kind & TypeFlags.Null ? @@ -14718,8 +14736,38 @@ namespace ts { return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right); } + // function checkOptionality(node: Node) { + // if (node.flags & NodeFlags.OptionalExpression) { + // if (!compilerOptions.experimentalOptionalChaining) { + // error(node, Diagnostics.Experimental_support_for_optional_chaining_is_a_feature_that_is_subject_to_change_in_a_future_release_Set_the_experimentalOptionalChaining_option_to_remove_this_warning); + // } + // return strictNullChecks; + // } + // return false; + // } + + function propagateOptionalChain(type: Type, sourceType: Type, optionality: NodeFlags) { + const expectedFacts = optionality & NodeFlags.OptionalExpression ? TypeFacts.EQUndefinedOrNull : TypeFacts.EQOptional; + return optionality && strictNullChecks && getTypeFacts(sourceType) & expectedFacts + ? getUnionType([type, optionalType]) + : type; + } + + function propagateOptionalChainSignature(signature: Signature, sourceType: Type, optionality: NodeFlags) { + const expectedFacts = optionality & NodeFlags.OptionalExpression ? TypeFacts.EQUndefinedOrNull : TypeFacts.EQOptional; + if (optionality && strictNullChecks && getTypeFacts(sourceType) & expectedFacts) { + signature = cloneSignature(signature); + signature.resolvedReturnType = getUnionType([getReturnTypeOfSignature(signature), optionalType]); + } + return signature; + } + function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) { - const type = checkNonNullExpression(left); + // if a node is an OptionalExpression, use its non-null type + const optionality = node.flags & NodeFlags.Optional; + const sourceType = checkExpression(left); + const type = checkNonNullType(sourceType, left, optionality); + if (isTypeAny(type) || type === silentNeverType) { return type; } @@ -14768,10 +14816,11 @@ 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 propagateOptionalChain(propType, sourceType, optionality); } const flowType = getFlowTypeOfReference(node, propType); - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + const resultType = assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + return propagateOptionalChain(resultType, sourceType, optionality); } function checkPropertyNotUsedBeforeDeclaration(prop: Symbol, node: PropertyAccessExpression | QualifiedName, right: Identifier): void { @@ -15040,7 +15089,9 @@ namespace ts { } function checkIndexedAccess(node: ElementAccessExpression): Type { - const objectType = checkNonNullExpression(node.expression); + const optionality = node.flags & NodeFlags.Optional; + const sourceType = checkExpression(node.expression); + const objectType = checkNonNullType(sourceType, node.expression, optionality); const indexExpression = node.argumentExpression; if (!indexExpression) { @@ -15069,7 +15120,8 @@ namespace ts { return unknownType; } - return checkIndexedAccessIndexType(getIndexedAccessType(objectType, indexType, node), node); + const resultType = checkIndexedAccessIndexType(getIndexedAccessType(objectType, indexType, node), node); + return propagateOptionalChain(resultType, objectType, optionality); } function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean { @@ -16131,7 +16183,9 @@ namespace ts { return resolveUntypedCall(node); } - const funcType = checkNonNullExpression(node.expression); + const optionality = node.flags & NodeFlags.Optional; + const sourceType = checkExpression(node.expression); + const funcType = checkNonNullType(sourceType, node.expression, optionality); if (funcType === silentNeverType) { return silentNeverSignature; } @@ -16172,7 +16226,9 @@ namespace ts { } return resolveErrorCall(node); } - return resolveCall(node, callSignatures, candidatesOutArray); + + const signature = resolveCall(node, callSignatures, candidatesOutArray); + return propagateOptionalChainSignature(signature, sourceType, optionality); } /** diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 54e5ee1d01d..a71a99e4680 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -433,6 +433,12 @@ namespace ts { category: Diagnostics.Experimental_Options, description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators }, + { + name: "experimentalOptionalChaining", + type: "boolean", + category: Diagnostics.Experimental_Options, + description: Diagnostics.Enables_experimental_support_for_optional_chaining + }, // Advanced { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 662e87d3159..e5c4de02ae0 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -831,6 +831,11 @@ "category": "Error", "code": 1254 }, + "Experimental support for optional chaining is a feature that is subject to change in a future release. Set the 'experimentalOptionalChaining' option to remove this warning.": { + "category": "Error", + "code": 1255 + }, + "'with' statements are not allowed in an async function block.": { "category": "Error", "code": 1300 @@ -3314,6 +3319,10 @@ "category": "Message", "code": 6185 }, + "Enables experimental support for optional chaining.": { + "category": "Message", + "code": 6186 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index d458e7e5ef5..3de8c39eb2f 100755 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1195,12 +1195,13 @@ namespace ts { } function emitPropertyAccessExpression(node: PropertyAccessExpression) { + const isOptionalExpression = node.flags & NodeFlags.OptionalExpression; 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 = createToken(SyntaxKind.DotToken); + const dotToken = createToken(isOptionalExpression ? SyntaxKind.QuestionDotToken : SyntaxKind.DotToken); dotToken.pos = dotRangeStart; dotToken.end = dotRangeEnd; indentBeforeDot = needsIndentation(node, node.expression, dotToken); @@ -1210,8 +1211,8 @@ namespace ts { emitExpression(node.expression); increaseIndentIf(indentBeforeDot); - const shouldEmitDotDot = !indentBeforeDot && needsDotDotForPropertyAccess(node.expression); - write(shouldEmitDotDot ? ".." : "."); + const shouldEmitDotDot = !isOptionalExpression && !indentBeforeDot && needsDotDotForPropertyAccess(node.expression); + write(shouldEmitDotDot ? ".." : isOptionalExpression ? "?." : "."); increaseIndentIf(indentAfterDot); emit(node.name); @@ -1240,13 +1241,16 @@ namespace ts { function emitElementAccessExpression(node: ElementAccessExpression) { emitExpression(node.expression); - write("["); + write(node.flags & NodeFlags.OptionalExpression ? "?.[" : "["); emitExpression(node.argumentExpression); write("]"); } function emitCallExpression(node: CallExpression) { emitExpression(node.expression); + if (node.flags & NodeFlags.OptionalExpression) { + write("?."); + } emitTypeArguments(node, node.typeArguments); emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments); } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 240ce347a4f..88a0dff1d8c 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -859,6 +859,7 @@ namespace ts { export function createPropertyAccess(expression: Expression, name: string | Identifier) { const node = createSynthesizedNode(SyntaxKind.PropertyAccessExpression); + if (expression.flags & NodeFlags.Optional) node.flags |= NodeFlags.OptionalChain; node.expression = parenthesizeForAccess(expression); node.name = asName(name); setEmitFlags(node, EmitFlags.NoIndentation); @@ -876,6 +877,7 @@ namespace ts { export function createElementAccess(expression: Expression, index: number | Expression) { const node = createSynthesizedNode(SyntaxKind.ElementAccessExpression); + if (expression.flags & NodeFlags.Optional) node.flags |= NodeFlags.OptionalChain; node.expression = parenthesizeForAccess(expression); node.argumentExpression = asExpression(index); return node; @@ -890,6 +892,7 @@ namespace ts { export function createCall(expression: Expression, typeArguments: ReadonlyArray | undefined, argumentsArray: ReadonlyArray) { const node = createSynthesizedNode(SyntaxKind.CallExpression); + if (expression.flags & NodeFlags.Optional) node.flags |= NodeFlags.OptionalChain; node.expression = parenthesizeForAccess(expression); node.typeArguments = asNodeArray(typeArguments); node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); @@ -2502,6 +2505,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); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 3809a071421..85ab79b3c35 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -2532,6 +2532,11 @@ namespace ts { return token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken; } + function nextTokenIsOpenBracket() { + nextToken(); + return token() === SyntaxKind.OpenBracketToken; + } + function parseTypeLiteral(): TypeLiteralNode { const node = createNode(SyntaxKind.TypeLiteral); node.members = parseObjectTypeMembers(); @@ -4186,9 +4191,22 @@ namespace ts { function parseMemberExpressionRest(expression: LeftHandSideExpression): MemberExpression { while (true) { - const dotToken = parseOptionalToken(SyntaxKind.DotToken); - if (dotToken) { + const isOptionalExpression = token() === SyntaxKind.QuestionDotToken; + const isOptionalCall = isOptionalExpression && lookAhead(nextTokenIsOpenParenOrLessThan); + if (isOptionalCall) { + // In an optional-chaining call or new expression, we defer parsing `.?` to parseCallExpressionRest. + return expression; + } + + let flags: NodeFlags = NodeFlags.None; + if (isOptionalExpression) flags |= NodeFlags.OptionalExpression; + if (expression.flags & NodeFlags.Optional) flags |= NodeFlags.OptionalChain; + + const isOptionalPropertyAccess = isOptionalExpression && !lookAhead(nextTokenIsOpenBracket); + if (isOptionalPropertyAccess || token() === SyntaxKind.DotToken) { + nextToken(); const propertyAccess = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); + propertyAccess.flags = flags; propertyAccess.expression = expression; propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true); expression = finishNode(propertyAccess); @@ -4198,14 +4216,18 @@ namespace ts { if (token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { nextToken(); const nonNullExpression = createNode(SyntaxKind.NonNullExpression, expression.pos); + nonNullExpression.flags = flags; nonNullExpression.expression = expression; expression = finishNode(nonNullExpression); continue; } // 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. + const isOptionalElementAccess = isOptionalExpression && !isOptionalPropertyAccess; + if (isOptionalElementAccess || (!inDecoratorContext() && parseOptional(SyntaxKind.OpenBracketToken))) { const indexedAccess = createNode(SyntaxKind.ElementAccessExpression, expression.pos); + indexedAccess.flags = flags; indexedAccess.expression = expression; // It's not uncommon for a user to write: "new Type[]". @@ -4225,6 +4247,7 @@ namespace ts { if (token() === SyntaxKind.NoSubstitutionTemplateLiteral || token() === SyntaxKind.TemplateHead) { const tagExpression = createNode(SyntaxKind.TaggedTemplateExpression, expression.pos); + tagExpression.flags = flags; tagExpression.tag = expression; tagExpression.template = token() === SyntaxKind.NoSubstitutionTemplateLiteral ? parseLiteralNode() @@ -4240,32 +4263,41 @@ namespace ts { function parseCallExpressionRest(expression: LeftHandSideExpression): LeftHandSideExpression { while (true) { expression = parseMemberExpressionRest(expression); + + // If we parsed MemberExpression and see a `?.` here, then we are part of an optional chain. + const isOptionalExpression = parseOptional(SyntaxKind.QuestionDotToken); + + let flags: NodeFlags = NodeFlags.None; + if (isOptionalExpression) flags |= NodeFlags.OptionalExpression; + if (expression.flags & NodeFlags.Optional) flags |= NodeFlags.OptionalChain; + + 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 (!typeArguments) { - return expression; + if (isOptionalExpression) { + // If we are part of an optional chain, this *must* be a generic call. + typeArguments = parseTypeArgumentsInExpression(); + } + else { + // 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. + typeArguments = 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.LessThanToken) { + return expression; } - return expression; + const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); + callExpr.flags = flags; + callExpr.expression = expression; + callExpr.typeArguments = typeArguments; + callExpr.arguments = parseArgumentList(); + expression = finishNode(callExpr); } } @@ -4306,6 +4338,7 @@ namespace ts { case SyntaxKind.ColonToken: // foo: case SyntaxKind.SemicolonToken: // foo; case SyntaxKind.QuestionToken: // foo? + case SyntaxKind.QuestionDotToken: // foo?. case SyntaxKind.EqualsEqualsToken: // foo == case SyntaxKind.EqualsEqualsEqualsToken: // foo === case SyntaxKind.ExclamationEqualsToken: // foo != diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index c9c14198279..95ca0e908a7 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -169,6 +169,7 @@ namespace ts { "&&": SyntaxKind.AmpersandAmpersandToken, "||": SyntaxKind.BarBarToken, "?": SyntaxKind.QuestionToken, + "?.": SyntaxKind.QuestionDotToken, ":": SyntaxKind.ColonToken, "=": SyntaxKind.EqualsToken, "+=": SyntaxKind.PlusEqualsToken, @@ -1553,6 +1554,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 0fca09b4540..3ceb1102a64 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; + let optionalExpression: PropertyAccessExpression | ElementAccessExpression | CallExpression | undefined; 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()); + optionalExpression = undefined; return visited; } @@ -103,6 +105,12 @@ namespace ts { return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue); case SyntaxKind.CatchClause: return visitCatchClause(node as CatchClause); + case SyntaxKind.CallExpression: + return visitCallExpression(node as CallExpression); + case SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression(node as PropertyAccessExpression); + case SyntaxKind.ElementAccessExpression: + return visitElementAccessExpression(node as ElementAccessExpression); default: return visitEachChild(node, visitor, context); } @@ -718,6 +726,101 @@ namespace ts { return statements; } + function getEffectiveExpressionOfOptionalExpression(node: PropertyAccessExpression | ElementAccessExpression | CallExpression): Expression | undefined { + if (node.flags & NodeFlags.OptionalExpression) { + return node.expression; + } + + if (node.flags & NodeFlags.OptionalChain) { + if (isPropertyAccessExpression(node.expression) || + isElementAccessExpression(node.expression) || + isCallExpression(node.expression)) { + return getEffectiveExpressionOfOptionalExpression(node.expression); + } + } + } + + type OptionalChain = PropertyAccessExpression | ElementAccessExpression | CallExpression; + + function isOptionalExpression(node: OptionalChain) { + return !!(node.flags & NodeFlags.OptionalExpression); + } + + function isOptionalChain(node: Expression): node is OptionalChain { + return isPropertyAccessExpression(node) + || isElementAccessExpression(node) + || isCallExpression(node); + } + + function visitOptionalChain(node: OptionalChain) { + let chain = node; + const stack: OptionalChain[] = [chain]; + while (!isOptionalExpression(chain) && isOptionalChain(chain.expression)) { + chain = chain.expression; + stack.push(chain); + } + + const temp = createTempVariable(hoistVariableDeclaration); + const root = visitNode(chain.expression, visitor, isExpression); + setOriginalNode(temp, root); + setSourceMapRange(temp, root); + setEmitFlags(temp, EmitFlags.NoComments); + + let expression: LeftHandSideExpression = temp; + while (stack.length) { + chain = stack.pop(); + switch (chain.kind) { + case SyntaxKind.PropertyAccessExpression: + expression = createPropertyAccess(expression, visitNode(chain.name, visitor, isIdentifier)); + break; + case SyntaxKind.ElementAccessExpression: + expression = createElementAccess(expression, visitNode(chain.argumentExpression, visitor, isExpression)); + break; + case SyntaxKind.CallExpression: + expression = createCall(expression, /*typeArguments*/ undefined, visitNodes(chain.arguments, visitor, isExpression)); + break; + } + + setOriginalNode(expression, chain); + setSourceMapRange(expression, chain); + setCommentRange(expression, chain); + setEmitFlags(expression, EmitFlags.NoLeadingComments); + } + + const condition = createEquality(createAssignment(temp, root), createNull()); + setSourceMapRange(condition, root); + + const voidZero = createVoidZero(); + setSourceMapRange(voidZero, root); + + const conditional = createConditional(condition, voidZero, expression); + setOriginalNode(conditional, node); + setSourceMapRange(conditional, node); + setCommentRange(conditional, node); + return conditional; + } + + function visitPropertyAccessExpression(node: PropertyAccessExpression) { + if (node.flags & NodeFlags.Optional) { + return visitOptionalChain(node); + } + return visitEachChild(node, visitor, context); + } + + function visitElementAccessExpression(node: ElementAccessExpression) { + if (node.flags & NodeFlags.Optional) { + return visitOptionalChain(node); + } + return visitEachChild(node, visitor, context); + } + + function visitCallExpression(node: CallExpression) { + if (node.flags & NodeFlags.Optional) { + return visitOptionalChain(node); + } + return visitEachChild(node, visitor, context); + } + function enableSubstitutionForAsyncMethodsWithSuper() { if ((enabledSubstitutions & ESNextSubstitutionFlags.AsyncMethodsWithSuper) === 0) { enabledSubstitutions |= ESNextSubstitutionFlags.AsyncMethodsWithSuper; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4e5ca9f07e7..c5402c01feb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -81,6 +81,7 @@ namespace ts { DotDotDotToken, SemicolonToken, CommaToken, + QuestionDotToken, LessThanToken, LessThanSlashToken, GreaterThanToken, @@ -382,6 +383,7 @@ namespace ts { CommaListExpression, MergeDeclarationMarker, EndOfDeclarationMarker, + OptionalExpression, // Enum value count Count, @@ -425,20 +427,22 @@ namespace ts { 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 + OptionalExpression = 1 << 5, // OptionalExpression (?.) + OptionalChain = 1 << 6, // Optional chaining (expressions following ?.) + ExportContext = 1 << 7, // Export context (initialized by binding) + ContainsThis = 1 << 8, // Interface contains references to "this" + HasImplicitReturn = 1 << 9, // If function implicitly returns on one of codepaths (initialized by binding) + HasExplicitReturn = 1 << 10, // If function has explicit reachable return on one of codepaths (initialized by binding) + GlobalAugmentation = 1 << 11, // Set if module declaration is an augmentation for the global scope + HasAsyncFunctions = 1 << 12, // If the file has async functions (initialized by binding) + DisallowInContext = 1 << 13, // If node was parsed in a context where 'in-expressions' are not allowed + YieldContext = 1 << 14, // If node was parsed in the 'yield' context created when parsing a generator + DecoratorContext = 1 << 15, // If node was parsed as part of a decorator + AwaitContext = 1 << 16, // If node was parsed in the 'await' context created when parsing an async function + ThisNodeHasError = 1 << 17, // If the parser encountered an error when parsing the code that created this node + JavaScriptFile = 1 << 18, // If node was parsed in a JavaScript + ThisNodeOrAnySubNodesHasError = 1 << 19, // If this node or any of its children had an error + HasAggregatedChildData = 1 << 20, // If we've computed data from children and cached it in this node // This flag will be set when the parser encounters a dynamic import expression so that module resolution // will not have to walk the tree if the flag is not set. However, this flag is just a approximation because @@ -449,8 +453,8 @@ namespace ts { // The advantage of this approach is its simplicity. For the case of batch compilation, // we guarantee that users won't have to pay the price of walking the tree if a dynamic import isn't used. /* @internal */ - PossiblyContainsDynamicImport = 1 << 19, - JSDoc = 1 << 20, // If node was parsed inside jsdoc + PossiblyContainsDynamicImport = 1 << 20, + JSDoc = 1 << 21, // If node was parsed inside jsdoc BlockScoped = Let | Const, @@ -458,10 +462,12 @@ namespace ts { ReachabilityAndEmitFlags = ReachabilityCheckFlags | HasAsyncFunctions, // Parsing context flags - ContextFlags = DisallowInContext | YieldContext | DecoratorContext | AwaitContext | JavaScriptFile, + ContextFlags = DisallowInContext | YieldContext | DecoratorContext | AwaitContext | JavaScriptFile | OptionalChain, // Exclude these flags when parsing a Type TypeExcludesFlags = YieldContext | AwaitContext, + + Optional = OptionalExpression | OptionalChain, } export const enum ModifierFlags { @@ -3215,17 +3221,18 @@ namespace ts { NonPrimitive = 1 << 24, // intrinsic object type /* @internal */ JsxAttributes = 1 << 25, // Jsx attributes type - /* @internal */ + Optional = 1 << 26, // Optional chaining type marker + Nullable = Undefined | Null, Literal = StringLiteral | NumberLiteral | BooleanLiteral, Unit = Literal | Nullable, StringOrNumberLiteral = StringLiteral | NumberLiteral, /* @internal */ - DefinitelyFalsy = StringLiteral | NumberLiteral | BooleanLiteral | Void | Undefined | Null, + DefinitelyFalsy = StringLiteral | NumberLiteral | BooleanLiteral | Void | Undefined | Null | Optional, PossiblyFalsy = DefinitelyFalsy | String | Number | Boolean, /* @internal */ - Intrinsic = Any | String | Number | Boolean | BooleanLiteral | ESSymbol | Void | Undefined | Null | Never | NonPrimitive, + Intrinsic = Any | String | Number | Boolean | BooleanLiteral | ESSymbol | Void | Undefined | Null | Never | NonPrimitive | Optional, /* @internal */ Primitive = String | Number | Boolean | Enum | EnumLiteral | ESSymbol | Void | Undefined | Null | Literal, StringLike = String | StringLiteral | Index, @@ -3656,6 +3663,7 @@ namespace ts { emitBOM?: boolean; emitDecoratorMetadata?: boolean; experimentalDecorators?: boolean; + experimentalOptionalChaining?: boolean; forceConsistentCasingInFileNames?: boolean; /*@internal*/help?: boolean; importHelpers?: boolean;