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