• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    const fixId = "addMissingConstraint";
4    const errorCodes = [
5        // We want errors this could be attached to:
6        // Diagnostics.This_type_parameter_probably_needs_an_extends_0_constraint
7        Diagnostics.Type_0_is_not_comparable_to_type_1.code,
8        Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated.code,
9        Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties.code,
10        Diagnostics.Type_0_is_not_assignable_to_type_1.code,
11        Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties.code,
12        Diagnostics.Property_0_is_incompatible_with_index_signature.code,
13        Diagnostics.Property_0_in_type_1_is_not_assignable_to_type_2.code,
14        Diagnostics.Type_0_does_not_satisfy_the_constraint_1.code,
15    ];
16    registerCodeFix({
17        errorCodes,
18        getCodeActions(context) {
19            const { sourceFile, span, program, preferences, host } = context;
20            const info = getInfo(program, sourceFile, span);
21            if (info === undefined) return;
22
23            const changes = textChanges.ChangeTracker.with(context, t => addMissingConstraint(t, program, preferences, host, sourceFile, info));
24            return [createCodeFixAction(fixId, changes, Diagnostics.Add_extends_constraint, fixId, Diagnostics.Add_extends_constraint_to_all_type_parameters)];
25        },
26        fixIds: [fixId],
27        getAllCodeActions: context => {
28            const { program, preferences, host } = context;
29            const seen = new Map<number, true>();
30
31            return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
32                eachDiagnostic(context, errorCodes, diag => {
33                    const info = getInfo(program, diag.file, createTextSpan(diag.start, diag.length));
34                    if (info) {
35                        if (addToSeen(seen, getNodeId(info.declaration))) {
36                            return addMissingConstraint(changes, program, preferences, host, diag.file, info);
37                        }
38                    }
39                    return undefined;
40                });
41            }));
42        }
43    });
44
45    interface Info {
46        constraint: Type | string;
47        declaration: TypeParameterDeclaration;
48        token: Node;
49    }
50
51    function getInfo(program: Program, sourceFile: SourceFile, span: TextSpan): Info | undefined {
52        const diag = find(program.getSemanticDiagnostics(sourceFile), diag => diag.start === span.start && diag.length === span.length);
53        if (diag === undefined || diag.relatedInformation === undefined) return;
54
55        const related = find(diag.relatedInformation, related => related.code === Diagnostics.This_type_parameter_might_need_an_extends_0_constraint.code);
56        if (related === undefined || related.file === undefined || related.start === undefined || related.length === undefined) return;
57
58        let declaration = findAncestorMatchingSpan(related.file, createTextSpan(related.start, related.length));
59        if (declaration === undefined) return;
60
61        if (isIdentifier(declaration) && isTypeParameterDeclaration(declaration.parent)) {
62            declaration = declaration.parent;
63        }
64
65        if (isTypeParameterDeclaration(declaration)) {
66            // should only issue fix on type parameters written using `extends`
67            if (isMappedTypeNode(declaration.parent)) return;
68
69            const token = getTokenAtPosition(sourceFile, span.start);
70            const checker = program.getTypeChecker();
71            const constraint = tryGetConstraintType(checker, token) || tryGetConstraintFromDiagnosticMessage(related.messageText);
72
73            return { constraint, declaration, token };
74        }
75        return undefined;
76    }
77
78    function addMissingConstraint(changes: textChanges.ChangeTracker, program: Program, preferences: UserPreferences, host: LanguageServiceHost, sourceFile: SourceFile, info: Info): void {
79        const { declaration, constraint } = info;
80        const checker = program.getTypeChecker();
81
82        if (isString(constraint)) {
83            changes.insertText(sourceFile, declaration.name.end, ` extends ${constraint}`);
84        }
85        else {
86            const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
87            const tracker = getNoopSymbolTrackerWithResolver({ program, host });
88            const importAdder = createImportAdder(sourceFile, program, preferences, host);
89            const typeNode = typeToAutoImportableTypeNode(checker, importAdder, constraint, /*contextNode*/ undefined, scriptTarget, /*flags*/ undefined, tracker);
90            if (typeNode) {
91                changes.replaceNode(sourceFile, declaration, factory.updateTypeParameterDeclaration(declaration, /*modifiers*/ undefined, declaration.name, typeNode, declaration.default));
92                importAdder.writeFixes(changes);
93            }
94        }
95    }
96
97    function tryGetConstraintFromDiagnosticMessage(messageText: string | DiagnosticMessageChain) {
98        const [_, constraint] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/`extends (.*)`/) || [];
99        return constraint;
100    }
101
102    function tryGetConstraintType(checker: TypeChecker, node: Node) {
103        if (isTypeNode(node.parent)) {
104            return checker.getTypeArgumentConstraint(node.parent);
105        }
106        const contextualType = isExpression(node) ? checker.getContextualType(node) : undefined;
107        return contextualType || checker.getTypeAtLocation(node);
108    }
109}
110