Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Optional Chaining #33294

Merged
merged 25 commits into from
Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c95daab
Add support for Optional Chaining
rbuckton Sep 6, 2019
c2f53fc
Add grammar error for invalid tagged template, more tests
rbuckton Sep 7, 2019
0f5d5d6
Prototype
andrewbranch Sep 23, 2019
24de747
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 23, 2019
7be47ab
PR feedback
rbuckton Sep 24, 2019
e073c05
Add errors for invalid assignments and a trailing '?.'
rbuckton Sep 24, 2019
72f44d9
Add additional signature help test, fix lint warnings
rbuckton Sep 24, 2019
488c9e6
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 24, 2019
2ffd8e1
Merge branch 'enhancement/auto-insert-question-dot' of github.com:and…
rbuckton Sep 24, 2019
096bb49
Fix to insert text for completions
rbuckton Sep 24, 2019
1bf2d56
Add initial control-flow analysis for optional chains
rbuckton Sep 25, 2019
1d7446f
PR Feedback and more tests
rbuckton Sep 26, 2019
b282b62
Update to control flow
rbuckton Sep 26, 2019
be3e21f
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 26, 2019
fd8c0d4
Remove mangled smart quotes in comments
rbuckton Sep 26, 2019
7c9ef50
Fix lint, PR feedback
rbuckton Sep 27, 2019
ad7c33c
Updates to control flow
rbuckton Sep 28, 2019
6b49a03
Switch to FlowCondition for CFA of optional chains
rbuckton Sep 29, 2019
aaa30f4
Fix ?. insertion for completions on type variables
rbuckton Sep 29, 2019
5ea7cb5
Accept API baseline change
rbuckton Sep 29, 2019
7463860
Clean up types
rbuckton Sep 29, 2019
0828674
improve control-flow debug output
rbuckton Sep 30, 2019
d408e81
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 30, 2019
c2070be
Revert Debug.formatControlFlowGraph helper
rbuckton Sep 30, 2019
dfc798f
Merge branch 'master' into optionalChainingStage3
rbuckton Sep 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3184,6 +3184,10 @@ namespace ts {
const callee = skipOuterExpressions(node.expression);
const expression = node.expression;

if (node.flags & NodeFlags.OptionalChain) {
transformFlags |= TransformFlags.ContainsESNext;
}

if (node.typeArguments) {
transformFlags |= TransformFlags.AssertTypeScript;
}
Expand Down Expand Up @@ -3579,6 +3583,10 @@ namespace ts {
function computePropertyAccess(node: PropertyAccessExpression, subtreeFlags: TransformFlags) {
let transformFlags = subtreeFlags;

if (node.flags & NodeFlags.OptionalChain) {
transformFlags |= TransformFlags.ContainsESNext;
}

// If a PropertyAccessExpression starts with a super keyword, then it is
// ES6 syntax, and requires a lexical `this` binding.
if (node.expression.kind === SyntaxKind.SuperKeyword) {
Expand All @@ -3594,6 +3602,10 @@ namespace ts {
function computeElementAccess(node: ElementAccessExpression, subtreeFlags: TransformFlags) {
let transformFlags = subtreeFlags;

if (node.flags & NodeFlags.OptionalChain) {
transformFlags |= TransformFlags.ContainsESNext;
}

// If an ElementAccessExpression starts with a super keyword, then it is
// ES6 syntax, and requires a lexical `this` binding.
if (node.expression.kind === SyntaxKind.SuperKeyword) {
Expand Down
165 changes: 125 additions & 40 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/compiler/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ namespace ts {
assertNode)
: noop;

export const assertNotNode = shouldAssert(AssertionLevel.Normal)
? (node: Node | undefined, test: ((node: Node | undefined) => boolean) | undefined, message?: string): void => assert(
test === undefined || !test(node),
message || "Unexpected node.",
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
assertNode)
: noop;

export const assertOptionalNode = shouldAssert(AssertionLevel.Normal)
? (node: Node, test: (node: Node) => boolean, message?: string): void => assert(
test === undefined || node === undefined || test(node),
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,10 @@
"category": "Error",
"code": 1357
},
"Tagged template expressions are not permitted in an optional chain.": {
"category": "Error",
"code": 1358
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down Expand Up @@ -2722,6 +2726,26 @@
"category": "Error",
"code": 2773
},
"The operand of an increment or decrement operator may not be an optional property access.": {
"category": "Error",
"code": 2774
},
"The target of an object rest assignment may not be an optional property access.": {
"category": "Error",
"code": 2775
},
"The left-hand side of an assignment expression may not be an optional property access.": {
"category": "Error",
"code": 2776
},
"The left-hand side of a 'for...in' statement may not be an optional property access.": {
"category": "Error",
"code": 2777
},
"The left-hand side of a 'for...of' statement may not be an optional property access.": {
"category": "Error",
"code": 2778
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
110 changes: 69 additions & 41 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,8 @@ namespace ts {
let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number}[] | undefined;
let hasWrittenComment = false;
let commentsDisabled = !!printerOptions.removeComments;
let lastNode: Node | undefined;
let lastSubstitution: Node | undefined;
const { enter: enterComment, exit: exitComment } = performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment");

reset();
Expand Down Expand Up @@ -1082,8 +1084,7 @@ namespace ts {
setSourceFile(sourceFile);
}

const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
pipelinePhase(hint, node);
pipelineEmit(hint, node);
}

function setSourceFile(sourceFile: SourceFile | undefined) {
Expand Down Expand Up @@ -1115,31 +1116,56 @@ namespace ts {
currentSourceFile = undefined!;
currentLineMap = undefined!;
detachedCommentsInfo = undefined;
lastNode = undefined;
lastSubstitution = undefined;
setWriter(/*output*/ undefined, /*_sourceMapGenerator*/ undefined);
}

function getCurrentLineMap() {
return currentLineMap || (currentLineMap = getLineStarts(currentSourceFile!));
}

function emit(node: Node): Node;
function emit(node: Node | undefined): Node | undefined;
function emit(node: Node | undefined) {
if (node === undefined) return;

const prevSourceFileTextKind = recordBundleFileInternalSectionStart(node);
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
pipelinePhase(EmitHint.Unspecified, node);
const substitute = pipelineEmit(EmitHint.Unspecified, node);
recordBundleFileInternalSectionEnd(prevSourceFileTextKind);
return substitute;
}

function emitIdentifierName(node: Identifier | undefined) {
function emitIdentifierName(node: Identifier): Node;
function emitIdentifierName(node: Identifier | undefined): Node | undefined;
function emitIdentifierName(node: Identifier | undefined): Node | undefined {
if (node === undefined) return;
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
pipelinePhase(EmitHint.IdentifierName, node);
return pipelineEmit(EmitHint.IdentifierName, node);
}

function emitExpression(node: Expression | undefined) {
function emitExpression(node: Expression): Node;
function emitExpression(node: Expression | undefined): Node | undefined;
function emitExpression(node: Expression | undefined): Node | undefined {
if (node === undefined) return;
return pipelineEmit(EmitHint.Expression, node);
}

function pipelineEmit(emitHint: EmitHint, node: Node) {
const savedLastNode = lastNode;
const savedLastSubstitution = lastSubstitution;
lastNode = node;
lastSubstitution = undefined;

const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
pipelinePhase(EmitHint.Expression, node);
pipelinePhase(emitHint, node);

Debug.assert(lastNode === node);

const substitute = lastSubstitution;
lastNode = savedLastNode;
lastSubstitution = savedLastSubstitution;

return substitute || node;
}

function getPipelinePhase(phase: PipelinePhase, node: Node) {
Expand Down Expand Up @@ -1181,11 +1207,14 @@ namespace ts {
}

function pipelineEmitWithNotification(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, node);
onEmitNode(hint, node, pipelinePhase);
Debug.assert(lastNode === node);
}

function pipelineEmitWithHint(hint: EmitHint, node: Node): void {
Debug.assert(lastNode === node || lastSubstitution === node);
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier));
if (hint === EmitHint.MappedTypeParameter) return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration));
Expand Down Expand Up @@ -1497,7 +1526,7 @@ namespace ts {
if (isExpression(node)) {
hint = EmitHint.Expression;
if (substituteNode !== noEmitSubstitution) {
node = substituteNode(hint, node);
lastSubstitution = node = substituteNode(hint, node);
}
}
else if (isToken(node)) {
Expand Down Expand Up @@ -1613,8 +1642,11 @@ namespace ts {
}

function pipelineEmitWithSubstitution(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node || lastSubstitution === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, node);
pipelinePhase(hint, substituteNode(hint, node));
lastSubstitution = substituteNode(hint, node);
pipelinePhase(hint, lastSubstitution);
Debug.assert(lastNode === node || lastSubstitution === node);
}

function getHelpersFromBundledSourceFiles(bundle: Bundle): string[] | undefined {
Expand Down Expand Up @@ -2118,8 +2150,7 @@ namespace ts {
}
writePunctuation("[");

const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node.typeParameter);
pipelinePhase(EmitHint.MappedTypeParameter, node.typeParameter);
pipelineEmit(EmitHint.MappedTypeParameter, node.typeParameter);

writePunctuation("]");
if (node.questionToken) {
Expand Down Expand Up @@ -2217,70 +2248,60 @@ namespace ts {
}

function emitPropertyAccessExpression(node: PropertyAccessExpression) {
let indentBeforeDot = false;
let indentAfterDot = false;
const dotRangeFirstCommentStart = skipTrivia(
currentSourceFile!.text,
node.expression.end,
/*stopAfterLineBreak*/ false,
/*stopAtComments*/ true
);
const dotRangeStart = skipTrivia(currentSourceFile!.text, dotRangeFirstCommentStart);
const dotRangeEnd = dotRangeStart + 1;
if (!(getEmitFlags(node) & EmitFlags.NoIndentation)) {
const dotToken = createToken(SyntaxKind.DotToken);
dotToken.pos = node.expression.end;
dotToken.end = dotRangeEnd;
indentBeforeDot = needsIndentation(node, node.expression, dotToken);
indentAfterDot = needsIndentation(node, dotToken, node.name);
}
const expression = cast(emitExpression(node.expression), isExpression);
const token = getDotOrQuestionDotToken(node);
const indentBeforeDot = needsIndentation(node, node.expression, token);
const indentAfterDot = needsIndentation(node, token, node.name);

emitExpression(node.expression);
increaseIndentIf(indentBeforeDot, /*writeSpaceIfNotIndenting*/ false);

const dotHasCommentTrivia = dotRangeFirstCommentStart !== dotRangeStart;
const shouldEmitDotDot = !indentBeforeDot && needsDotDotForPropertyAccess(node.expression, dotHasCommentTrivia);
const shouldEmitDotDot =
token.kind !== SyntaxKind.QuestionDotToken &&
mayNeedDotDotForPropertyAccess(expression) &&
!writer.hasPrecedingComment() &&
!writer.hasPrecedingWhitespace();

if (shouldEmitDotDot) {
writePunctuation(".");
}
emitTokenWithComment(SyntaxKind.DotToken, node.expression.end, writePunctuation, node);

emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node);
increaseIndentIf(indentAfterDot, /*writeSpaceIfNotIndenting*/ false);
emit(node.name);
decreaseIndentIf(indentBeforeDot, indentAfterDot);
}

// 1..toString is a valid property access, emit a dot after the literal
// Also emit a dot if expression is a integer const enum value - it will appear in generated code as numeric literal
function needsDotDotForPropertyAccess(expression: Expression, dotHasTrivia: boolean) {
function mayNeedDotDotForPropertyAccess(expression: Expression) {
expression = skipPartiallyEmittedExpressions(expression);
if (isNumericLiteral(expression)) {
// check if numeric literal is a decimal literal that was originally written with a dot
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true);
// If he number will be printed verbatim and it doesn't already contain a dot, add one
// if the expression doesn't have any comments that will be emitted.
return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!) &&
(!dotHasTrivia || printerOptions.removeComments);
return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!);
}
else if (isPropertyAccessExpression(expression) || isElementAccessExpression(expression)) {
// check if constant enum value is integer
const constantValue = getConstantValue(expression);
// isFinite handles cases when constantValue is undefined
return typeof constantValue === "number" && isFinite(constantValue)
&& Math.floor(constantValue) === constantValue
&& printerOptions.removeComments;
&& Math.floor(constantValue) === constantValue;
}
}

function emitElementAccessExpression(node: ElementAccessExpression) {
emitExpression(node.expression);
emit(node.questionDotToken);
emitTokenWithComment(SyntaxKind.OpenBracketToken, node.expression.end, writePunctuation, node);
emitExpression(node.argumentExpression);
emitTokenWithComment(SyntaxKind.CloseBracketToken, node.argumentExpression.end, writePunctuation, node);
}

function emitCallExpression(node: CallExpression) {
emitExpression(node.expression);
emit(node.questionDotToken);
emitTypeArguments(node, node.typeArguments);
emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments);
}
Expand Down Expand Up @@ -3744,8 +3765,7 @@ namespace ts {
writeLine();
increaseIndent();
if (isEmptyStatement(node)) {
const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, node);
pipelinePhase(EmitHint.EmbeddedStatement, node);
pipelineEmit(EmitHint.EmbeddedStatement, node);
}
else {
emit(node);
Expand Down Expand Up @@ -4216,6 +4236,10 @@ namespace ts {
}

function needsIndentation(parent: Node, node1: Node, node2: Node): boolean {
if (getEmitFlags(parent) & EmitFlags.NoIndentation) {
return false;
}

parent = skipSynthesizedParentheses(parent);
node1 = skipSynthesizedParentheses(node1);
node2 = skipSynthesizedParentheses(node2);
Expand Down Expand Up @@ -4671,6 +4695,7 @@ namespace ts {
// Comments

function pipelineEmitWithComments(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node || lastSubstitution === node);
enterComment();
hasWrittenComment = false;
const emitFlags = getEmitFlags(node);
Expand Down Expand Up @@ -4737,6 +4762,7 @@ namespace ts {
}
}
exitComment();
Debug.assert(lastNode === node || lastSubstitution === node);
}

function emitLeadingSynthesizedComment(comment: SynthesizedComment) {
Expand Down Expand Up @@ -4983,6 +5009,7 @@ namespace ts {
}

function pipelineEmitWithSourceMap(hint: EmitHint, node: Node) {
Debug.assert(lastNode === node || lastSubstitution === node);
const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, node);
if (isUnparsedSource(node) || isUnparsedPrepend(node)) {
pipelinePhase(hint, node);
Expand Down Expand Up @@ -5025,6 +5052,7 @@ namespace ts {
emitSourcePos(source, end);
}
}
Debug.assert(lastNode === node || lastSubstitution === node);
}

/**
Expand Down
Loading