Skip to content

Commit 50adabe

Browse files
authored
Improve diagnostics and add code fixes for top-level await (#36173)
1 parent afa11d3 commit 50adabe

File tree

42 files changed

+500
-104
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+500
-104
lines changed

src/compiler/checker.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -26665,13 +26665,18 @@ namespace ts {
2666526665
if (!(node.flags & NodeFlags.AwaitContext)) {
2666626666
if (isTopLevelAwait(node)) {
2666726667
const sourceFile = getSourceFileOfNode(node);
26668-
if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) ||
26669-
languageVersion < ScriptTarget.ES2017 ||
26670-
!isEffectiveExternalModule(sourceFile, compilerOptions)) {
26671-
if (!hasParseDiagnostics(sourceFile)) {
26672-
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
26668+
if (!hasParseDiagnostics(sourceFile)) {
26669+
let span: TextSpan | undefined;
26670+
if (!isEffectiveExternalModule(sourceFile, compilerOptions)) {
26671+
if (!span) span = getSpanOfTokenAtPosition(sourceFile, node.pos);
26672+
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length,
26673+
Diagnostics.await_expressions_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module);
26674+
diagnostics.add(diagnostic);
26675+
}
26676+
if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) || languageVersion < ScriptTarget.ES2017) {
26677+
span = getSpanOfTokenAtPosition(sourceFile, node.pos);
2667326678
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length,
26674-
Diagnostics.await_outside_of_an_async_function_is_only_allowed_at_the_top_level_of_a_module_when_module_is_esnext_or_system_and_target_is_es2017_or_higher);
26679+
Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_esnext_or_system_and_the_target_option_is_set_to_es2017_or_higher);
2667526680
diagnostics.add(diagnostic);
2667626681
}
2667726682
}
@@ -26681,7 +26686,7 @@ namespace ts {
2668126686
const sourceFile = getSourceFileOfNode(node);
2668226687
if (!hasParseDiagnostics(sourceFile)) {
2668326688
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
26684-
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expression_is_only_allowed_within_an_async_function);
26689+
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules);
2668526690
const func = getContainingFunction(node);
2668626691
if (func && func.kind !== SyntaxKind.Constructor && (getFunctionFlags(func) & FunctionFlags.Async) === 0) {
2668726692
const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async);

src/compiler/core.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,11 @@ namespace ts {
12691269
return result;
12701270
}
12711271

