Skip to content

Commit 455ea9b

Browse files
authored
fix(49964): handle auto-import dependencies/omit duplicate constraints (#50004)
1 parent 7b76416 commit 455ea9b

File tree

3 files changed

+170
-27
lines changed

3 files changed

+170
-27
lines changed

src/services/codefixes/fixAddMissingConstraint.ts

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,104 @@ namespace ts.codefix {
1616
registerCodeFix({
1717
errorCodes,
1818
getCodeActions(context) {
19-
const { sourceFile, span, program } = context;
20-
const related = getDiagnosticRelatedInfo(program, sourceFile, span);
21-
if (!related) {
22-
return;
23-
}
24-
const changes = textChanges.ChangeTracker.with(context, t => addMissingConstraint(t, related));
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));
2524
return [createCodeFixAction(fixId, changes, Diagnostics.Add_extends_constraint, fixId, Diagnostics.Add_extends_constraint_to_all_type_parameters)];
2625
},
2726
fixIds: [fixId],
28-
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
29-
const info = getDiagnosticRelatedInfo(context.program, context.sourceFile, diag);
30-
if (!info) return;
31-
return addMissingConstraint(changes, info);
32-
}),
27+
getAllCodeActions: context => {
28+
const { program, preferences, host } = context;
29+
const seen = new Map<string, 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+
const id = getNodeId(info.declaration) + "#" + info.token.getText();
36+
if (addToSeen(seen, id)) {
37+
return addMissingConstraint(changes, program, preferences, host, diag.file, info);
38+
}
39+
}
40+
return undefined;
41+
});
42+
}));
43+
}
3344
});
3445

