• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}