diff --git a/src/services/codefixes/fixAddMissingConstraint.ts b/src/services/codefixes/fixAddMissingConstraint.ts index bd6b8650bfe59..2520c806e6690 100644 --- a/src/services/codefixes/fixAddMissingConstraint.ts +++ b/src/services/codefixes/fixAddMissingConstraint.ts @@ -16,50 +16,104 @@ namespace ts.codefix { registerCodeFix({ errorCodes, getCodeActions(context) { - const { sourceFile, span, program } = context; - const related = getDiagnosticRelatedInfo(program, sourceFile, span); - if (!related) { - return; - } - const changes = textChanges.ChangeTracker.with(context, t => addMissingConstraint(t, related)); + const { sourceFile, span, program, preferences, host } = context; + const info = getInfo(program, sourceFile, span); + if (info === undefined) return; + + const changes = textChanges.ChangeTracker.with(context, t => addMissingConstraint(t, program, preferences, host, sourceFile, info)); return [createCodeFixAction(fixId, changes, Diagnostics.Add_extends_constraint, fixId, Diagnostics.Add_extends_constraint_to_all_type_parameters)]; }, fixIds: [fixId], - getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => { - const info = getDiagnosticRelatedInfo(context.program, context.sourceFile, diag); - if (!info) return; - return addMissingConstraint(changes, info); - }), + getAllCodeActions: context => { + const { program, preferences, host } = context; + const seen = new Map(); + + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { + eachDiagnostic(context, errorCodes, diag => { + const info = getInfo(program, diag.file, createTextSpan(diag.start, diag.length)); + if (info) { + const id = getNodeId(info.declaration) + "#" + info.token.getText(); + if (addToSeen(seen, id)) { + return addMissingConstraint(changes, program, preferences, host, diag.file, info); + } + } + return undefined; + }); + })); + } }); - function getDiagnosticRelatedInfo(program: Program, sourceFile: SourceFile, span: TextSpan) { + interface Info { + constraint: Type | string; + declaration: TypeParameterDeclaration; + token: Node; + } + + function getInfo(program: Program, sourceFile: SourceFile, span: TextSpan): Info | undefined { const diag = find(program.getSemanticDiagnostics(sourceFile), diag => diag.start === span.start && diag.length === span.length); - if (!diag || !diag.relatedInformation) return; + if (diag === undefined || diag.relatedInformation === undefined) return; + const related = find(diag.relatedInformation, related => related.code === Diagnostics.This_type_parameter_might_need_an_extends_0_constraint.code); - if (!related) return; - return related; - } + if (related === undefined || related.file === undefined || related.start === undefined || related.length === undefined) return; + + let declaration = findAncestorMatchingSpan(related.file, createTextSpan(related.start, related.length)); + if (declaration === undefined) return; - function addMissingConstraint(changes: textChanges.ChangeTracker, related: DiagnosticRelatedInformation): void { - let decl = findAncestorMatchingSpan(related.file!, related as TextSpan); - if (!decl) return; - if (isIdentifier(decl) && isTypeParameterDeclaration(decl.parent)) { - decl = decl.parent; + if (isIdentifier(declaration) && isTypeParameterDeclaration(declaration.parent)) { + declaration = declaration.parent; } - if (!isTypeParameterDeclaration(decl) || isMappedTypeNode(decl.parent)) return; // should only issue fix on type parameters written using `extends` - const newConstraint = flattenDiagnosticMessageText(related.messageText, "\n", 0).match(/`extends (.*)`/); - if (!newConstraint) return; - const newConstraintText = newConstraint[1]; - changes.insertText(related.file!, decl.name.end, ` extends ${newConstraintText}`); + if (isTypeParameterDeclaration(declaration)) { + // should only issue fix on type parameters written using `extends` + if (isMappedTypeNode(declaration.parent)) return; + + const token = getTokenAtPosition(sourceFile, span.start); + const checker = program.getTypeChecker(); + const constraint = tryGetConstraintType(checker, token) || tryGetConstraintFromDiagnosticMessage(related.messageText); + + return { constraint, declaration, token }; + } + return undefined; + } + + function addMissingConstraint(changes: textChanges.ChangeTracker, program: Program, preferences: UserPreferences, host: LanguageServiceHost, sourceFile: SourceFile, info: Info): void { + const { declaration, constraint } = info; + const checker = program.getTypeChecker(); + + if (isString(constraint)) { + changes.insertText(sourceFile, declaration.name.end, ` extends ${constraint}`); + } + else { + const scriptTarget = getEmitScriptTarget(program.getCompilerOptions()); + const tracker = getNoopSymbolTrackerWithResolver({ program, host }); + const importAdder = createImportAdder(sourceFile, program, preferences, host); + const typeNode = typeToAutoImportableTypeNode(checker, importAdder, constraint, /*contextNode*/ undefined, scriptTarget, /*flags*/ undefined, tracker); + if (typeNode) { + changes.replaceNode(sourceFile, declaration, factory.updateTypeParameterDeclaration(declaration, /*modifiers*/ undefined, declaration.name, typeNode, declaration.default)); + importAdder.writeFixes(changes); + } + } } function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node { - let token = getTokenAtPosition(sourceFile, span.start); const end = textSpanEnd(span); + let token = getTokenAtPosition(sourceFile, span.start); while (token.end < end) { token = token.parent; } return token; } + + function tryGetConstraintFromDiagnosticMessage(messageText: string | DiagnosticMessageChain) { + const [_, constraint] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/`extends (.*)`/) || []; + return constraint; + } + + function tryGetConstraintType(checker: TypeChecker, node: Node) { + if (isTypeNode(node.parent)) { + return checker.getTypeArgumentConstraint(node.parent); + } + const contextualType = isExpression(node) ? checker.getContextualType(node) : undefined; + return contextualType || checker.getTypeAtLocation(node); + } } diff --git a/tests/cases/fourslash/quickfixAddMissingConstraint4.ts b/tests/cases/fourslash/quickfixAddMissingConstraint4.ts new file mode 100644 index 0000000000000..6a973adedb78d --- /dev/null +++ b/tests/cases/fourslash/quickfixAddMissingConstraint4.ts @@ -0,0 +1,24 @@ +/// + +// @filename: /bar.ts +////export type Bar = Record +////export function bar(obj: { prop: T }) {} + +// @filename: /foo.ts +////import { bar } from "./bar"; +//// +////export function foo(x: T) { +//// bar({ prop: x/**/ }) +////} + +goTo.marker(""); +verify.codeFix({ + index: 0, + description: "Add `extends` constraint.", + newFileContent: +`import { bar } from "./bar"; + +export function foo(x: T) { + bar({ prop: x }) +}` +}); diff --git a/tests/cases/fourslash/quickfixAddMissingConstraint_all.ts b/tests/cases/fourslash/quickfixAddMissingConstraint_all.ts new file mode 100644 index 0000000000000..140e11f99bc87 --- /dev/null +++ b/tests/cases/fourslash/quickfixAddMissingConstraint_all.ts @@ -0,0 +1,65 @@ +/// + +// @strict: true + +// @filename: /bar.ts +////export type Bar = Record +////export function bar(obj: { prop: T }) {} + +// @filename: /foo.ts +////import { bar } from "./bar"; +//// +////export function f1(x: T) { +//// bar({ prop: x }) +////} +//// +////function f2(x: T) { +//// const y: `${number}` = x; +////} +//// +////interface Fn {} +////function f3(x: Fn) { +////} +//// +////function f4(x: T) { +//// const y: `${number}` = x; +////} +//// +////interface TypeRef { +//// x: T +////} +////function f5(): TypeRef { +//// throw undefined as any as TypeRef; +////} + +goTo.file("/foo.ts"); +verify.codeFixAll({ + fixId: "addMissingConstraint", + fixAllDescription: ts.Diagnostics.Add_extends_constraint_to_all_type_parameters.message, + newFileContent: +"import { bar } from \"./bar\";\n\n" + + +"export function f1(x: T) {\n" + +" bar({ prop: x })\n" + +"}\n\n" + + +"function f2(x: T) {\n" + +" const y: `${number}` = x;\n" + +"}\n\n" + + +"interface Fn {}\n" + +"function f3(x: Fn) {\n" + +"}\n\n" + + +"function f4(x: T) {\n" + +" const y: `${number}` = x;\n" + +"}\n\n" + + +"interface TypeRef {\n" + +" x: T\n" + +"}\n" + +"function f5(): TypeRef {\n" + +" throw undefined as any as TypeRef;\n" + +"}" + +});