35-
function getDiagnosticRelatedInfo(program: Program, sourceFile: SourceFile, span: TextSpan) {
46+
interface Info {
47+
constraint: Type | string;
48+
declaration: TypeParameterDeclaration;
49+
token: Node;
50+
}
51+
52+
function getInfo(program: Program, sourceFile: SourceFile, span: TextSpan): Info | undefined {
3653
const diag = find(program.getSemanticDiagnostics(sourceFile), diag => diag.start === span.start && diag.length === span.length);
37-
if (!diag || !diag.relatedInformation) return;
54+
if (diag === undefined || diag.relatedInformation === undefined) return;
55+
3856
const related = find(diag.relatedInformation, related => related.code === Diagnostics.This_type_parameter_might_need_an_extends_0_constraint.code);
39-
if (!related) return;
40-
return related;
41-
}
57+
if (related === undefined || related.file === undefined || related.start === undefined || related.length === undefined) return;
58+
59+
let declaration = findAncestorMatchingSpan(related.file, createTextSpan(related.start, related.length));
60+
if (declaration === undefined) return;
4261

43-
function addMissingConstraint(changes: textChanges.ChangeTracker, related: DiagnosticRelatedInformation): void {
44-
let decl = findAncestorMatchingSpan(related.file!, related as TextSpan);
45-
if (!decl) return;
46-
if (isIdentifier(decl) && isTypeParameterDeclaration(decl.parent)) {
47-
decl = decl.parent;
62+
if (isIdentifier(declaration) && isTypeParameterDeclaration(declaration.parent)) {
63+
declaration = declaration.parent;
4864
}
49-
if (!isTypeParameterDeclaration(decl) || isMappedTypeNode(decl.parent)) return; // should only issue fix on type parameters written using `extends`
50-
const newConstraint = flattenDiagnosticMessageText(related.messageText, "\n", 0).match(/`extends (.*)`/);
51-
if (!newConstraint) return;
52-
const newConstraintText = newConstraint[1];
5365

54-
changes.insertText(related.file!, decl.name.end, ` extends ${newConstraintText}`);
66+
if (isTypeParameterDeclaration(declaration)) {
67+
// should only issue fix on type parameters written using `extends`
68+
if (isMappedTypeNode(declaration.parent)) return;
69+
70+
const token = getTokenAtPosition(sourceFile, span.start);
71+
const checker = program.getTypeChecker();
72+
const constraint = tryGetConstraintType(checker, token) || tryGetConstraintFromDiagnosticMessage(related.messageText);
73+
74+
return { constraint, declaration, token };
75+
}
76+
return undefined;
77+
}
78+
79+
function addMissingConstraint(changes: textChanges.ChangeTracker, program: Program, preferences: UserPreferences, host: LanguageServiceHost, sourceFile: SourceFile, info: Info): void {
80+
const { declaration, constraint } = info;
81+
const checker = program.getTypeChecker();
82+
83+
if (isString(constraint)) {
84+
changes.insertText(sourceFile, declaration.name.end, ` extends ${constraint}`);
85+
}
86+
else {
87+
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
88+
const tracker = getNoopSymbolTrackerWithResolver({ program, host });
89+
const importAdder = createImportAdder(sourceFile, program, preferences, host);
90+
const typeNode = typeToAutoImportableTypeNode(checker, importAdder, constraint, /*contextNode*/ undefined, scriptTarget, /*flags*/ undefined, tracker);
91+
if (typeNode) {
92+
changes.replaceNode(sourceFile, declaration, factory.updateTypeParameterDeclaration(declaration, /*modifiers*/ undefined, declaration.name, typeNode, declaration.default));
93+
importAdder.writeFixes(changes);
94+
}
95+
}
5596
}
5697

5798
function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node {
58-
let token = getTokenAtPosition(sourceFile, span.start);
5999
const end = textSpanEnd(span);
100+
let token = getTokenAtPosition(sourceFile, span.start);
60101
while (token.end < end) {
61102
token = token.parent;
62103
}
63104
return token;
64105
}
106+
107+
function tryGetConstraintFromDiagnosticMessage(messageText: string | DiagnosticMessageChain) {
108+
const [_, constraint] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/`extends (.*)`/) || [];
109+
return constraint;
110+
}
111+
112+
function tryGetConstraintType(checker: TypeChecker, node: Node) {
113+
if (isTypeNode(node.parent)) {
114+
return checker.getTypeArgumentConstraint(node.parent);
115+
}
116+
const contextualType = isExpression(node) ? checker.getContextualType(node) : undefined;
117+
return contextualType || checker.getTypeAtLocation(node);
118+
}
65119
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @filename: /bar.ts
4+
////export type Bar = Record<string, string>
5+
////export function bar<T extends Bar>(obj: { prop: T }) {}
6+
7+
// @filename: /foo.ts
8+
////import { bar } from "./bar";
9+
////
10+
////export function foo<T>(x: T) {
11+
//// bar({ prop: x/**/ })
12+
////}
13+
14+
goTo.marker("");
15+
verify.codeFix({
16+
index: 0,
17+
description: "Add `extends` constraint.",
18+
newFileContent:
19+
`import { bar } from "./bar";
20+
21+
export function foo<T extends Bar>(x: T) {
22+
bar({ prop: x })
23+
}`
24+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @strict: true
4+
5+
// @filename: /bar.ts
6+
////export type Bar = Record<string, string>
7+
////export function bar<T extends Bar>(obj: { prop: T }) {}
8+
9+
// @filename: /foo.ts
10+
////import { bar } from "./bar";
11+
////
12+
////export function f1<T>(x: T) {
13+
//// bar({ prop: x })
14+
////}
15+
////
16+
////function f2<T>(x: T) {
17+
//// const y: `${number}` = x;
18+
////}
19+
////
20+
////interface Fn<T extends string> {}
21+
////function f3<T>(x: Fn<T>) {
22+
////}
23+
////
24+
////function f4<T = `${number}`>(x: T) {
25+
//// const y: `${number}` = x;
26+
////}
27+
////
28+
////interface TypeRef<T extends {}> {
29+
//// x: T
30+
////}
31+
////function f5<T>(): TypeRef</**/T> {
32+
//// throw undefined as any as TypeRef<T>;
33+
////}
34+
35+
goTo.file("/foo.ts");
36+
verify.codeFixAll({
37+
fixId: "addMissingConstraint",
38+
fixAllDescription: ts.Diagnostics.Add_extends_constraint_to_all_type_parameters.message,
39+
newFileContent:
40+
"import { bar } from \"./bar\";\n\n" +
41+
42+
"export function f1<T extends Bar>(x: T) {\n" +
43+
" bar({ prop: x })\n" +
44+
"}\n\n" +
45+
46+
"function f2<T extends \`${number}\`>(x: T) {\n" +
47+
" const y: `${number}` = x;\n" +
48+
"}\n\n" +
49+
50+
"interface Fn<T extends string> {}\n" +
51+
"function f3<T extends string>(x: Fn<T>) {\n" +
52+
"}\n\n" +
53+
54+
"function f4<T extends `${number}` = `${number}`>(x: T) {\n" +
55+
" const y: `${number}` = x;\n" +
56+
"}\n\n" +
57+
58+
"interface TypeRef<T extends {}> {\n" +
59+
" x: T\n" +
60+
"}\n" +
61+
"function f5<T extends {}>(): TypeRef<T> {\n" +
62+
" throw undefined as any as TypeRef<T>;\n" +
63+
"}"
64+
65+
});

0 commit comments

Comments
 (0)