Add quick fix to add 'void' to Promise resolved without value (#40558)

* Add codefix to add 'void' to Promise resolved without value

* Add specific error message in checker to reduce quick-fix time in editor
This commit is contained in:
Ron Buckton 2020-09-14 19:12:33 -07:00 committed by GitHub
parent 7db91182f7
commit dba042d7d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 322 additions and 18 deletions

View File

@ -26708,6 +26708,22 @@ namespace ts {
}
}
function isPromiseResolveArityError(node: CallLikeExpression) {
if (!isCallExpression(node) || !isIdentifier(node.expression)) return false;
const symbol = resolveName(node.expression, node.expression.escapedText, SymbolFlags.Value, undefined, undefined, false);
const decl = symbol?.valueDeclaration;
if (!decl || !isParameter(decl) || !isFunctionExpressionOrArrowFunction(decl.parent) || !isNewExpression(decl.parent.parent) || !isIdentifier(decl.parent.parent.expression)) {
return false;
}
const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false);
if (!globalPromiseSymbol) return false;
const constructorSymbol = getSymbolAtLocation(decl.parent.parent.expression, /*ignoreErrors*/ true);
return constructorSymbol === globalPromiseSymbol;
}
function getArgumentArityError(node: CallLikeExpression, signatures: readonly Signature[], args: readonly Expression[]) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
@ -26740,9 +26756,15 @@ namespace ts {
let spanArray: NodeArray<Node>;
let related: DiagnosticWithLocation | undefined;
const error = hasRestParameter || hasSpreadArgument ? hasRestParameter && hasSpreadArgument ? Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more :
hasRestParameter ? Diagnostics.Expected_at_least_0_arguments_but_got_1 :
Diagnostics.Expected_0_arguments_but_got_1_or_more : Diagnostics.Expected_0_arguments_but_got_1;
const error = hasRestParameter || hasSpreadArgument ?
hasRestParameter && hasSpreadArgument ?
Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more :
hasRestParameter ?
Diagnostics.Expected_at_least_0_arguments_but_got_1 :
Diagnostics.Expected_0_arguments_but_got_1_or_more :
paramRange === 1 && argCount === 0 && isPromiseResolveArityError(node) ?
Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise :
Diagnostics.Expected_0_arguments_but_got_1;
if (closestSignature && getMinArgumentCount(closestSignature) > argCount && closestSignature.declaration) {
const paramDecl = closestSignature.declaration.parameters[closestSignature.thisParameter ? argCount + 1 : argCount];

View File

@ -3039,6 +3039,10 @@
"category": "Error",
"code": 2793
},
"Expected {0} arguments, but got {1}. Did you forget to include 'void' in your type argument to 'Promise'?": {
"category": "Error",
"code": 2794
},
"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
@ -5927,6 +5931,14 @@
"category": "Message",
"code": 95142
},
"Add 'void' to Promise resolved without a value": {
"category": "Message",
"code": 95143
},
"Add 'void' to all Promises resolved without a value": {
"category": "Message",
"code": 95144
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",

View File

@ -339,17 +339,23 @@ namespace FourSlash {
this.languageServiceAdapterHost.addScript(fileName, file, /*isRootFile*/ true);
}
});
if (!compilationOptions.noLib) {
this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName,
Harness.Compiler.getDefaultLibrarySourceFile()!.text, /*isRootFile*/ false);
compilationOptions.lib?.forEach(fileName => {
if (!compilationOptions.noLib) {
const seen = new Set<string>();
const addSourceFile = (fileName: string) => {
if (seen.has(fileName)) return;
seen.add(fileName);
const libFile = Harness.Compiler.getDefaultLibrarySourceFile(fileName);
ts.Debug.assertIsDefined(libFile, `Could not find lib file '${fileName}'`);
if (libFile) {
this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
if (!ts.some(libFile.libReferenceDirectives)) return;
for (const directive of libFile.libReferenceDirectives) {
addSourceFile(`lib.${directive.fileName}.d.ts`);
}
});
};
addSourceFile(Harness.Compiler.defaultLibFileName);
compilationOptions.lib?.forEach(addSourceFile);
}
}
@ -3878,7 +3884,7 @@ namespace FourSlash {
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
const state = new TestState(absoluteFileName, absoluteBasePath, testType, testData);
const actualFileName = Harness.IO.resolvePath(fileName) || absoluteFileName;
const output = ts.transpileModule(content, { reportDiagnostics: true, fileName: actualFileName, compilerOptions: { target: ts.ScriptTarget.ES2015, inlineSourceMap: true } });
const output = ts.transpileModule(content, { reportDiagnostics: true, fileName: actualFileName, compilerOptions: { target: ts.ScriptTarget.ES2015, inlineSourceMap: true, inlineSources: true } });
if (output.diagnostics!.length > 0) {
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`);
}
@ -3888,7 +3894,7 @@ namespace FourSlash {
function runCode(code: string, state: TestState, fileName: string): void {
// Compile and execute the test
const generatedFile = ts.changeExtension(fileName, ".js");
const wrappedCode = `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, verifyOperationIsCancelled) {${code}\n//# sourceURL=${generatedFile}\n})`;
const wrappedCode = `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, verifyOperationIsCancelled) {${code}\n//# sourceURL=${ts.getBaseFileName(generatedFile)}\n})`;
type SourceMapSupportModule = typeof import("source-map-support") & {
// TODO(rbuckton): This is missing from the DT definitions and needs to be added.

View File

@ -0,0 +1,82 @@
/* @internal */
namespace ts.codefix {
const fixName = "addVoidToPromise";
const fixId = "addVoidToPromise";
const errorCodes = [
Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise.code
];
registerCodeFix({
errorCodes,
fixIds: [fixId],
getCodeActions(context) {
const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span, context.program));
if (changes.length > 0) {
return [createCodeFixAction(fixName, changes, Diagnostics.Add_void_to_Promise_resolved_without_a_value, fixId, Diagnostics.Add_void_to_all_Promises_resolved_without_a_value)];
}
},
getAllCodeActions(context: CodeFixAllContext) {
return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag, context.program, new Set()));
}
});
function makeChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan, program: Program, seen?: Set<ParameterDeclaration>) {
const node = getTokenAtPosition(sourceFile, span.start);
if (!isIdentifier(node) || !isCallExpression(node.parent) || node.parent.expression !== node || node.parent.arguments.length !== 0) return;
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(node);
// decl should be `new Promise((<decl>) => {})`
const decl = symbol?.valueDeclaration;
if (!decl || !isParameter(decl) || !isNewExpression(decl.parent.parent)) return;
// no need to make this change if we have already seen this parameter.
if (seen?.has(decl)) return;
seen?.add(decl);
const typeArguments = getEffectiveTypeArguments(decl.parent.parent);
if (some(typeArguments)) {
// append ` | void` to type argument
const typeArgument = typeArguments[0];
const needsParens = !isUnionTypeNode(typeArgument) && !isParenthesizedTypeNode(typeArgument) &&
isParenthesizedTypeNode(factory.createUnionTypeNode([typeArgument, factory.createKeywordTypeNode(SyntaxKind.VoidKeyword)]).types[0]);
if (needsParens) {
changes.insertText(sourceFile, typeArgument.pos, "(");
}
changes.insertText(sourceFile, typeArgument.end, needsParens ? ") | void" : " | void");
}
else {
// make sure the Promise is type is untyped (i.e., `unknown`)
const signature = checker.getResolvedSignature(node.parent);
const parameter = signature?.parameters[0];
const parameterType = parameter && checker.getTypeOfSymbolAtLocation(parameter, decl.parent.parent);
if (isInJSFile(decl)) {
if (!parameterType || parameterType.flags & TypeFlags.AnyOrUnknown) {
// give the expression a type
changes.insertText(sourceFile, decl.parent.parent.end, `)`);
changes.insertText(sourceFile, skipTrivia(sourceFile.text, decl.parent.parent.pos), `/** @type {Promise<void>} */(`);
}
}
else {
if (!parameterType || parameterType.flags & TypeFlags.Unknown) {
// add `void` type argument
changes.insertText(sourceFile, decl.parent.parent.expression.end, "<void>");
}
}
}
}
function getEffectiveTypeArguments(node: NewExpression) {
if (isInJSFile(node)) {
if (isParenthesizedExpression(node.parent)) {
const jsDocType = getJSDocTypeTag(node.parent)?.typeExpression.type;
if (jsDocType && isTypeReferenceNode(jsDocType) && isIdentifier(jsDocType.typeName) && idText(jsDocType.typeName) === "Promise") {
return jsDocType.typeArguments;
}
}
}
else {
return node.typeArguments;
}
}
}

