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:
Wenlu Wang 2018-05-24 05:09:49 +08:00 committed by Andy
parent b9ed782f98
commit 4606709672
19 changed files with 308 additions and 2 deletions

View File

@ -4280,5 +4280,9 @@
"Remove all unused labels": {
"category": "Message",
"code": 95054
},
"Convert '{0}' to mapped object type": {
"category": "Message",
"code": 95055
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View 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));
}
}

View File

@ -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);

View File

@ -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",

View 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;
};`
})

View 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;
};`
})

View 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;
};`
})

View 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()

View 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;
};`
})

View 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;
};`
})

View 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;
};`
})

View File

@ -0,0 +1,8 @@
/// <reference path='fourslash.ts' />
//// type K = "foo" | "bar";
//// class SomeType {
//// [prop: K]: any;
//// }
verify.not.codeFixAvailable()

View 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;
};`
})

View 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;
};`
})

View 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;
};`
})

View 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;
};`
})