1272+
/**
1273+
* Creates a new object by adding the own properties of `second`, then the own properties of `first`.
1274+
*
1275+
* NOTE: This means that if a property exists in both `first` and `second`, the property in `first` will be chosen.
1276+
*/
12721277
export function extend<T1, T2>(first: T1, second: T2): T1 & T2 {
12731278
const result: T1 & T2 = <any>{};
12741279
for (const id in second) {

src/compiler/diagnosticMessages.json

+19-3
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@
863863
"category": "Error",
864864
"code": 1300
865865
},
866-
"'await' expression is only allowed within an async function.": {
866+
"'await' expressions are only allowed within async functions and at the top levels of modules.": {
867867
"category": "Error",
868868
"code": 1308
869869
},
@@ -1085,7 +1085,7 @@
10851085
},
10861086
"Split all invalid type-only imports": {
10871087
"category": "Message",
1088-
"code": 1377
1088+
"code": 1367
10891089
},
10901090
"Specify emit/checking behavior for imports that are only used for types": {
10911091
"category": "Message",
@@ -1115,10 +1115,14 @@
11151115
"category": "Message",
11161116
"code": 1374
11171117
},
1118-
"'await' outside of an async function is only allowed at the top level of a module when '--module' is 'esnext' or 'system' and '--target' is 'es2017' or higher.": {
1118+
"'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.": {
11191119
"category": "Error",
11201120
"code": 1375
11211121
},
1122+
"Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.": {
1123+
"category": "Error",
1124+
"code": 1376
1125+
},
11221126
"The types of '{0}' are incompatible between these types.": {
11231127
"category": "Error",
11241128
"code": 2200
@@ -5416,6 +5420,18 @@
54165420
"category": "Message",
54175421
"code": 95096
54185422
},
5423+
"Add 'export {}' to make this file into a module": {
5424+
"category": "Message",
5425+
"code": 95097
5426+
},
5427+
"Set the 'target' option in your configuration file to '{0}'": {
5428+
"category": "Message",
5429+
"code": 95098
5430+
},
5431+
"Set the 'module' option in your configuration file to '{0}'": {
5432+
"category": "Message",
5433+
"code": 95099
5434+
},
54195435

54205436
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
54215437
"category": "Error",

src/harness/fourslashImpl.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ namespace FourSlash {
254254
const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);
255255

256256
if (!tsConfig.errors || !tsConfig.errors.length) {
257-
compilationOptions = ts.extend(compilationOptions, tsConfig.options);
257+
compilationOptions = ts.extend(tsConfig.options, compilationOptions);
258258
}
259259
}
260260
configFileName = file.fileName;
@@ -2574,6 +2574,10 @@ namespace FourSlash {
25742574
if (typeof options.description === "string") {
25752575
assert.equal(action.description, options.description);
25762576
}
2577+
else if (Array.isArray(options.description)) {
2578+
const description = ts.formatStringFromArgs(options.description[0], options.description, 1);
2579+
assert.equal(action.description, description);
2580+
}
25772581
else {
25782582
assert.match(action.description, templateToRegExp(options.description.template));
25792583
}

src/harness/fourslashInterfaceImpl.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,7 @@ namespace FourSlashInterface {
15561556
}
15571557

15581558
export interface VerifyCodeFixOptions extends NewContentOptions {
1559-
readonly description: string | DiagnosticIgnoredInterpolations;
1559+
readonly description: string | [string, ...(string | number)[]] | DiagnosticIgnoredInterpolations;
15601560
readonly errorCode?: number;
15611561
readonly index?: number;
15621562
readonly preferences?: ts.UserPreferences;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
registerCodeFix({
4+
errorCodes: [Diagnostics.await_expressions_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module.code],
5+
getCodeActions: context => {
6+
const { sourceFile } = context;
7+
const changes = textChanges.ChangeTracker.with(context, changes => {
8+
const exportDeclaration = createExportDeclaration(
9+
/*decorators*/ undefined,
10+
/*modifiers*/ undefined,
11+
createNamedExports([]),
12+
/*moduleSpecifier*/ undefined,
13+
/*isTypeOnly*/ false
14+
);
15+
changes.insertNodeAtEndOfScope(sourceFile, sourceFile, exportDeclaration);
16+
});
17+
return [createCodeFixActionWithoutFixAll("addEmptyExportDeclaration", changes, Diagnostics.Add_export_to_make_this_file_into_a_module)];
18+
},
19+
});
20+
}

src/services/codefixes/fixAwaitInSyncFunction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
namespace ts.codefix {
33
const fixId = "fixAwaitInSyncFunction";
44
const errorCodes = [
5-
Diagnostics.await_expression_is_only_allowed_within_an_async_function.code,
5+
Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules.code,
66
Diagnostics.A_for_await_of_statement_is_only_allowed_within_an_async_function_or_async_generator.code,
77
];
88
registerCodeFix({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
registerCodeFix({
4+
errorCodes: [Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_esnext_or_system_and_the_target_option_is_set_to_es2017_or_higher.code],
5+
getCodeActions: context => {
6+
const compilerOptions = context.program.getCompilerOptions();
7+
const { configFile } = compilerOptions;
8+
if (configFile === undefined) {
9+
return undefined;
10+
}
11+
12+
const codeFixes: CodeFixAction[] = [];
13+
const moduleKind = getEmitModuleKind(compilerOptions);
14+
const moduleOutOfRange = moduleKind >= ModuleKind.ES2015 && moduleKind < ModuleKind.ESNext;
15+
if (moduleOutOfRange) {
16+
const changes = textChanges.ChangeTracker.with(context, changes => {
17+
setJsonCompilerOptionValue(changes, configFile, "module", createStringLiteral("esnext"));
18+
});
19+
codeFixes.push(createCodeFixActionWithoutFixAll("fixModuleOption", changes, [Diagnostics.Set_the_module_option_in_your_configuration_file_to_0, "esnext"]));
20+
}
21+
22+
const target = getEmitScriptTarget(compilerOptions);
23+
const targetOutOfRange = target < ScriptTarget.ES2017 || target > ScriptTarget.ESNext;
24+
if (targetOutOfRange) {
25+
const changes = textChanges.ChangeTracker.with(context, tracker => {
26+
const configObject = getTsConfigObjectLiteralExpression(configFile);
27+
if (!configObject) return;
28+
29+
const options: [string, Expression][] = [["target", createStringLiteral("es2017")]];
30+
if (moduleKind === ModuleKind.CommonJS) {
31+
// Ensure we preserve the default module kind (commonjs), as targets >= ES2015 have a default module kind of es2015.
32+
options.push(["module", createStringLiteral("commonjs")]);
33+
}
34+
35+
setJsonCompilerOptionValues(tracker, configFile, options);
36+
});
37+
38+
codeFixes.push(createCodeFixActionWithoutFixAll("fixTargetOption", changes, [Diagnostics.Set_the_target_option_in_your_configuration_file_to_0, "es2017"]));
39+
}
40+
41+
return codeFixes.length ? codeFixes : undefined;
42+
}
43+
});
44+
}

src/services/codefixes/helpers.ts

+20-13
Original file line numberDiff line numberDiff line change
@@ -310,11 +310,10 @@ namespace ts.codefix {
310310
return undefined;
311311
}
312312

313-
export function setJsonCompilerOptionValue(
313+
export function setJsonCompilerOptionValues(
314314
changeTracker: textChanges.ChangeTracker,
315315
configFile: TsConfigSourceFile,
316-
optionName: string,
317-
optionValue: Expression,
316+
options: [string, Expression][]
318317
) {
319318
const tsconfigObjectLiteral = getTsConfigObjectLiteralExpression(configFile);
320319
if (!tsconfigObjectLiteral) return undefined;
@@ -323,9 +322,7 @@ namespace ts.codefix {
323322
if (compilerOptionsProperty === undefined) {
324323
changeTracker.insertNodeAtObjectStart(configFile, tsconfigObjectLiteral, createJsonPropertyAssignment(
325324
"compilerOptions",
326-
createObjectLiteral([
327-
createJsonPropertyAssignment(optionName, optionValue),
328-
])));
325+
createObjectLiteral(options.map(([optionName, optionValue]) => createJsonPropertyAssignment(optionName, optionValue)), /*multiLine*/ true)));
329326
return;
330327
}
331328

@@ -334,16 +331,26 @@ namespace ts.codefix {
334331
return;
335332
}
336333

337-
const optionProperty = findJsonProperty(compilerOptions, optionName);
338-
339-
if (optionProperty === undefined) {
340-
changeTracker.insertNodeAtObjectStart(configFile, compilerOptions, createJsonPropertyAssignment(optionName, optionValue));
341-
}
342-
else {
343-
changeTracker.replaceNode(configFile, optionProperty.initializer, optionValue);
334+
for (const [optionName, optionValue] of options) {
335+
const optionProperty = findJsonProperty(compilerOptions, optionName);
336+
if (optionProperty === undefined) {
337+
changeTracker.insertNodeAtObjectStart(configFile, compilerOptions, createJsonPropertyAssignment(optionName, optionValue));
338+
}
339+
else {
340+
changeTracker.replaceNode(configFile, optionProperty.initializer, optionValue);
341+
}
344342
}
345343
}
346344

345+
export function setJsonCompilerOptionValue(
346+
changeTracker: textChanges.ChangeTracker,
347+
configFile: TsConfigSourceFile,
348+
optionName: string,
349+
optionValue: Expression,
350+
) {
351+
setJsonCompilerOptionValues(changeTracker, configFile, [[optionName, optionValue]]);
352+
}
353+
347354
export function createJsonPropertyAssignment(name: string, initializer: Expression) {
348355
return createPropertyAssignment(createStringLiteral(name), initializer);
349356
}

0 commit comments

Comments
 (0)