Skip to content

Commit 4606709

Browse files
KingwlAndy
authored and
Andy
committed
add code fix convert to mapped object type (microsoft#24286)
* add code fix convert to mapped object type * add support for type literal and improve test * fix typo * add support for heritageClauses * only determine declaration is not class
1 parent b9ed782 commit 4606709

19 files changed

+308
-2
lines changed

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -4280,5 +4280,9 @@
42804280
"Remove all unused labels": {
42814281
"category": "Message",
42824282
"code": 95054
4283+
},
4284+
"Convert '{0}' to mapped object type": {
4285+
"category": "Message",
4286+
"code": 95055
42834287
}
42844288
}

src/harness/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"../services/codefixes/requireInTs.ts",
119119
"../services/codefixes/useDefaultImport.ts",
120120
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
121+
"../services/codefixes/convertToMappedObjectType.ts",
121122
"../services/refactors/extractSymbol.ts",
122123
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
123124
"../services/refactors/moveToNewFile.ts",

src/server/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"../services/codefixes/requireInTs.ts",
115115
"../services/codefixes/useDefaultImport.ts",
116116
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
117+
"../services/codefixes/convertToMappedObjectType.ts",
117118
"../services/refactors/extractSymbol.ts",
118119
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
119120
"../services/refactors/moveToNewFile.ts",

src/server/tsconfig.library.json

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"../services/codefixes/requireInTs.ts",
121121
"../services/codefixes/useDefaultImport.ts",
122122
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
123+
"../services/codefixes/convertToMappedObjectType.ts",
123124
"../services/refactors/extractSymbol.ts",
124125
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
125126
"../services/refactors/moveToNewFile.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixIdAddMissingTypeof = "fixConvertToMappedObjectType";
4+
const fixId = fixIdAddMissingTypeof;
5+
const errorCodes = [Diagnostics.An_index_signature_parameter_type_cannot_be_a_union_type_Consider_using_a_mapped_object_type_instead.code];
6+
7+
type FixableDeclaration = InterfaceDeclaration | TypeAliasDeclaration;
8+
9+
interface Info {
10+
indexSignature: IndexSignatureDeclaration;
11+
container: FixableDeclaration;
12+
otherMembers: ReadonlyArray<TypeElement>;
13+
parameterName: Identifier;
14+
parameterType: TypeNode;
15+
}
16+
17+
registerCodeFix({
18+
errorCodes,
19+
getCodeActions: context => {
20+
const { sourceFile, span } = context;
21+
const info = getFixableSignatureAtPosition(sourceFile, span.start);
22+
if (!info) return;
23+
const { indexSignature, container, otherMembers, parameterName, parameterType } = info;
24+
25+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, indexSignature, container, otherMembers, parameterName, parameterType));
26+
return [createCodeFixAction(fixId, changes, [Diagnostics.Convert_0_to_mapped_object_type, idText(container.name)], fixId, [Diagnostics.Convert_0_to_mapped_object_type, idText(container.name)])];
27+
},
28+
fixIds: [fixId],
29+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
30+
const info = getFixableSignatureAtPosition(diag.file, diag.start);
31+
if (!info) return;
32+
const { indexSignature, container, otherMembers, parameterName, parameterType } = info;
33+
34+
doChange(changes, context.sourceFile, indexSignature, container, otherMembers, parameterName, parameterType);
35+
})
36+
});
37+
38+
function isFixableParameterName(node: Node): boolean {
39+
return node && node.parent && node.parent.parent && node.parent.parent.parent && !isClassDeclaration(node.parent.parent.parent);
40+
}
41+
42+
function getFixableSignatureAtPosition(sourceFile: SourceFile, pos: number): Info | undefined {
43+
const token = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
44+
if (!isFixableParameterName(token)) return undefined;
45+
46+
const indexSignature = <IndexSignatureDeclaration>token.parent.parent;
47+
const container = isInterfaceDeclaration(indexSignature.parent) ? indexSignature.parent : <TypeAliasDeclaration>indexSignature.parent.parent;
48+
const members = isInterfaceDeclaration(container) ? container.members : (<TypeLiteralNode>container.type).members;
49+
const otherMembers = filter(members, member => !isIndexSignatureDeclaration(member));
50+
const parameter = first(indexSignature.parameters);
51+
52+
return {
53+
indexSignature,
54+
container,
55+
otherMembers,
56+
parameterName: <Identifier>parameter.name,
57+
parameterType: parameter.type!
58+
};
59+
}
60+
61+
function getInterfaceHeritageClauses(declaration: FixableDeclaration): NodeArray<ExpressionWithTypeArguments> | undefined {
62+
if (!isInterfaceDeclaration(declaration)) return undefined;
63+
64+
const heritageClause = getHeritageClause(declaration.heritageClauses, SyntaxKind.ExtendsKeyword);
65+
return heritageClause && heritageClause.types;
66+
}
67+
68+
function createTypeAliasFromInterface(indexSignature: IndexSignatureDeclaration, declaration: FixableDeclaration, otherMembers: ReadonlyArray<TypeElement>, parameterName: Identifier, parameterType: TypeNode) {
69+
const heritageClauses = getInterfaceHeritageClauses(declaration);
70+
const mappedTypeParameter = createTypeParameterDeclaration(parameterName, parameterType);
71+
const mappedIntersectionType = createMappedTypeNode(
72+
hasReadonlyModifier(indexSignature) ? createModifier(SyntaxKind.ReadonlyKeyword) : undefined,
73+
mappedTypeParameter,
74+
indexSignature.questionToken,
75+
indexSignature.type);
76+
77+
return createTypeAliasDeclaration(
78+
declaration.decorators,
79+
declaration.modifiers,
80+
declaration.name,
81+
declaration.typeParameters,
82+
createIntersectionTypeNode(
83+
concatenate(
84+
heritageClauses,
85+
append<TypeNode>([mappedIntersectionType], otherMembers.length ? createTypeLiteralNode(otherMembers) : undefined)
86+
)
87+
)
88+
);
89+
}
90+
91+
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, indexSignature: IndexSignatureDeclaration, declaration: FixableDeclaration, otherMembers: ReadonlyArray<TypeElement>, parameterName: Identifier, parameterType: TypeNode) {
92+
changes.replaceNode(sourceFile, declaration, createTypeAliasFromInterface(indexSignature, declaration, otherMembers, parameterName, parameterType));
93+
}
94+
}