View File

@ -107,6 +107,7 @@
"codefixes/splitTypeOnlyImport.ts",
"codefixes/convertConstToLet.ts",
"codefixes/fixExpectedComma.ts",
"codefixes/fixAddVoidToPromise.ts",
"refactors/convertExport.ts",
"refactors/convertImport.ts",
"refactors/convertToOptionalChainExpression.ts",

View File

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p1 = new Promise(resolve => resolve());
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 0,
newFileContent: `const p1 = new Promise<void>(resolve => resolve());`
});

View File

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p2 = new Promise<number>(resolve => resolve());
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 0,
newFileContent: `const p2 = new Promise<number | void>(resolve => resolve());`
});

View File

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p3 = new Promise<number | string>(resolve => resolve());
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 0,
newFileContent: `const p3 = new Promise<number | string | void>(resolve => resolve());`
});

View File

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p4 = new Promise<{ x: number } & { y: string }>(resolve => resolve());
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 0,
newFileContent: `const p4 = new Promise<({ x: number } & { y: string }) | void>(resolve => resolve());`
});

View File

@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p4: Promise<number> = new Promise(resolve => resolve());
verify.not.codeFixAvailable();

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
////const p1 = new Promise(resolve => resolve());
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 2,
newFileContent: `const p1 = /** @type {Promise<void>} */(new Promise(resolve => resolve()));`
});

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
////const p2 = /** @type {Promise<number>} */(new Promise(resolve => resolve()));
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 2,
newFileContent: `const p2 = /** @type {Promise<number | void>} */(new Promise(resolve => resolve()));`
});

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
////const p3 = /** @type {Promise<number | string>} */(new Promise(resolve => resolve()));
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 2,
newFileContent: `const p3 = /** @type {Promise<number | string | void>} */(new Promise(resolve => resolve()));`
});

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
////const p4 = /** @type {Promise<{ x: number } & { y: string }>} */(new Promise(resolve => resolve()));
verify.codeFix({
errorCode: 2794,
description: "Add 'void' to Promise resolved without a value",
index: 2,
newFileContent: `const p4 = /** @type {Promise<({ x: number } & { y: string }) | void>} */(new Promise(resolve => resolve()));`
});

