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:
parent
5d36aab06f
commit
af9ca21643
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
16
tests/cases/fourslash/refactorExtractType60.ts
Normal file
16
tests/cases/fourslash/refactorExtractType60.ts
Normal 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) { }`,
|
||||
});
|
18
tests/cases/fourslash/refactorExtractType61.ts
Normal file
18
tests/cases/fourslash/refactorExtractType61.ts
Normal 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) { }`,
|
||||
});
|
7
tests/cases/fourslash/refactorExtractType62.ts
Normal file
7
tests/cases/fourslash/refactorExtractType62.ts
Normal 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")
|
7
tests/cases/fourslash/refactorExtractType63.ts
Normal file
7
tests/cases/fourslash/refactorExtractType63.ts
Normal 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")
|
7
tests/cases/fourslash/refactorExtractType64.ts
Normal file
7
tests/cases/fourslash/refactorExtractType64.ts
Normal 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")
|
7
tests/cases/fourslash/refactorExtractType65.ts
Normal file
7
tests/cases/fourslash/refactorExtractType65.ts
Normal 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")
|
16
tests/cases/fourslash/refactorExtractType66.ts
Normal file
16
tests/cases/fourslash/refactorExtractType66.ts
Normal 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>) { }`,
|
||||
});
|
6
tests/cases/fourslash/refactorExtractType67.ts
Normal file
6
tests/cases/fourslash/refactorExtractType67.ts
Normal 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")
|
6
tests/cases/fourslash/refactorExtractType68.ts
Normal file
6
tests/cases/fourslash/refactorExtractType68.ts
Normal 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")
|
Loading…
Reference in a new issue