src/services/refactors/generateGetAccessorAndSetAccessor.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
8888
return { renameFilename, renameLocation, edits };
8989
}
9090

91-
function isConvertableName (name: DeclarationName): name is AcceptedNameType {
91+
function isConvertibleName (name: DeclarationName): name is AcceptedNameType {
9292
return isIdentifier(name) || isStringLiteral(name);
9393
}
9494

@@ -125,7 +125,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
125125
// make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier
126126
const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly;
127127
if (!declaration || !rangeOverlapsWithStartEnd(declaration.name, startPosition, endPosition!) // TODO: GH#18217
128-
|| !isConvertableName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) return undefined;
128+
|| !isConvertibleName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) return undefined;
129129

130130
const name = declaration.name.text;
131131
const startWithUnderscore = startsWithUnderscore(name);

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"codefixes/requireInTs.ts",
112112
"codefixes/useDefaultImport.ts",
113113
"codefixes/fixAddModuleReferTypeMissingTypeof.ts",
114+
"codefixes/convertToMappedObjectType.ts",
114115
"refactors/extractSymbol.ts",
115116
"refactors/generateGetAccessorAndSetAccessor.ts",
116117
"refactors/moveToNewFile.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface SomeType {
5+
//// a: string;
6+
//// [prop: K]: any;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Convert 'SomeType' to mapped object type`,
11+
newFileContent: `type K = "foo" | "bar";
12+
type SomeType = {
13+
[prop in K]: any;
14+
} & {
15+
a: string;
16+
};`
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar<T> { bar: T; }
6+
//// interface SomeType<T> extends Foo, Bar<T> {
7+
//// a: number;
8+
//// b: T;
9+
//// [prop: K]: any;
10+
//// }
11+
12+
verify.codeFix({
13+
description: `Convert 'SomeType' to mapped object type`,
14+
newFileContent: `type K = "foo" | "bar";
15+
interface Foo { }
16+
interface Bar<T> { bar: T; }
17+
type SomeType<T> = Foo & Bar<T> & {
18+
[prop in K]: any;
19+
} & {
20+
a: number;
21+
b: T;
22+
};`
23+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar<T> { bar: T; }
6+
//// interface SomeType<T> extends Foo, Bar<T> {
7+
//// a: number;
8+
//// b: T;
9+
//// readonly [prop: K]: any;
10+
//// }
11+
12+
verify.codeFix({
13+
description: `Convert 'SomeType' to mapped object type`,
14+
newFileContent: `type K = "foo" | "bar";
15+
interface Foo { }
16+
interface Bar<T> { bar: T; }
17+
type SomeType<T> = Foo & Bar<T> & {
18+
readonly [prop in K]: any;
19+
} & {
20+
a: number;
21+
b: T;
22+
};`
23+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar<T> { bar: T; }
6+
//// interface SomeType<T> extends Foo, Bar<T> {
7+
//// a: number;
8+
//// b: T;
9+
//// readonly [prop: K]?: any;
10+
//// }
11+
12+
verify.not.codeFixAvailable()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// type SomeType = {
5+
//// a: string;
6+
//// [prop: K]: any;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Convert 'SomeType' to mapped object type`,
11+
newFileContent: `type K = "foo" | "bar";
12+
type SomeType = {
13+
[prop in K]: any;
14+
} & {
15+
a: string;
16+
};`
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// type SomeType = {
5+
//// [prop: K]: any;
6+
//// }
7+
8+
verify.codeFix({
9+
description: `Convert 'SomeType' to mapped object type`,
10+
newFileContent: `type K = "foo" | "bar";
11+
type SomeType = {
12+
[prop in K]: any;
13+
};`
14+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface SomeType {
5+
//// [prop: K]: any;
6+
//// }
7+
8+
verify.codeFix({
9+
description: `Convert 'SomeType' to mapped object type`,
10+
newFileContent: `type K = "foo" | "bar";
11+
type SomeType = {
12+
[prop in K]: any;
13+
};`
14+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// class SomeType {
5+
//// [prop: K]: any;
6+
//// }
7+
8+
verify.not.codeFixAvailable()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface SomeType extends Foo {
6+
//// [prop: K]: any;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Convert 'SomeType' to mapped object type`,
11+
newFileContent: `type K = "foo" | "bar";
12+
interface Foo { }
13+
type SomeType = Foo & {
14+
[prop in K]: any;
15+
};`
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar { }
6+
//// interface SomeType extends Foo, Bar {
7+
//// [prop: K]: any;
8+
//// }
9+
10+
verify.codeFix({
11+
description: `Convert 'SomeType' to mapped object type`,
12+
newFileContent: `type K = "foo" | "bar";
13+
interface Foo { }
14+
interface Bar { }
15+
type SomeType = Foo & Bar & {
16+
[prop in K]: any;
17+
};`
18+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar { }
6+
//// interface SomeType extends Foo, Bar {
7+
//// a: number;
8+
//// [prop: K]: any;
9+
//// }
10+
11+
verify.codeFix({
12+
description: `Convert 'SomeType' to mapped object type`,
13+
newFileContent: `type K = "foo" | "bar";
14+
interface Foo { }
15+
interface Bar { }
16+
type SomeType = Foo & Bar & {
17+
[prop in K]: any;
18+
} & {
19+
a: number;
20+
};`
21+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type K = "foo" | "bar";
4+
//// interface Foo { }
5+
//// interface Bar<T> { bar: T; }
6+
//// interface SomeType extends Foo, Bar<number> {
7+
//// a: number;
8+
//// [prop: K]: any;
9+
//// }
10+
11+
verify.codeFix({
12+
description: `Convert 'SomeType' to mapped object type`,
13+
newFileContent: `type K = "foo" | "bar";
14+
interface Foo { }
15+
interface Bar<T> { bar: T; }
16+
type SomeType = Foo & Bar<number> & {
17+
[prop in K]: any;
18+
} & {
19+
a: number;
20+
};`
21+
})

0 commit comments

Comments
 (0)