View File

@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
/////** @type {Promise<number>} */
////const p2 = new Promise(resolve => resolve());
verify.not.codeFixAvailable("Add 'void' to Promise resolved without a value");

View File

@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
// @allowJS: true
// @checkJS: true
// @filename: main.js
////const p1 = new Promise(resolve => resolve());
////const p2 = /** @type {Promise<number>} */(new Promise(resolve => resolve()));
////const p3 = /** @type {Promise<number | string>} */(new Promise(resolve => resolve()));
////const p4 = /** @type {Promise<{ x: number } & { y: string }>} */(new Promise(resolve => resolve()));
verify.codeFixAll({
fixId: "addVoidToPromise",
fixAllDescription: ts.Diagnostics.Add_void_to_all_Promises_resolved_without_a_value.message,
newFileContent: `const p1 = /** @type {Promise<void>} */(new Promise(resolve => resolve()));
const p2 = /** @type {Promise<number | void>} */(new Promise(resolve => resolve()));
const p3 = /** @type {Promise<number | string | void>} */(new Promise(resolve => resolve()));
const p4 = /** @type {Promise<({ x: number } & { y: string }) | void>} */(new Promise(resolve => resolve()));`
});

View File

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// @target: esnext
// @lib: es2015
// @strict: true
////const p1 = new Promise(resolve => resolve());
////const p2 = new Promise<number>(resolve => resolve());
////const p3 = new Promise<number | string>(resolve => resolve());
////const p4 = new Promise<{ x: number } & { y: string }>(resolve => resolve());
verify.codeFixAll({
fixId: "addVoidToPromise",
fixAllDescription: ts.Diagnostics.Add_void_to_all_Promises_resolved_without_a_value.message,
newFileContent: `const p1 = new Promise<void>(resolve => resolve());
const p2 = new Promise<number | void>(resolve => resolve());
const p3 = new Promise<number | string | void>(resolve => resolve());
const p4 = new Promise<({ x: number } & { y: string }) | void>(resolve => resolve());`
});

View File

@ -47,7 +47,7 @@ class C implements I<number> {
throw new Error("Method not implemented.");
}
[Symbol.match]: boolean;
[Symbol.replace](...args: {}) {
[Symbol.replace](...args: any[]) {
throw new Error("Method not implemented.");
}
[Symbol.search](str: string): number {
@ -56,7 +56,7 @@ class C implements I<number> {
[Symbol.species](): number {
throw new Error("Method not implemented.");
}
[Symbol.split](str: string, limit?: number): {} {
[Symbol.split](str: string, limit?: number): string[] {
throw new Error("Method not implemented.");
}
[Symbol.toPrimitive](hint: "number"): number;
@ -65,7 +65,7 @@ class C implements I<number> {
[Symbol.toPrimitive](hint: any) {
throw new Error("Method not implemented.");
}
[Symbol.toStringTag]: string\;
[Symbol.toStringTag]: string;
[Symbol.unscopables]: any;
}`,
});

View File

@ -11,7 +11,7 @@
//// })
//// /*1*/
verify.numberOfErrorsInCurrentFile(2);
verify.numberOfErrorsInCurrentFile(1);
goTo.marker("1");
edit.insert(" ");
verify.numberOfErrorsInCurrentFile(2);
verify.numberOfErrorsInCurrentFile(1);

View File

@ -11,4 +11,4 @@
verify.numberOfErrorsInCurrentFile(0);
goTo.marker("1");
edit.insert("(");
verify.numberOfErrorsInCurrentFile(3);
verify.numberOfErrorsInCurrentFile(1);