mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 08:11:30 -06:00
add code fix convert to mapped object type (#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
This commit is contained in:
parent
b9ed782f98
commit
4606709672
@ -4280,5 +4280,9 @@
|
||||
"Remove all unused labels": {
|
||||
"category": "Message",
|
||||
"code": 95054
|
||||
},
|
||||
"Convert '{0}' to mapped object type": {
|
||||
"category": "Message",
|
||||
"code": 95055
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,6 +118,7 @@
|
||||
"../services/codefixes/requireInTs.ts",
|
||||
"../services/codefixes/useDefaultImport.ts",
|
||||
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
|
||||
"../services/codefixes/convertToMappedObjectType.ts",
|
||||
"../services/refactors/extractSymbol.ts",
|
||||
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
"../services/refactors/moveToNewFile.ts",
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
"../services/codefixes/requireInTs.ts",
|
||||
"../services/codefixes/useDefaultImport.ts",
|
||||
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
|
||||
"../services/codefixes/convertToMappedObjectType.ts",
|
||||
"../services/refactors/extractSymbol.ts",
|
||||
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
"../services/refactors/moveToNewFile.ts",
|
||||
|
||||
@ -120,6 +120,7 @@
|
||||
"../services/codefixes/requireInTs.ts",
|
||||
"../services/codefixes/useDefaultImport.ts",
|
||||
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
|
||||
"../services/codefixes/convertToMappedObjectType.ts",
|
||||
"../services/refactors/extractSymbol.ts",
|
||||
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
"../services/refactors/moveToNewFile.ts",
|
||||
|
||||
94
src/services/codefixes/convertToMappedObjectType.ts
Normal file
94
src/services/codefixes/convertToMappedObjectType.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/* @internal */
|
||||
namespace ts.codefix {
|
||||
const fixIdAddMissingTypeof = "fixConvertToMappedObjectType";
|
||||
const fixId = fixIdAddMissingTypeof;
|
||||
const errorCodes = [Diagnostics.An_index_signature_parameter_type_cannot_be_a_union_type_Consider_using_a_mapped_object_type_instead.code];
|
||||
|
||||
type FixableDeclaration = InterfaceDeclaration | TypeAliasDeclaration;
|
||||
|
||||
interface Info {
|
||||
indexSignature: IndexSignatureDeclaration;
|
||||
container: FixableDeclaration;
|
||||
otherMembers: ReadonlyArray<TypeElement>;
|
||||
parameterName: Identifier;
|
||||
parameterType: TypeNode;
|
||||
}
|
||||
|
||||
registerCodeFix({
|
||||
errorCodes,
|
||||
getCodeActions: context => {
|
||||
const { sourceFile, span } = context;
|
||||
const info = getFixableSignatureAtPosition(sourceFile, span.start);
|
||||
if (!info) return;
|
||||
const { indexSignature, container, otherMembers, parameterName, parameterType } = info;
|
||||
|
||||
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, indexSignature, container, otherMembers, parameterName, parameterType));
|
||||
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)])];
|
||||
},
|
||||
fixIds: [fixId],
|
||||
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
|
||||
const info = getFixableSignatureAtPosition(diag.file, diag.start);
|
||||
if (!info) return;
|
||||
const { indexSignature, container, otherMembers, parameterName, parameterType } = info;
|
||||
|
||||
doChange(changes, context.sourceFile, indexSignature, container, otherMembers, parameterName, parameterType);
|
||||
})
|
||||
});
|
||||
|
||||
function isFixableParameterName(node: Node): boolean {
|
||||
return node && node.parent && node.parent.parent && node.parent.parent.parent && !isClassDeclaration(node.parent.parent.parent);
|
||||
}
|
||||
|
||||
function getFixableSignatureAtPosition(sourceFile: SourceFile, pos: number): Info | undefined {
|
||||
const token = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
|
||||
if (!isFixableParameterName(token)) return undefined;
|
||||
|
||||
const indexSignature = <IndexSignatureDeclaration>token.parent.parent;
|
||||
const container = isInterfaceDeclaration(indexSignature.parent) ? indexSignature.parent : <TypeAliasDeclaration>indexSignature.parent.parent;
|
||||
const members = isInterfaceDeclaration(container) ? container.members : (<TypeLiteralNode>container.type).members;
|
||||
const otherMembers = filter(members, member => !isIndexSignatureDeclaration(member));
|
||||
const parameter = first(indexSignature.parameters);
|
||||
|
||||
return {
|
||||
indexSignature,
|
||||
container,
|
||||
otherMembers,
|
||||
parameterName: <Identifier>parameter.name,
|
||||
parameterType: parameter.type!
|
||||
};
|
||||
}
|
||||
|
||||
function getInterfaceHeritageClauses(declaration: FixableDeclaration): NodeArray<ExpressionWithTypeArguments> | undefined {
|
||||
if (!isInterfaceDeclaration(declaration)) return undefined;
|
||||
|
||||
const heritageClause = getHeritageClause(declaration.heritageClauses, SyntaxKind.ExtendsKeyword);
|
||||
return heritageClause && heritageClause.types;
|
||||
}
|
||||
|
||||
function createTypeAliasFromInterface(indexSignature: IndexSignatureDeclaration, declaration: FixableDeclaration, otherMembers: ReadonlyArray<TypeElement>, parameterName: Identifier, parameterType: TypeNode) {
|
||||
const heritageClauses = getInterfaceHeritageClauses(declaration);
|
||||
const mappedTypeParameter = createTypeParameterDeclaration(parameterName, parameterType);
|
||||
const mappedIntersectionType = createMappedTypeNode(
|
||||
hasReadonlyModifier(indexSignature) ? createModifier(SyntaxKind.ReadonlyKeyword) : undefined,
|
||||
mappedTypeParameter,
|
||||
indexSignature.questionToken,
|
||||
indexSignature.type);
|
||||
|
||||
return createTypeAliasDeclaration(
|
||||
declaration.decorators,
|
||||
declaration.modifiers,
|
||||
declaration.name,
|
||||
declaration.typeParameters,
|
||||
createIntersectionTypeNode(
|
||||
concatenate(
|
||||
heritageClauses,
|
||||
append<TypeNode>([mappedIntersectionType], otherMembers.length ? createTypeLiteralNode(otherMembers) : undefined)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, indexSignature: IndexSignatureDeclaration, declaration: FixableDeclaration, otherMembers: ReadonlyArray<TypeElement>, parameterName: Identifier, parameterType: TypeNode) {
|
||||
changes.replaceNode(sourceFile, declaration, createTypeAliasFromInterface(indexSignature, declaration, otherMembers, parameterName, parameterType));
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
return { renameFilename, renameLocation, edits };
|
||||
}
|
||||
|
||||
function isConvertableName (name: DeclarationName): name is AcceptedNameType {
|
||||
function isConvertibleName (name: DeclarationName): name is AcceptedNameType {
|
||||
return isIdentifier(name) || isStringLiteral(name);
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
// make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier
|
||||
const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly;
|
||||
if (!declaration || !rangeOverlapsWithStartEnd(declaration.name, startPosition, endPosition!) // TODO: GH#18217
|
||||
|| !isConvertableName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) return undefined;
|
||||
|| !isConvertibleName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) return undefined;
|
||||
|
||||
const name = declaration.name.text;
|
||||
const startWithUnderscore = startsWithUnderscore(name);
|
||||
|
||||
@ -111,6 +111,7 @@
|
||||
"codefixes/requireInTs.ts",
|
||||
"codefixes/useDefaultImport.ts",
|
||||
"codefixes/fixAddModuleReferTypeMissingTypeof.ts",
|
||||
"codefixes/convertToMappedObjectType.ts",
|
||||
"refactors/extractSymbol.ts",
|
||||
"refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
"refactors/moveToNewFile.ts",
|
||||
|
||||
17
tests/cases/fourslash/codeFixConvertToMappedObjectType1.ts
Normal file
17
tests/cases/fourslash/codeFixConvertToMappedObjectType1.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface SomeType {
|
||||
//// a: string;
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
type SomeType = {
|
||||
[prop in K]: any;
|
||||
} & {
|
||||
a: string;
|
||||
};`
|
||||
})
|
||||
23
tests/cases/fourslash/codeFixConvertToMappedObjectType10.ts
Normal file
23
tests/cases/fourslash/codeFixConvertToMappedObjectType10.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar<T> { bar: T; }
|
||||
//// interface SomeType<T> extends Foo, Bar<T> {
|
||||
//// a: number;
|
||||
//// b: T;
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
interface Bar<T> { bar: T; }
|
||||
type SomeType<T> = Foo & Bar<T> & {
|
||||
[prop in K]: any;
|
||||
} & {
|
||||
a: number;
|
||||
b: T;
|
||||
};`
|
||||
})
|
||||
23
tests/cases/fourslash/codeFixConvertToMappedObjectType11.ts
Normal file
23
tests/cases/fourslash/codeFixConvertToMappedObjectType11.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar<T> { bar: T; }
|
||||
//// interface SomeType<T> extends Foo, Bar<T> {
|
||||
//// a: number;
|
||||
//// b: T;
|
||||
//// readonly [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
interface Bar<T> { bar: T; }
|
||||
type SomeType<T> = Foo & Bar<T> & {
|
||||
readonly [prop in K]: any;
|
||||
} & {
|
||||
a: number;
|
||||
b: T;
|
||||
};`
|
||||
})
|
||||
12
tests/cases/fourslash/codeFixConvertToMappedObjectType12.ts
Normal file
12
tests/cases/fourslash/codeFixConvertToMappedObjectType12.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar<T> { bar: T; }
|
||||
//// interface SomeType<T> extends Foo, Bar<T> {
|
||||
//// a: number;
|
||||
//// b: T;
|
||||
//// readonly [prop: K]?: any;
|
||||
//// }
|
||||
|
||||
verify.not.codeFixAvailable()
|
||||
17
tests/cases/fourslash/codeFixConvertToMappedObjectType2.ts
Normal file
17
tests/cases/fourslash/codeFixConvertToMappedObjectType2.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// type SomeType = {
|
||||
//// a: string;
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
type SomeType = {
|
||||
[prop in K]: any;
|
||||
} & {
|
||||
a: string;
|
||||
};`
|
||||
})
|
||||
14
tests/cases/fourslash/codeFixConvertToMappedObjectType3.ts
Normal file
14
tests/cases/fourslash/codeFixConvertToMappedObjectType3.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// type SomeType = {
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
type SomeType = {
|
||||
[prop in K]: any;
|
||||
};`
|
||||
})
|
||||
14
tests/cases/fourslash/codeFixConvertToMappedObjectType4.ts
Normal file
14
tests/cases/fourslash/codeFixConvertToMappedObjectType4.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface SomeType {
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
type SomeType = {
|
||||
[prop in K]: any;
|
||||
};`
|
||||
})
|
||||
@ -0,0 +1,8 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// class SomeType {
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.not.codeFixAvailable()
|
||||
16
tests/cases/fourslash/codeFixConvertToMappedObjectType6.ts
Normal file
16
tests/cases/fourslash/codeFixConvertToMappedObjectType6.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface SomeType extends Foo {
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
type SomeType = Foo & {
|
||||
[prop in K]: any;
|
||||
};`
|
||||
})
|
||||
18
tests/cases/fourslash/codeFixConvertToMappedObjectType7.ts
Normal file
18
tests/cases/fourslash/codeFixConvertToMappedObjectType7.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar { }
|
||||
//// interface SomeType extends Foo, Bar {
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
interface Bar { }
|
||||
type SomeType = Foo & Bar & {
|
||||
[prop in K]: any;
|
||||
};`
|
||||
})
|
||||
21
tests/cases/fourslash/codeFixConvertToMappedObjectType8.ts
Normal file
21
tests/cases/fourslash/codeFixConvertToMappedObjectType8.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar { }
|
||||
//// interface SomeType extends Foo, Bar {
|
||||
//// a: number;
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
interface Bar { }
|
||||
type SomeType = Foo & Bar & {
|
||||
[prop in K]: any;
|
||||
} & {
|
||||
a: number;
|
||||
};`
|
||||
})
|
||||
21
tests/cases/fourslash/codeFixConvertToMappedObjectType9.ts
Normal file
21
tests/cases/fourslash/codeFixConvertToMappedObjectType9.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// type K = "foo" | "bar";
|
||||
//// interface Foo { }
|
||||
//// interface Bar<T> { bar: T; }
|
||||
//// interface SomeType extends Foo, Bar<number> {
|
||||
//// a: number;
|
||||
//// [prop: K]: any;
|
||||
//// }
|
||||
|
||||
verify.codeFix({
|
||||
description: `Convert 'SomeType' to mapped object type`,
|
||||
newFileContent: `type K = "foo" | "bar";
|
||||
interface Foo { }
|
||||
interface Bar<T> { bar: T; }
|
||||
type SomeType = Foo & Bar<number> & {
|
||||
[prop in K]: any;
|
||||
} & {
|
||||
a: number;
|
||||
};`
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user