diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index f668037861798..62545e54bc7a4 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2358,9 +2358,9 @@ namespace FourSlash { if (!details) { return this.raiseError(`No completions were found for the given name, source, and preferences.`); } - const codeActions = details.codeActions!; - if (codeActions.length !== 1) { - this.raiseError(`Expected one code action, got ${codeActions.length}`); + const codeActions = details.codeActions; + if (codeActions?.length !== 1) { + this.raiseError(`Expected one code action, got ${codeActions?.length ?? 0}`); } const codeAction = ts.first(codeActions); diff --git a/src/services/codefixes/addMissingAwait.ts b/src/services/codefixes/addMissingAwait.ts index 233c91b153b74..96cf09ed08a75 100644 --- a/src/services/codefixes/addMissingAwait.ts +++ b/src/services/codefixes/addMissingAwait.ts @@ -257,6 +257,7 @@ namespace ts.codefix { sourceFile, insertionSite.parent.expression, createParen(createAwait(insertionSite.parent.expression))); + insertLeadingSemicolonIfNeeded(changeTracker, insertionSite.parent.expression, sourceFile); } else if (contains(callableConstructableErrorCodes, errorCode) && isCallOrNewExpression(insertionSite.parent)) { if (fixedDeclarations && isIdentifier(insertionSite)) { @@ -266,6 +267,7 @@ namespace ts.codefix { } } changeTracker.replaceNode(sourceFile, insertionSite, createParen(createAwait(insertionSite))); + insertLeadingSemicolonIfNeeded(changeTracker, insertionSite, sourceFile); } else { if (fixedDeclarations && isVariableDeclaration(insertionSite.parent) && isIdentifier(insertionSite.parent.name)) { @@ -277,4 +279,11 @@ namespace ts.codefix { changeTracker.replaceNode(sourceFile, insertionSite, createAwait(insertionSite)); } } + + function insertLeadingSemicolonIfNeeded(changeTracker: textChanges.ChangeTracker, beforeNode: Node, sourceFile: SourceFile) { + const precedingToken = findPrecedingToken(beforeNode.pos, sourceFile); + if (precedingToken && positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { + changeTracker.insertText(sourceFile, beforeNode.getStart(sourceFile), ";"); + } + } } diff --git a/src/services/completions.ts b/src/services/completions.ts index c47b4dab95a07..8cca9b678d93e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -373,7 +373,13 @@ namespace ts.Completions { } if (origin && originIsPromise(origin) && propertyAccessToConvert) { if (insertText === undefined) insertText = name; - const awaitText = `(await ${propertyAccessToConvert.expression.getText()})`; + const precedingToken = findPrecedingToken(propertyAccessToConvert.pos, sourceFile); + let awaitText = ""; + if (precedingToken && positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { + awaitText = ";"; + } + + awaitText += `(await ${propertyAccessToConvert.expression.getText()})`; insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`; replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); } diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 8a86cd7a65de2..59dcabfd6bcb2 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -855,13 +855,6 @@ namespace ts.formatting { } function isSemicolonInsertionContext(context: FormattingContext): boolean { - const contextAncestor = findAncestor(context.currentTokenParent, ancestor => { - if (ancestor.end !== context.currentTokenSpan.end) { - return "quit"; - } - return syntaxMayBeASICandidate(ancestor.kind); - }); - - return !!contextAncestor && isASICandidate(contextAncestor, context.sourceFile); + return positionIsASICandidate(context.currentTokenSpan.end, context.currentTokenParent, context.sourceFile); } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 55dc95d3fc2a7..315f61c967d92 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2066,7 +2066,7 @@ namespace ts { syntaxRequiresTrailingModuleBlockOrSemicolonOrASI, syntaxRequiresTrailingSemicolonOrASI); - export function isASICandidate(node: Node, sourceFile: SourceFileLike): boolean { + function nodeIsASICandidate(node: Node, sourceFile: SourceFileLike): boolean { const lastToken = node.getLastToken(sourceFile); if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) { return false; @@ -2109,6 +2109,17 @@ namespace ts { return startLine !== endLine; } + export function positionIsASICandidate(pos: number, context: Node, sourceFile: SourceFileLike): boolean { + const contextAncestor = findAncestor(context, ancestor => { + if (ancestor.end !== pos) { + return "quit"; + } + return syntaxMayBeASICandidate(ancestor.kind); + }); + + return !!contextAncestor && nodeIsASICandidate(contextAncestor, sourceFile); + } + export function probablyUsesSemicolons(sourceFile: SourceFile): boolean { let withSemicolon = 0; let withoutSemicolon = 0; diff --git a/tests/cases/fourslash/codeFixAddMissingAwait_propertyAccess2.ts b/tests/cases/fourslash/codeFixAddMissingAwait_propertyAccess2.ts new file mode 100644 index 0000000000000..e024028b1406f --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwait_propertyAccess2.ts @@ -0,0 +1,15 @@ +/// +////async function fn(a: Promise<{ x: string }>) { +//// console.log(3) +//// a.x; +////} + +verify.codeFix({ + description: ts.Diagnostics.Add_await.message, + index: 0, + newFileContent: +`async function fn(a: Promise<{ x: string }>) { + console.log(3) + ;(await a).x; +}` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingAwait_signatures2.ts b/tests/cases/fourslash/codeFixAddMissingAwait_signatures2.ts new file mode 100644 index 0000000000000..17f6651b3791c --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingAwait_signatures2.ts @@ -0,0 +1,50 @@ +/// +////async function fn(a: Promise<() => void>, b: Promise<() => void> | (() => void), C: Promise<{ new(): any }>) { +//// a() +//// b() +//// new C() +////} + +verify.codeFix({ + description: ts.Diagnostics.Add_await.message, + index: 0, + newFileContent: +`async function fn(a: Promise<() => void>, b: Promise<() => void> | (() => void), C: Promise<{ new(): any }>) { + (await a)() + b() + new C() +}` +}); + +verify.codeFix({ + description: ts.Diagnostics.Add_await.message, + index: 1, + newFileContent: +`async function fn(a: Promise<() => void>, b: Promise<() => void> | (() => void), C: Promise<{ new(): any }>) { + a() + ;(await b)() + new C() +}` +}); + +verify.codeFix({ + description: ts.Diagnostics.Add_await.message, + index: 2, + newFileContent: +`async function fn(a: Promise<() => void>, b: Promise<() => void> | (() => void), C: Promise<{ new(): any }>) { + a() + b() + new (await C)() +}` +}); + +verify.codeFixAll({ + fixAllDescription: ts.Diagnostics.Fix_all_expressions_possibly_missing_await.message, + fixId: "addMissingAwait", + newFileContent: +`async function fn(a: Promise<() => void>, b: Promise<() => void> | (() => void), C: Promise<{ new(): any }>) { + (await a)() + ;(await b)() + new (await C)() +}` +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise7.ts b/tests/cases/fourslash/completionOfAwaitPromise7.ts new file mode 100644 index 0000000000000..b8860a7f2c16c --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise7.ts @@ -0,0 +1,18 @@ +/// + +////async function foo(x: Promise) { +//// console.log +//// [|x./**/|] +////} + +const replacementSpan = test.ranges()[0]; +verify.completions({ + marker: "", + includes: [ + "then", + { name: "trim", insertText: ';(await x).trim', replacementSpan }, + ], + preferences: { + includeInsertTextCompletions: true, + }, +});