Skip to content

Improve diagnostics and add code fixes for top-level await #36173

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 3 commits into from
Jan 18, 2020
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
19 changes: 12 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26665,13 +26665,18 @@ namespace ts {
if (!(node.flags & NodeFlags.AwaitContext)) {
if (isTopLevelAwait(node)) {
const sourceFile = getSourceFileOfNode(node);
if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) ||
languageVersion < ScriptTarget.ES2017 ||
!isEffectiveExternalModule(sourceFile, compilerOptions)) {
if (!hasParseDiagnostics(sourceFile)) {
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
if (!hasParseDiagnostics(sourceFile)) {
let span: TextSpan | undefined;
if (!isEffectiveExternalModule(sourceFile, compilerOptions)) {
if (!span) span = getSpanOfTokenAtPosition(sourceFile, node.pos);
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length,
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);
diagnostics.add(diagnostic);
}
if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) || languageVersion < ScriptTarget.ES2017) {
span = getSpanOfTokenAtPosition(sourceFile, node.pos);
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length,
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);
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);
diagnostics.add(diagnostic);
}
}
Expand All @@ -26681,7 +26686,7 @@ namespace ts {
const sourceFile = getSourceFileOfNode(node);
if (!hasParseDiagnostics(sourceFile)) {
const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expression_is_only_allowed_within_an_async_function);
const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules);
const func = getContainingFunction(node);
if (func && func.kind !== SyntaxKind.Constructor && (getFunctionFlags(func) & FunctionFlags.Async) === 0) {
const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async);
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,11 @@ namespace ts {
return result;
}

