Skip to content

fix(44639): Transpilation of optional chaining combined with type casting results in function call losing its context #44666

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

Merged
merged 4 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 53 additions & 8 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2747,7 +2747,7 @@ namespace ts {
function emitYieldExpression(node: YieldExpression) {
emitTokenWithComment(SyntaxKind.YieldKeyword, node.pos, writeKeyword, node);
emit(node.asteriskToken);
emitExpressionWithLeadingSpace(node.expression, parenthesizer.parenthesizeExpressionForDisallowedComma);
emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsiAndDisallowedComma);
}

function emitSpreadElement(node: SpreadElement) {
Expand Down Expand Up @@ -2971,9 +2971,49 @@ namespace ts {
return pos;
}

function commentWillEmitNewLine(node: CommentRange) {
return node.kind === SyntaxKind.SingleLineCommentTrivia || !!node.hasTrailingNewLine;
}

function willEmitLeadingNewLine(node: Expression): boolean {
if (!currentSourceFile) return false;
if (some(getLeadingCommentRanges(currentSourceFile.text, node.pos), commentWillEmitNewLine)) return true;
if (some(getSyntheticLeadingComments(node), commentWillEmitNewLine)) return true;
if (isPartiallyEmittedExpression(node)) {
if (node.pos !== node.expression.pos) {
if (some(getTrailingCommentRanges(currentSourceFile.text, node.expression.pos), commentWillEmitNewLine)) return true;
}
return willEmitLeadingNewLine(node.expression);
}
return false;
}

/**
* Wraps an expression in parens if we would emit a leading comment that would introduce a line separator
* between the node and its parent.
*/
function parenthesizeExpressionForNoAsi(node: Expression) {
if (!commentsDisabled && isPartiallyEmittedExpression(node) && willEmitLeadingNewLine(node)) {
const parseNode = getParseTreeNode(node);
if (parseNode && isParenthesizedExpression(parseNode)) {
// If the original node was a parenthesized expression, restore it to preserve comment and source map emit
const parens = factory.createParenthesizedExpression(node.expression);
setOriginalNode(parens, node);
setTextRange(parens, parseNode);
return parens;
}
return factory.createParenthesizedExpression(node);
}
return node;
}

function parenthesizeExpressionForNoAsiAndDisallowedComma(node: Expression) {
return parenthesizeExpressionForNoAsi(parenthesizer.parenthesizeExpressionForDisallowedComma(node));
}

function emitReturnStatement(node: ReturnStatement) {
emitTokenWithComment(SyntaxKind.ReturnKeyword, node.pos, writeKeyword, /*contextNode*/ node);
emitExpressionWithLeadingSpace(node.expression);
emitExpressionWithLeadingSpace(node.expression && parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi);
writeTrailingSemicolon();
}

Expand Down Expand Up @@ -3005,7 +3045,7 @@ namespace ts {

function emitThrowStatement(node: ThrowStatement) {
emitTokenWithComment(SyntaxKind.ThrowKeyword, node.pos, writeKeyword, node);
emitExpressionWithLeadingSpace(node.expression);
emitExpressionWithLeadingSpace(parenthesizeExpressionForNoAsi(node.expression), parenthesizeExpressionForNoAsi);
writeTrailingSemicolon();
}

Expand Down Expand Up @@ -3924,7 +3964,14 @@ namespace ts {
// Transformation nodes

function emitPartiallyEmittedExpression(node: PartiallyEmittedExpression) {
const emitFlags = getEmitFlags(node);
if (!(emitFlags & EmitFlags.NoLeadingComments) && node.pos !== node.expression.pos) {
emitTrailingCommentsOfPosition(node.expression.pos);
}
emitExpression(node.expression);
if (!(emitFlags & EmitFlags.NoTrailingComments) && node.end !== node.expression.end) {
emitLeadingCommentsOfPosition(node.expression.end);
}
}

function emitCommaList(node: CommaListExpression) {
Expand Down Expand Up @@ -4312,10 +4359,8 @@ namespace ts {
// Emit this child.
previousSourceFileTextKind = recordBundleFileInternalSectionStart(child);
if (shouldEmitInterveningComments) {
if (emitTrailingCommentsOfPosition) {
const commentRange = getCommentRange(child);
emitTrailingCommentsOfPosition(commentRange.pos);
}
const commentRange = getCommentRange(child);
emitTrailingCommentsOfPosition(commentRange.pos);
}
else {
shouldEmitInterveningComments = mayEmitInterveningComments;
Expand Down Expand Up @@ -4683,7 +4728,7 @@ namespace ts {
function writeLineSeparatorsAndIndentBefore(node: Node, parent: Node): boolean {
const leadingNewlines = preserveSourceNewlines && getLeadingLineTerminatorCount(parent, [node], ListFormat.None);
if (leadingNewlines) {
writeLinesAndIndent(leadingNewlines, /*writeLinesIfNotIndenting*/ false);
writeLinesAndIndent(leadingNewlines, /*writeSpaceIfNotIndenting*/ false);
}
return !!leadingNewlines;
}
Expand Down
14 changes: 9 additions & 5 deletions src/compiler/transformers/es2020.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@ namespace ts {

function visitOptionalExpression(node: OptionalChain, captureThisArg: boolean, isDelete: boolean): Expression {
const { expression, chain } = flattenChain(node);
const left = visitNonOptionalExpression(expression, isCallChain(chain[0]), /*isDelete*/ false);
const leftThisArg = isSyntheticReference(left) ? left.thisArg : undefined;
let leftExpression = isSyntheticReference(left) ? left.expression : left;
let capturedLeft: Expression = leftExpression;
if (!isSimpleCopiableExpression(leftExpression)) {
const left = visitNonOptionalExpression(skipPartiallyEmittedExpressions(expression), isCallChain(chain[0]), /*isDelete*/ false);
let leftThisArg = isSyntheticReference(left) ? left.thisArg : undefined;
let capturedLeft = isSyntheticReference(left) ? left.expression : left;
let leftExpression = factory.restoreOuterExpressions(expression, capturedLeft, OuterExpressionKinds.PartiallyEmittedExpressions);
if (!isSimpleCopiableExpression(capturedLeft)) {
capturedLeft = factory.createTempVariable(hoistVariableDeclaration);
leftExpression = factory.createAssignment(capturedLeft, leftExpression);
}
Expand All @@ -152,6 +152,10 @@ namespace ts {
break;
case SyntaxKind.CallExpression:
if (i === 0 && leftThisArg) {
if (!isGeneratedIdentifier(leftThisArg)) {
leftThisArg = factory.cloneNode(leftThisArg);
addEmitFlags(leftThisArg, EmitFlags.NoComments);
}
rightExpression = factory.createFunctionCallCall(
rightExpression,
leftThisArg.kind === SyntaxKind.SuperKeyword ? factory.createThis() : leftThisArg,
Expand Down
9 changes: 4 additions & 5 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2237,11 +2237,10 @@ namespace ts {
// we can safely elide the parentheses here, as a new synthetic
// ParenthesizedExpression will be inserted if we remove parentheses too
// aggressively.
// HOWEVER - if there are leading comments on the expression itself, to handle ASI
// correctly for return and throw, we must keep the parenthesis
if (length(getLeadingCommentRangesOfNode(expression, currentSourceFile))) {
return factory.updateParenthesizedExpression(node, expression);
}
//
// If there are leading comments on the expression itself, the emitter will handle ASI
// for return, throw, and yield by re-introducing parenthesis during emit on an as-need
// basis.
return factory.createPartiallyEmittedExpression(expression, node);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//// [optionalChainingInTypeAssertions.ts]
class Foo {
m() {}
}

const foo = new Foo();

(foo.m as any)?.();
(<any>foo.m)?.();

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();


//// [optionalChainingInTypeAssertions.js]
var _a, _b, _c, _d;
class Foo {
m() { }
}
const foo = new Foo();
(_a = foo.m) === null || _a === void 0 ? void 0 : _a.call(foo);
(_b = foo.m) === null || _b === void 0 ? void 0 : _b.call(foo);
/*a1*/ (_c = /*a2*/ foo.m /*a3*/ /*a4*/) === null || _c === void 0 ? void 0 : _c.call(foo);
/*b1*/ (_d = /*b2*/ foo.m /*b3*/ /*b4*/) === null || _d === void 0 ? void 0 : _d.call(foo);
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
=== tests/cases/conformance/expressions/optionalChaining/optionalChainingInTypeAssertions.ts ===
class Foo {
>Foo : Symbol(Foo, Decl(optionalChainingInTypeAssertions.ts, 0, 0))

m() {}
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
}

const foo = new Foo();
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>Foo : Symbol(Foo, Decl(optionalChainingInTypeAssertions.ts, 0, 0))

(foo.m as any)?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

(<any>foo.m)?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
=== tests/cases/conformance/expressions/optionalChaining/optionalChainingInTypeAssertions.ts ===
class Foo {
>Foo : Foo

m() {}
>m : () => void
}

const foo = new Foo();
>foo : Foo
>new Foo() : Foo
>Foo : typeof Foo

(foo.m as any)?.();
>(foo.m as any)?.() : any
>(foo.m as any) : any
>foo.m as any : any
>foo.m : () => void
>foo : Foo
>m : () => void

(<any>foo.m)?.();
>(<any>foo.m)?.() : any
>(<any>foo.m) : any
><any>foo.m : any
>foo.m : () => void
>foo : Foo
>m : () => void

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
>(/*a2*/foo.m as any/*a3*/)/*a4*/?.() : any
>(/*a2*/foo.m as any/*a3*/) : any
>foo.m as any : any
>foo.m : () => void
>foo : Foo
>m : () => void

/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();
>(/*b2*/<any>foo.m/*b3*/)/*b4*/?.() : any
>(/*b2*/<any>foo.m/*b3*/) : any
><any>foo.m : any
>foo.m : () => void
>foo : Foo
>m : () => void

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//// [optionalChainingInTypeAssertions.ts]
class Foo {
m() {}
}

const foo = new Foo();

(foo.m as any)?.();
(<any>foo.m)?.();

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();


//// [optionalChainingInTypeAssertions.js]
class Foo {
m() { }
}
const foo = new Foo();
foo.m?.();
foo.m?.();
/*a1*/ /*a2*/ foo.m /*a3*/ /*a4*/?.();
/*b1*/ /*b2*/ foo.m /*b3*/ /*b4*/?.();
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
=== tests/cases/conformance/expressions/optionalChaining/optionalChainingInTypeAssertions.ts ===
class Foo {
>Foo : Symbol(Foo, Decl(optionalChainingInTypeAssertions.ts, 0, 0))

m() {}
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
}

const foo = new Foo();
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>Foo : Symbol(Foo, Decl(optionalChainingInTypeAssertions.ts, 0, 0))

(foo.m as any)?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

(<any>foo.m)?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();
>foo.m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))
>foo : Symbol(foo, Decl(optionalChainingInTypeAssertions.ts, 4, 5))
>m : Symbol(Foo.m, Decl(optionalChainingInTypeAssertions.ts, 0, 11))

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
=== tests/cases/conformance/expressions/optionalChaining/optionalChainingInTypeAssertions.ts ===
class Foo {
>Foo : Foo

m() {}
>m : () => void
}

const foo = new Foo();
>foo : Foo
>new Foo() : Foo
>Foo : typeof Foo

(foo.m as any)?.();
>(foo.m as any)?.() : any
>(foo.m as any) : any
>foo.m as any : any
>foo.m : () => void
>foo : Foo
>m : () => void

(<any>foo.m)?.();
>(<any>foo.m)?.() : any
>(<any>foo.m) : any
><any>foo.m : any
>foo.m : () => void
>foo : Foo
>m : () => void

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
>(/*a2*/foo.m as any/*a3*/)/*a4*/?.() : any
>(/*a2*/foo.m as any/*a3*/) : any
>foo.m as any : any
>foo.m : () => void
>foo : Foo
>m : () => void

/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();
>(/*b2*/<any>foo.m/*b3*/)/*b4*/?.() : any
>(/*b2*/<any>foo.m/*b3*/) : any
><any>foo.m : any
>foo.m : () => void
>foo : Foo
>m : () => void

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @target: es2015, esnext

class Foo {
m() {}
}

const foo = new Foo();

(foo.m as any)?.();
(<any>foo.m)?.();

/*a1*/(/*a2*/foo.m as any/*a3*/)/*a4*/?.();
/*b1*/(/*b2*/<any>foo.m/*b3*/)/*b4*/?.();