1import { 2 CodeFixAllContext, Diagnostics, factory, getJSDocTypeTag, getTokenAtPosition, idText, isCallExpression, 3 isIdentifier, isInJSFile, isNewExpression, isParameter, isParenthesizedExpression, isParenthesizedTypeNode, 4 isTypeReferenceNode, isUnionTypeNode, NewExpression, ParameterDeclaration, Program, Set, skipTrivia, some, 5 SourceFile, SyntaxKind, textChanges, TextSpan, TypeFlags, 6} from "../_namespaces/ts"; 7import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix"; 8 9const fixName = "addVoidToPromise"; 10const fixId = "addVoidToPromise"; 11const errorCodes = [ 12 Diagnostics.Expected_1_argument_but_got_0_new_Promise_needs_a_JSDoc_hint_to_produce_a_resolve_that_can_be_called_without_arguments.code, 13 Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise.code 14]; 15registerCodeFix({ 16 errorCodes, 17 fixIds: [fixId], 18 getCodeActions(context) { 19 const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span, context.program)); 20 if (changes.length > 0) { 21 return [createCodeFixAction(fixName, changes, Diagnostics.Add_void_to_Promise_resolved_without_a_value, fixId, Diagnostics.Add_void_to_all_Promises_resolved_without_a_value)]; 22 } 23 }, 24 getAllCodeActions(context: CodeFixAllContext) { 25 return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag, context.program, new Set())); 26 } 27}); 28 29function makeChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan, program: Program, seen?: Set<ParameterDeclaration>) { 30 const node = getTokenAtPosition(sourceFile, span.start); 31 if (!isIdentifier(node) || !isCallExpression(node.parent) || node.parent.expression !== node || node.parent.arguments.length !== 0) return; 32 33 const checker = program.getTypeChecker(); 34 const symbol = checker.getSymbolAtLocation(node); 35 36 // decl should be `new Promise((<decl>) => {})` 37 const decl = symbol?.valueDeclaration; 38 if (!decl || !isParameter(decl) || !isNewExpression(decl.parent.parent)) return; 39 40 // no need to make this change if we have already seen this parameter. 41 if (seen?.has(decl)) return; 42 seen?.add(decl); 43 44 const typeArguments = getEffectiveTypeArguments(decl.parent.parent); 45 if (some(typeArguments)) { 46 // append ` | void` to type argument 47 const typeArgument = typeArguments[0]; 48 const needsParens = !isUnionTypeNode(typeArgument) && !isParenthesizedTypeNode(typeArgument) && 49 isParenthesizedTypeNode(factory.createUnionTypeNode([typeArgument, factory.createKeywordTypeNode(SyntaxKind.VoidKeyword)]).types[0]); 50 if (needsParens) { 51 changes.insertText(sourceFile, typeArgument.pos, "("); 52 } 53 changes.insertText(sourceFile, typeArgument.end, needsParens ? ") | void" : " | void"); 54 } 55 else { 56 // make sure the Promise is type is untyped (i.e., `unknown`) 57 const signature = checker.getResolvedSignature(node.parent); 58 const parameter = signature?.parameters[0]; 59 const parameterType = parameter && checker.getTypeOfSymbolAtLocation(parameter, decl.parent.parent); 60 if (isInJSFile(decl)) { 61 if (!parameterType || parameterType.flags & TypeFlags.AnyOrUnknown) { 62 // give the expression a type 63 changes.insertText(sourceFile, decl.parent.parent.end, `)`); 64 changes.insertText(sourceFile, skipTrivia(sourceFile.text, decl.parent.parent.pos), `/** @type {Promise<void>} */(`); 65 } 66 } 67 else { 68 if (!parameterType || parameterType.flags & TypeFlags.Unknown) { 69 // add `void` type argument 70 changes.insertText(sourceFile, decl.parent.parent.expression.end, "<void>"); 71 } 72 } 73 } 74} 75 76function getEffectiveTypeArguments(node: NewExpression) { 77 if (isInJSFile(node)) { 78 if (isParenthesizedExpression(node.parent)) { 79 const jsDocType = getJSDocTypeTag(node.parent)?.typeExpression.type; 80 if (jsDocType && isTypeReferenceNode(jsDocType) && isIdentifier(jsDocType.typeName) && idText(jsDocType.typeName) === "Promise") { 81 return jsDocType.typeArguments; 82 } 83 } 84 } 85 else { 86 return node.typeArguments; 87 } 88}