Skip to content

Commit 71bec5b

Browse files
authored
Add quick fix to add missing 'await' (microsoft#32356)
* Start prototyping addMissingAwait codefix * Filter by diagnostics that have missing-await related info * Start writing tests and checking precedence * Implement codeFixAll, add test for binary expressions * Add test for iterables * Add test for passing argument * Add test for call/construct signatures * Add test for awaiting initializer * Improve assertion error * Replace specific property access error with general one and add await related info * Add test for property access * Require code to be inside a function body to offer await * Accept suggestion Co-Authored-By: Nathan Shively-Sanders <[email protected]> * Add explicit test for code fix being not available unless something is a Promise * Skip looking for function body if already in AwaitContext flags * Inline getCodeActions function for symmetry
1 parent d4b2149 commit 71bec5b

16 files changed

+438
-12
lines changed

src/compiler/checker.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20715,7 +20715,8 @@ namespace ts {
2071520715
else {
2071620716
const promisedType = getPromisedTypeOfPromise(containingType);
2071720717
if (promisedType && getPropertyOfType(promisedType, propNode.escapedText)) {
20718-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_forget_to_use_await, declarationNameToString(propNode), typeToString(containingType));
20718+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
20719+
relatedInfo = createDiagnosticForNode(propNode, Diagnostics.Did_you_forget_to_use_await);
2071920720
}
2072020721
else {
2072120722
const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType);

src/compiler/diagnosticMessages.json

+13-4
Original file line numberDiff line numberDiff line change
@@ -2076,10 +2076,6 @@
20762076
"category": "Error",
20772077
"code": 2569
20782078
},
2079-
"Property '{0}' does not exist on type '{1}'. Did you forget to use 'await'?": {
2080-
"category": "Error",
2081-
"code": 2570
2082-
},
20832079
"Object is of type 'unknown'.": {
20842080
"category": "Error",
20852081
"code": 2571
@@ -5087,6 +5083,19 @@
50875083
"category": "Message",
50885084
"code": 95082
50895085
},
5086+
"Add 'await'": {
5087+
"category": "Message",
5088+
"code": 95083
5089+
},
5090+
"Add 'await' to initializer for '{0}'": {
5091+
"category": "Message",
5092+
"code": 95084
5093+
},
5094+
"Fix all expressions possibly missing 'await'": {
5095+
"category": "Message",
5096+
"code": 95085
5097+
},
5098+
50905099
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
50915100
"category": "Error",
50925101
"code": 18004

src/harness/fourslash.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -2828,11 +2828,28 @@ Actual: ${stringify(fullActual)}`);
28282828
}
28292829
}
28302830

2831-
public verifyCodeFixAvailable(negative: boolean, expected: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined): void {
2832-
assert(!negative || !expected);
2831+
public verifyCodeFixAvailable(negative: boolean, expected: FourSlashInterface.VerifyCodeFixAvailableOptions[] | string | undefined): void {
28332832
const codeFixes = this.getCodeFixes(this.activeFile.fileName);
2834-
const actuals = codeFixes.map((fix): FourSlashInterface.VerifyCodeFixAvailableOptions => ({ description: fix.description, commands: fix.commands }));
2835-
this.assertObjectsEqual(actuals, negative ? ts.emptyArray : expected);
2833+
if (negative) {
2834+
if (typeof expected === "undefined") {
2835+
this.assertObjectsEqual(codeFixes, ts.emptyArray);
2836+
}
2837+
else if (typeof expected === "string") {
2838+
if (codeFixes.some(fix => fix.fixName === expected)) {
2839+
this.raiseError(`Expected not to find a fix with the name '${expected}', but one exists.`);
2840+
}
2841+
}
2842+
else {
2843+
assert(typeof expected === "undefined" || typeof expected === "string", "With a negated assertion, 'expected' must be undefined or a string value of a codefix name.");
2844+
}
2845+
}
2846+
else if (typeof expected === "string") {
2847+
this.assertObjectsEqual(codeFixes.map(fix => fix.fixName), [expected]);
2848+
}
2849+
else {
2850+
const actuals = codeFixes.map((fix): FourSlashInterface.VerifyCodeFixAvailableOptions => ({ description: fix.description, commands: fix.commands }));
2851+
this.assertObjectsEqual(actuals, negative ? ts.emptyArray : expected);
2852+
}
28362853
}
28372854

28382855
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
type ContextualTrackChangesFunction = (cb: (changeTracker: textChanges.ChangeTracker) => void) => FileTextChanges[];
4+
const fixId = "addMissingAwait";
5+
const propertyAccessCode = Diagnostics.Property_0_does_not_exist_on_type_1.code;
6+
const callableConstructableErrorCodes = [
7+
Diagnostics.This_expression_is_not_callable.code,
8+
Diagnostics.This_expression_is_not_constructable.code,
9+
];
10+
const errorCodes = [
11+
Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type.code,
12+
Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type.code,
13+
Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type.code,
14+
Diagnostics.Operator_0_cannot_be_applied_to_type_1.code,
15+
Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2.code,
16+
Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap.code,
17+
Diagnostics.Type_0_is_not_an_array_type.code,
18+
Diagnostics.Type_0_is_not_an_array_type_or_a_string_type.code,
19+
Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_Use_compiler_option_downlevelIteration_to_allow_iterating_of_iterators.code,
20+
Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator.code,
21+
Diagnostics.Type_0_is_not_an_array_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator.code,
22+
Diagnostics.Type_0_must_have_a_Symbol_iterator_method_that_returns_an_iterator.code,
23+
Diagnostics.Type_0_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator.code,
24+
Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code,
25+
propertyAccessCode,
26+
...callableConstructableErrorCodes,
27+
];
28+
29+
registerCodeFix({
30+
fixIds: [fixId],
31+
errorCodes,
32+
getCodeActions: context => {
33+
const { sourceFile, errorCode, span, cancellationToken, program } = context;
34+
const expression = getAwaitableExpression(sourceFile, errorCode, span, cancellationToken, program);
35+
if (!expression) {
36+
return;
37+
}
38+
39+
const checker = context.program.getTypeChecker();
40+
const trackChanges: ContextualTrackChangesFunction = cb => textChanges.ChangeTracker.with(context, cb);
41+
return compact([
42+
getDeclarationSiteFix(context, expression, errorCode, checker, trackChanges),
43+
getUseSiteFix(context, expression, errorCode, checker, trackChanges)]);
44+
},
45+
getAllCodeActions: context => {
46+
const { sourceFile, program, cancellationToken } = context;
47+
const checker = context.program.getTypeChecker();
48+
return codeFixAll(context, errorCodes, (t, diagnostic) => {
49+
const expression = getAwaitableExpression(sourceFile, diagnostic.code, diagnostic, cancellationToken, program);
50+
if (!expression) {
51+
return;
52+
}
53+
const trackChanges: ContextualTrackChangesFunction = cb => (cb(t), []);
54+
return getDeclarationSiteFix(context, expression, diagnostic.code, checker, trackChanges)
55+
|| getUseSiteFix(context, expression, diagnostic.code, checker, trackChanges);
56+
});
57+
},
58+
});
59+
60+
function getDeclarationSiteFix(context: CodeFixContext | CodeFixAllContext, expression: Expression, errorCode: number, checker: TypeChecker, trackChanges: ContextualTrackChangesFunction) {
61+
const { sourceFile } = context;
62+
const awaitableInitializer = findAwaitableInitializer(expression, sourceFile, checker);
63+
if (awaitableInitializer) {
64+
const initializerChanges = trackChanges(t => makeChange(t, errorCode, sourceFile, checker, awaitableInitializer));
65+
return createCodeFixActionNoFixId(
66+
"addMissingAwaitToInitializer",
67+
initializerChanges,
68+
[Diagnostics.Add_await_to_initializer_for_0, expression.getText(sourceFile)]);
69+
}
70+
}
71+
72+
function getUseSiteFix(context: CodeFixContext | CodeFixAllContext, expression: Expression, errorCode: number, checker: TypeChecker, trackChanges: ContextualTrackChangesFunction) {
73+
const changes = trackChanges(t => makeChange(t, errorCode, context.sourceFile, checker, expression));
74+
return createCodeFixAction(fixId, changes, Diagnostics.Add_await, fixId, Diagnostics.Fix_all_expressions_possibly_missing_await);
75+
}
76+
77+
function isMissingAwaitError(sourceFile: SourceFile, errorCode: number, span: TextSpan, cancellationToken: CancellationToken, program: Program) {
78+
const checker = program.getDiagnosticsProducingTypeChecker();
79+
const diagnostics = checker.getDiagnostics(sourceFile, cancellationToken);
80+
return some(diagnostics, ({ start, length, relatedInformation, code }) =>
81+
isNumber(start) && isNumber(length) && textSpansEqual({ start, length }, span) &&
82+
code === errorCode &&
83+
!!relatedInformation &&
84+
some(relatedInformation, related => related.code === Diagnostics.Did_you_forget_to_use_await.code));
85+
}
86+
87+
function getAwaitableExpression(sourceFile: SourceFile, errorCode: number, span: TextSpan, cancellationToken: CancellationToken, program: Program): Expression | undefined {
88+
const token = getTokenAtPosition(sourceFile, span.start);
89+
// Checker has already done work to determine that await might be possible, and has attached
90+
// related info to the node, so start by finding the expression that exactly matches up
91+
// with the diagnostic range.
92+
const expression = findAncestor(token, node => {
93+
if (node.getStart(sourceFile) < span.start || node.getEnd() > textSpanEnd(span)) {
94+
return "quit";
95+
}
96+
return isExpression(node) && textSpansEqual(span, createTextSpanFromNode(node, sourceFile));
97+
}) as Expression | undefined;
98+
99+
return expression
100+
&& isMissingAwaitError(sourceFile, errorCode, span, cancellationToken, program)
101+
&& isInsideAwaitableBody(expression)
102+
? expression
103+
: undefined;
104+
}
105+
106+
function findAwaitableInitializer(expression: Node, sourceFile: SourceFile, checker: TypeChecker): Expression | undefined {
107+
if (!isIdentifier(expression)) {
108+
return;
109+
}
110+
111+
const symbol = checker.getSymbolAtLocation(expression);
112+
if (!symbol) {
113+
return;
114+
}
115+
116+
const declaration = tryCast(symbol.valueDeclaration, isVariableDeclaration);
117+
const variableName = tryCast(declaration && declaration.name, isIdentifier);
118+
const variableStatement = getAncestor(declaration, SyntaxKind.VariableStatement);
119+
if (!declaration || !variableStatement ||
120+
declaration.type ||
121+
!declaration.initializer ||
122+
variableStatement.getSourceFile() !== sourceFile ||
123+
hasModifier(variableStatement, ModifierFlags.Export) ||
124+
!variableName ||
125+
!isInsideAwaitableBody(declaration.initializer)) {
126+
return;
127+
}
128+
129+
const isUsedElsewhere = FindAllReferences.Core.eachSymbolReferenceInFile(variableName, checker, sourceFile, identifier => {
130+
return identifier !== expression;
131+
});
132+
133+
if (isUsedElsewhere) {
134+
return;
135+
}
136+
137+
return declaration.initializer;
138+
}
139+
140+
function isInsideAwaitableBody(node: Node) {
141+
return node.kind & NodeFlags.AwaitContext || !!findAncestor(node, ancestor =>
142+
ancestor.parent && isArrowFunction(ancestor.parent) && ancestor.parent.body === ancestor ||
143+
isBlock(ancestor) && (
144+
ancestor.parent.kind === SyntaxKind.FunctionDeclaration ||
145+
ancestor.parent.kind === SyntaxKind.FunctionExpression ||
146+
ancestor.parent.kind === SyntaxKind.ArrowFunction ||
147+
ancestor.parent.kind === SyntaxKind.MethodDeclaration));
148+
}
149+
150+
function makeChange(changeTracker: textChanges.ChangeTracker, errorCode: number, sourceFile: SourceFile, checker: TypeChecker, insertionSite: Expression) {
151+
if (isBinaryExpression(insertionSite)) {
152+
const { left, right } = insertionSite;
153+
const leftType = checker.getTypeAtLocation(left);
154+
const rightType = checker.getTypeAtLocation(right);
155+
const newLeft = checker.getPromisedTypeOfPromise(leftType) ? createAwait(left) : left;
156+
const newRight = checker.getPromisedTypeOfPromise(rightType) ? createAwait(right) : right;
157+
changeTracker.replaceNode(sourceFile, left, newLeft);
158+
changeTracker.replaceNode(sourceFile, right, newRight);
159+
}
160+
else if (errorCode === propertyAccessCode && isPropertyAccessExpression(insertionSite.parent)) {
161+
changeTracker.replaceNode(
162+
sourceFile,
163+
insertionSite.parent.expression,
164+
createParen(createAwait(insertionSite.parent.expression)));
165+
}
166+
else if (contains(callableConstructableErrorCodes, errorCode) && isCallOrNewExpression(insertionSite.parent)) {
167+
changeTracker.replaceNode(sourceFile, insertionSite, createParen(createAwait(insertionSite)));
168+
}
169+
else {
170+
changeTracker.replaceNode(sourceFile, insertionSite, createAwait(insertionSite));
171+
}
172+
}
173+
}

src/services/textChanges.ts

+4
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ namespace ts.textChanges {
704704
}
705705
}
706706

707+
public parenthesizeExpression(sourceFile: SourceFile, expression: Expression) {
708+
this.replaceRange(sourceFile, rangeOfNode(expression), createParen(expression));
709+
}
710+
707711
private finishClassesWithNodesInsertedAtStart(): void {
708712
this.classesWithNodesInsertedAtStart.forEach(({ node, sourceFile }) => {
709713
const [openBraceEnd, closeBraceEnd] = getClassOrObjectBraceEnds(node, sourceFile);

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"codeFixProvider.ts",
4646
"refactorProvider.ts",
4747
"codefixes/addConvertToUnknownForNonOverlappingTypes.ts",
48+
"codefixes/addMissingAwait.ts",
4849
"codefixes/addMissingConst.ts",
4950
"codefixes/addMissingInvocationForDecorator.ts",
5051
"codefixes/addNameToNamelessParameter.ts",

tests/baselines/reference/operationsAvailableOnPromisedType.errors.txt

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tests/cases/compiler/operationsAvailableOnPromisedType.ts(17,5): error TS2367: T
88
tests/cases/compiler/operationsAvailableOnPromisedType.ts(18,9): error TS2461: Type 'Promise<string[]>' is not an array type.
99
tests/cases/compiler/operationsAvailableOnPromisedType.ts(19,21): error TS2495: Type 'Promise<string[]>' is not an array type or a string type.
1010
tests/cases/compiler/operationsAvailableOnPromisedType.ts(20,12): error TS2345: Argument of type 'Promise<number>' is not assignable to parameter of type 'number'.
11-
tests/cases/compiler/operationsAvailableOnPromisedType.ts(21,11): error TS2570: Property 'prop' does not exist on type 'Promise<{ prop: string; }>'. Did you forget to use 'await'?
11+
tests/cases/compiler/operationsAvailableOnPromisedType.ts(21,11): error TS2339: Property 'prop' does not exist on type 'Promise<{ prop: string; }>'.
1212
tests/cases/compiler/operationsAvailableOnPromisedType.ts(23,27): error TS2495: Type 'Promise<string[]>' is not an array type or a string type.
1313
tests/cases/compiler/operationsAvailableOnPromisedType.ts(24,5): error TS2349: This expression is not callable.
1414
Type 'Promise<() => void>' has no call signatures.
@@ -72,7 +72,8 @@ tests/cases/compiler/operationsAvailableOnPromisedType.ts(27,5): error TS2349: T
7272
!!! related TS2773 tests/cases/compiler/operationsAvailableOnPromisedType.ts:20:12: Did you forget to use 'await'?
7373
d.prop;
7474
~~~~
75-
!!! error TS2570: Property 'prop' does not exist on type 'Promise<{ prop: string; }>'. Did you forget to use 'await'?
75+
!!! error TS2339: Property 'prop' does not exist on type 'Promise<{ prop: string; }>'.
76+
!!! related TS2773 tests/cases/compiler/operationsAvailableOnPromisedType.ts:21:11: Did you forget to use 'await'?
7677
}
7778
for await (const s of c) {}
7879
~
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path="fourslash.ts" />
2+
////async function fn(a: Promise<string>, b: string) {
3+
//// fn(a, a);
4+
////}
5+
6+
verify.codeFix({
7+
description: ts.Diagnostics.Add_await.message,
8+
index: 0,
9+
newFileContent:
10+
`async function fn(a: Promise<string>, b: string) {
11+
fn(a, await a);
12+
}`
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/// <reference path="fourslash.ts" />
2+
////async function fn(a: Promise<number>, b: number) {
3+
//// a | b;
4+
//// b + a;
5+
//// a + a;
6+
////}
7+
8+
verify.codeFix({
9+
description: ts.Diagnostics.Add_await.message,
10+
index: 0,
11+
newFileContent:
12+
`async function fn(a: Promise<number>, b: number) {
13+
await a | b;
14+
b + a;
15+
a + a;
16+
}`
17+
});
18+
19+
verify.codeFix({
20+
description: ts.Diagnostics.Add_await.message,
21+
index: 1,
22+
newFileContent:
23+
`async function fn(a: Promise<number>, b: number) {
24+
a | b;
25+
b + await a;
26+
a + a;
27+
}`
28+
});
29+
30+
verify.codeFix({
31+
description: ts.Diagnostics.Add_await.message,
32+
index: 2,
33+
newFileContent:
34+
`async function fn(a: Promise<number>, b: number) {
35+
a | b;
36+
b + a;
37+
await a + await a;
38+
}`
39+
});
40+
41+
verify.codeFixAll({
42+
fixAllDescription: ts.Diagnostics.Fix_all_expressions_possibly_missing_await.message,
43+
fixId: "addMissingAwait",
44+
newFileContent:
45+
`async function fn(a: Promise<number>, b: number) {
46+
await a | b;
47+
b + await a;
48+
await a + await a;
49+
}`
50+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path="fourslash.ts" />
2+
////async function fn(a: string, b: Promise<string>) {
3+
//// const x = b;
4+
//// fn(x, b);
5+
//// fn(b, b);
6+
////}
7+
8+
verify.codeFix({
9+
description: "Add 'await' to initializer for 'x'",
10+
index: 0,
11+
newFileContent:
12+
`async function fn(a: string, b: Promise<string>) {
13+
const x = await b;
14+
fn(x, b);
15+
fn(b, b);
16+
}`
17+
});
18+
19+
verify.codeFixAll({
20+
fixAllDescription: ts.Diagnostics.Fix_all_expressions_possibly_missing_await.message,
21+
fixId: "addMissingAwait",
22+
newFileContent:
23+
`async function fn(a: string, b: Promise<string>) {
24+
const x = await b;
25+
fn(x, b);
26+
fn(await b, b);
27+
}`
28+
});

0 commit comments

Comments
 (0)