add support for extract as interface (#31644)

* add support for extract as interface

* fix action assert

* Donot provide convert to interface if duplicate member
This commit is contained in:
Wenlu Wang 2019-08-29 02:34:41 +08:00 committed by Andrew Branch
parent 5d36aab06f
commit af9ca21643
11 changed files with 172 additions and 13 deletions

View file

@ -5120,6 +5120,10 @@
"category": "Message",
"code": 95089
},
"Extract to interface": {
"category": "Message",
"code": 95090
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",

View file

@ -2,6 +2,7 @@
namespace ts.refactor {
const refactorName = "Extract type";
const extractToTypeAlias = "Extract to type alias";
const extractToInterface = "Extract to interface";
const extractToTypeDef = "Extract to typedef";
registerRefactor(refactorName, {
getAvailableActions(context): ReadonlyArray<ApplicableRefactorInfo> {
@ -11,23 +12,35 @@ namespace ts.refactor {
return [{
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
actions: [info.isJS ? {
actions: info.isJS ? [{
name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef)
} : {
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
}]
}] : append([{
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
}], info.typeElements && {
name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface)
})
}];
},
getEditsForAction(context, actionName): RefactorEditInfo {
Debug.assert(actionName === extractToTypeAlias || actionName === extractToTypeDef, "Unexpected action name");
const { file } = context;
const info = Debug.assertDefined(getRangeToExtract(context), "Expected to find a range to extract");
Debug.assert(actionName === extractToTypeAlias && !info.isJS || actionName === extractToTypeDef && info.isJS, "Invalid actionName/JS combo");
const name = getUniqueName("NewType", file);
const edits = textChanges.ChangeTracker.with(context, changes => info.isJS ?
doTypedefChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters) :
doTypeAliasChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters));
const edits = textChanges.ChangeTracker.with(context, changes => {
switch (actionName) {
case extractToTypeAlias:
Debug.assert(!info.isJS, "Invalid actionName/JS combo");
return doTypeAliasChange(changes, file, name, info);
case extractToTypeDef:
Debug.assert(info.isJS, "Invalid actionName/JS combo");
return doTypedefChange(changes, file, name, info);
case extractToInterface:
Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo");
return doInterfaceChange(changes, file, name, info as InterfaceInfo);
default:
Debug.fail("Unexpected action name");
}
});
const renameFilename = file.fileName;
const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false);
@ -35,7 +48,15 @@ namespace ts.refactor {
}
});
interface Info { isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; }
interface TypeAliasInfo {
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; typeElements?: ReadonlyArray<TypeElement>;
}
interface InterfaceInfo {
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; typeElements: ReadonlyArray<TypeElement>;
}
type Info = TypeAliasInfo | InterfaceInfo;
function getRangeToExtract(context: RefactorContext): Info | undefined {
const { file, startPosition } = context;
@ -51,7 +72,32 @@ namespace ts.refactor {
const typeParameters = collectTypeParameters(checker, selection, firstStatement, file);
if (!typeParameters) return undefined;
return { isJS, selection, firstStatement, typeParameters };
const typeElements = flattenTypeLiteralNodeReference(checker, selection);
return { isJS, selection, firstStatement, typeParameters, typeElements };
}
function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): ReadonlyArray<TypeElement> | undefined {
if (!node) return undefined;
if (isIntersectionTypeNode(node)) {
const result: TypeElement[] = [];
const seen = createMap<true>();
for (const type of node.types) {
const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type);
if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && addToSeen(seen, getNameFromPropertyName(type.name) as string))) {
return undefined;
}
addRange(result, flattenedTypeMembers);
}
return result;
}
else if (isParenthesizedTypeNode(node)) {
return flattenTypeLiteralNodeReference(checker, node.type);
}
else if (isTypeLiteralNode(node)) {
return node.members;
}
return undefined;
}
function isStatementAndHasJSDoc(n: Node): n is (Statement & HasJSDoc) {
@ -107,7 +153,9 @@ namespace ts.refactor {
}
}
function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) {
const { firstStatement, selection, typeParameters } = info;
const newTypeNode = createTypeAliasDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
@ -119,7 +167,24 @@ namespace ts.refactor {
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
}
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) {
const { firstStatement, selection, typeParameters, typeElements } = info;
const newTypeNode = createInterfaceDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
name,
typeParameters,
/* heritageClauses */ undefined,
typeElements
);
changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true);
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
}
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) {
const { firstStatement, selection, typeParameters } = info;
const node = <JSDocTypedefTag>createNode(SyntaxKind.JSDocTypedefTag);
node.tagName = createIdentifier("typedef"); // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539
node.fullName = createIdentifier(name);

View file

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
//// function foo(a: /*a*/{ a: number | string, b: string }/*b*/) { }
goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Extract type",
actionName: "Extract to interface",
actionDescription: "Extract to interface",
newContent: `interface /*RENAME*/NewType {
a: number | string;
b: string;
}
function foo(a: NewType) { }`,
});

View file

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
//// function foo(a: /*a*/{ a: number | string, b: string } & { c: string } & { d: boolean }/*b*/) { }
goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Extract type",
actionName: "Extract to interface",
actionDescription: "Extract to interface",
newContent: `interface /*RENAME*/NewType {
a: number | string;
b: string;
c: string;
d: boolean;
}
function foo(a: NewType) { }`,
});

View file

@ -0,0 +1,7 @@
/// <reference path='fourslash.ts' />
//// type T = { c: string }
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")

View file

@ -0,0 +1,7 @@
/// <reference path='fourslash.ts' />
//// type T = { c: string } & { d: boolean }
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")

View file

@ -0,0 +1,7 @@
/// <reference path='fourslash.ts' />
//// type T = { c: string } & Record<string, string>
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")

View file

@ -0,0 +1,7 @@
/// <reference path='fourslash.ts' />
//// type T = { c: string }
//// function foo(a: /*a*/T/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")

View file

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
//// function foo<U>(a: /*a*/{ a: string } & { b: U }/*b*/) { }
goTo.select("a", "b");
edit.applyRefactor({
refactorName: "Extract type",
actionName: "Extract to interface",
actionDescription: "Extract to interface",
newContent: `interface /*RENAME*/NewType<U> {
a: string;
b: U;
}
function foo<U>(a: NewType<U>) { }`,
});

View file

@ -0,0 +1,6 @@
/// <reference path='fourslash.ts' />
//// function foo(a: /*a*/{ a: number | string, b: string } & { b: string } & { d: boolean }/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")

View file

@ -0,0 +1,6 @@
/// <reference path='fourslash.ts' />
//// function foo(a: /*a*/{ a: number | string, b: string } & { b: number } & { d: boolean }/*b*/) { }
goTo.select("a", "b");
verify.not.refactorAvailable("Extract type", "Extract to interface")