/**
* Creates a new object by adding the own properties of `second`, then the own properties of `first`.
*
* NOTE: This means that if a property exists in both `first` and `second`, the property in `first` will be chosen.
*/
export function extend<T1, T2>(first: T1, second: T2): T1 & T2 {
const result: T1 & T2 = <any>{};
for (const id in second) {
Expand Down
22 changes: 19 additions & 3 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@
"category": "Error",
"code": 1300
},
"'await' expression is only allowed within an async function.": {
"'await' expressions are only allowed within async functions and at the top levels of modules.": {
"category": "Error",
"code": 1308
},
Expand Down Expand Up @@ -1085,7 +1085,7 @@
},
"Split all invalid type-only imports": {
"category": "Message",
"code": 1377
"code": 1367
},
"Specify emit/checking behavior for imports that are only used for types": {
"category": "Message",
Expand Down Expand Up @@ -1115,10 +1115,14 @@
"category": "Message",
"code": 1374
},
"'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.": {
"'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.": {
"category": "Error",
"code": 1375
},
"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.": {
"category": "Error",
"code": 1376
},
"The types of '{0}' are incompatible between these types.": {
"category": "Error",
"code": 2200
Expand Down Expand Up @@ -5416,6 +5420,18 @@
"category": "Message",
"code": 95096
},
"Add 'export {}' to make this file into a module": {
"category": "Message",
"code": 95097
},
"Set the 'target' option in your configuration file to '{0}'": {
"category": "Message",
"code": 95098
},
"Set the 'module' option in your configuration file to '{0}'": {
"category": "Message",
"code": 95099
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
6 changes: 5 additions & 1 deletion src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ namespace FourSlash {
const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);

if (!tsConfig.errors || !tsConfig.errors.length) {
compilationOptions = ts.extend(compilationOptions, tsConfig.options);
compilationOptions = ts.extend(tsConfig.options, compilationOptions);
}
}
configFileName = file.fileName;
Expand Down Expand Up @@ -2574,6 +2574,10 @@ namespace FourSlash {
if (typeof options.description === "string") {
assert.equal(action.description, options.description);
}
else if (Array.isArray(options.description)) {
const description = ts.formatStringFromArgs(options.description[0], options.description, 1);
assert.equal(action.description, description);
}
else {
assert.match(action.description, templateToRegExp(options.description.template));
}
Expand Down
2 changes: 1 addition & 1 deletion src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1556,7 +1556,7 @@ namespace FourSlashInterface {
}

export interface VerifyCodeFixOptions extends NewContentOptions {
readonly description: string | DiagnosticIgnoredInterpolations;
readonly description: string | [string, ...(string | number)[]] | DiagnosticIgnoredInterpolations;
readonly errorCode?: number;
readonly index?: number;
readonly preferences?: ts.UserPreferences;
Expand Down
20 changes: 20 additions & 0 deletions src/services/codefixes/addEmptyExportDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* @internal */
namespace ts.codefix {
registerCodeFix({
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],
getCodeActions: context => {
const { sourceFile } = context;
const changes = textChanges.ChangeTracker.with(context, changes => {
const exportDeclaration = createExportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createNamedExports([]),
/*moduleSpecifier*/ undefined,
/*isTypeOnly*/ false
);
changes.insertNodeAtEndOfScope(sourceFile, sourceFile, exportDeclaration);
});
return [createCodeFixActionWithoutFixAll("addEmptyExportDeclaration", changes, Diagnostics.Add_export_to_make_this_file_into_a_module)];
},
});
}
2 changes: 1 addition & 1 deletion src/services/codefixes/fixAwaitInSyncFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
namespace ts.codefix {
const fixId = "fixAwaitInSyncFunction";
const errorCodes = [
Diagnostics.await_expression_is_only_allowed_within_an_async_function.code,
Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules.code,
Diagnostics.A_for_await_of_statement_is_only_allowed_within_an_async_function_or_async_generator.code,
];
registerCodeFix({
Expand Down
44 changes: 44 additions & 0 deletions src/services/codefixes/fixModuleAndTargetOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* @internal */
namespace ts.codefix {
registerCodeFix({
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],
getCodeActions: context => {
const compilerOptions = context.program.getCompilerOptions();
const { configFile } = compilerOptions;
if (configFile === undefined) {
return undefined;
}

const codeFixes: CodeFixAction[] = [];
const moduleKind = getEmitModuleKind(compilerOptions);
const moduleOutOfRange = moduleKind >= ModuleKind.ES2015 && moduleKind < ModuleKind.ESNext;
if (moduleOutOfRange) {
const changes = textChanges.ChangeTracker.with(context, changes => {
setJsonCompilerOptionValue(changes, configFile, "module", createStringLiteral("esnext"));
});
codeFixes.push(createCodeFixActionWithoutFixAll("fixModuleOption", changes, [Diagnostics.Set_the_module_option_in_your_configuration_file_to_0, "esnext"]));
}

const target = getEmitScriptTarget(compilerOptions);
const targetOutOfRange = target < ScriptTarget.ES2017 || target > ScriptTarget.ESNext;
if (targetOutOfRange) {
const changes = textChanges.ChangeTracker.with(context, tracker => {
const configObject = getTsConfigObjectLiteralExpression(configFile);
if (!configObject) return;

const options: [string, Expression][] = [["target", createStringLiteral("es2017")]];
if (moduleKind === ModuleKind.CommonJS) {
// Ensure we preserve the default module kind (commonjs), as targets >= ES2015 have a default module kind of es2015.
options.push(["module", createStringLiteral("commonjs")]);
}

setJsonCompilerOptionValues(tracker, configFile, options);
});

codeFixes.push(createCodeFixActionWithoutFixAll("fixTargetOption", changes, [Diagnostics.Set_the_target_option_in_your_configuration_file_to_0, "es2017"]));
}

return codeFixes.length ? codeFixes : undefined;
}
});
}
33 changes: 20 additions & 13 deletions src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,10 @@ namespace ts.codefix {
return undefined;
}

export function setJsonCompilerOptionValue(
export function setJsonCompilerOptionValues(
changeTracker: textChanges.ChangeTracker,
configFile: TsConfigSourceFile,
optionName: string,
optionValue: Expression,
options: [string, Expression][]
) {
const tsconfigObjectLiteral = getTsConfigObjectLiteralExpression(configFile);
if (!tsconfigObjectLiteral) return undefined;
Expand All @@ -323,9 +322,7 @@ namespace ts.codefix {
if (compilerOptionsProperty === undefined) {
changeTracker.insertNodeAtObjectStart(configFile, tsconfigObjectLiteral, createJsonPropertyAssignment(
"compilerOptions",
createObjectLiteral([
createJsonPropertyAssignment(optionName, optionValue),
])));
createObjectLiteral(options.map(([optionName, optionValue]) => createJsonPropertyAssignment(optionName, optionValue)), /*multiLine*/ true)));
return;
}

Expand All @@ -334,16 +331,26 @@ namespace ts.codefix {
return;
}

const optionProperty = findJsonProperty(compilerOptions, optionName);

if (optionProperty === undefined) {
changeTracker.insertNodeAtObjectStart(configFile, compilerOptions, createJsonPropertyAssignment(optionName, optionValue));
}
else {
changeTracker.replaceNode(configFile, optionProperty.initializer, optionValue);
for (const [optionName, optionValue] of options) {
const optionProperty = findJsonProperty(compilerOptions, optionName);
if (optionProperty === undefined) {
changeTracker.insertNodeAtObjectStart(configFile, compilerOptions, createJsonPropertyAssignment(optionName, optionValue));
}
else {
changeTracker.replaceNode(configFile, optionProperty.initializer, optionValue);
}
}
}

export function setJsonCompilerOptionValue(
changeTracker: textChanges.ChangeTracker,
configFile: TsConfigSourceFile,
optionName: string,
optionValue: Expression,
) {
setJsonCompilerOptionValues(changeTracker, configFile, [[optionName, optionValue]]);
}

export function createJsonPropertyAssignment(name: string, initializer: Expression) {
return createPropertyAssignment(createStringLiteral(name), initializer);
}
Expand Down
Loading