Skip to content

Commit 714821f

Browse files
Kingwlsandersn
authored andcommitted
add refactor of extract type (#30562)
* add basically implement * add rename location and add testcase * collection type arguments * disallow infer type * add support for typedef convert * refactor info to make type safe * disallow type pred * avoid unnecessary branch * disallow type query * haha😂 Co-Authored-By: Kingwl <[email protected]> * Update src/services/refactors/extractType.ts Co-Authored-By: Kingwl <[email protected]> * Update src/services/refactors/extractType.ts Co-Authored-By: Kingwl <[email protected]> * add more tests * add template tag support in jsdoc * add support of type parameters constraint * add more tests * merge branch * add more tests * refactor and update function name
1 parent 6c4876a commit 714821f

File tree

68 files changed

+1065
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1065
-0
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4962,6 +4962,19 @@
49624962
"category": "Message",
49634963
"code": 95076
49644964
},
4965+
"Extract type": {
4966+
"category": "Message",
4967+
"code": 95077
4968+
},
4969+
"Extract to type alias": {
4970+
"category": "Message",
4971+
"code": 95078
4972+
},
4973+
"Extract to typedef": {
4974+
"category": "Message",
4975+
"code": 95079
4976+
},
4977+
49654978
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer." :{
49664979
"category": "Error",
49674980
"code": 18004

src/services/refactors/extractType.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* @internal */
2+
namespace ts.refactor {
3+
const refactorName = "Extract type";
4+
const extractToTypeAlias = "Extract to type alias";
5+
const extractToTypeDef = "Extract to typedef";
6+
registerRefactor(refactorName, {
7+
getAvailableActions(context): ReadonlyArray<ApplicableRefactorInfo> {
8+
const info = getRangeToExtract(context);
9+
if (!info) return emptyArray;
10+
11+
return [{
12+
name: refactorName,
13+
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
14+
actions: [info.isJS ? {
15+
name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef)
16+
} : {
17+
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
18+
}]
19+
}];
20+
},
21+
getEditsForAction(context, actionName): RefactorEditInfo {
22+
Debug.assert(actionName === extractToTypeAlias || actionName === extractToTypeDef);
23+
const { file } = context;
24+
const info = Debug.assertDefined(getRangeToExtract(context));
25+
Debug.assert(actionName === extractToTypeAlias && !info.isJS || actionName === extractToTypeDef && info.isJS);
26+
27+
const name = getUniqueName("NewType", file);
28+
const edits = textChanges.ChangeTracker.with(context, changes => info.isJS ?
29+
doTypedefChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters) :
30+
doTypeAliasChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters));
31+
32+
const renameFilename = file.fileName;
33+
const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false);
34+
return { edits, renameFilename, renameLocation };
35+
}
36+
});
37+
38+
interface Info { isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; }
39+
40+
function getRangeToExtract(context: RefactorContext): Info | undefined {
41+
const { file, startPosition } = context;
42+
const isJS = isSourceFileJS(file);
43+
const current = getTokenAtPosition(file, startPosition);
44+
const range = createTextRangeFromSpan(getRefactorContextSpan(context));
45+
46+
const selection = findAncestor(current, (node => node.parent && rangeContainsSkipTrivia(range, node, file) && !rangeContainsSkipTrivia(range, node.parent, file)));
47+
if (!selection || !isTypeNode(selection)) return undefined;
48+
49+
const checker = context.program.getTypeChecker();
50+
const firstStatement = Debug.assertDefined(isJS ? findAncestor(selection, isStatementAndHasJSDoc) : findAncestor(selection, isStatement));
51+
const typeParameters = collectTypeParameters(checker, selection, firstStatement, file);
52+
if (!typeParameters) return undefined;
53+
54+
return { isJS, selection, firstStatement, typeParameters };
55+
}
56+
57+
function isStatementAndHasJSDoc(n: Node): n is (Statement & HasJSDoc) {
58+
return isStatement(n) && hasJSDocNodes(n);
59+
}
60+
61+
function rangeContainsSkipTrivia(r1: TextRange, node: Node, file: SourceFile): boolean {
62+
return rangeContainsStartEnd(r1, skipTrivia(file.text, node.pos), node.end);
63+
}
64+
65+
function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined {
66+
const result: TypeParameterDeclaration[] = [];
67+
return visitor(selection) ? undefined : result;
68+
69+
function visitor(node: Node): true | undefined {
70+
if (isTypeReferenceNode(node)) {
71+
if (isIdentifier(node.typeName)) {
72+
const symbol = checker.resolveName(node.typeName.text, node.typeName, SymbolFlags.TypeParameter, /* excludeGlobals */ true);
73+
if (symbol) {
74+
const declaration = cast(first(symbol.declarations), isTypeParameterDeclaration);
75+
if (rangeContainsSkipTrivia(statement, declaration, file) && !rangeContainsSkipTrivia(selection, declaration, file)) {
76+
result.push(declaration);
77+
}
78+
}
79+
}
80+
}
81+
else if (isInferTypeNode(node)) {
82+
const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file));
83+
if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) {
84+
return true;
85+
}
86+
}
87+
else if ((isTypePredicateNode(node) || isThisTypeNode(node))) {
88+
const functionLikeNode = findAncestor(node.parent, isFunctionLike);
89+
if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) {
90+
return true;
91+
}
92+
}
93+
else if (isTypeQueryNode(node)) {
94+
if (isIdentifier(node.exprName)) {
95+
const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false);
96+
if (symbol && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) {
97+
return true;
98+
}
99+
}
100+
else {
101+
if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) {
102+
return true;
103+
}
104+
}
105+
}
106+
return forEachChild(node, visitor);
107+
}
108+
}
109+
110+
function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
111+
const newTypeNode = createTypeAliasDeclaration(
112+
/* decorators */ undefined,
113+
/* modifiers */ undefined,
114+
name,
115+
typeParameters.map(id => updateTypeParameterDeclaration(id, id.name, id.constraint, /* defaultType */ undefined)),
116+
selection
117+
);
118+
changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true);
119+
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
120+
}
121+
122+
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
123+
const node = <JSDocTypedefTag>createNode(SyntaxKind.JSDocTypedefTag);
124+
node.tagName = createIdentifier("typedef"); // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539
125+
node.fullName = createIdentifier(name);
126+
node.name = node.fullName;
127+
node.typeExpression = createJSDocTypeExpression(selection);
128+
129+
const templates: JSDocTemplateTag[] = [];
130+
forEach(typeParameters, typeParameter => {
131+
const constraint = getEffectiveConstraintOfTypeParameter(typeParameter);
132+
133+
const template = <JSDocTemplateTag>createNode(SyntaxKind.JSDocTemplateTag);
134+
template.tagName = createIdentifier("template");
135+
template.constraint = constraint && cast(constraint, isJSDocTypeExpression);
136+
137+
const parameter = <TypeParameterDeclaration>createNode(SyntaxKind.TypeParameter);
138+
parameter.name = typeParameter.name;
139+
template.typeParameters = createNodeArray([parameter]);
140+
141+
templates.push(template);
142+
});
143+
144+
changes.insertNodeBefore(file, firstStatement, createJSDocComment(/* comment */ undefined, createNodeArray(concatenate<JSDocTag>(templates, [node]))), /* blankLineBetween */ true);
145+
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
146+
}
147+
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"refactors/convertExport.ts",
8181
"refactors/convertImport.ts",
8282
"refactors/extractSymbol.ts",
83+
"refactors/extractType.ts",
8384
"refactors/generateGetAccessorAndSetAccessor.ts",
8485
"refactors/moveToNewFile.ts",
8586
"refactors/addOrRemoveBracesToArrowFunction.ts",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// var x: /*a*/{ a?: number, b?: string }/*b*/ = { };
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType = {
11+
a?: number;
12+
b?: string;
13+
};
14+
15+
var x: NewType = { };`,
16+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: number, b?: number, ...c: number[]): /*a*/boolean/*b*/ {
4+
//// return false as boolean
5+
//// }
6+
7+
goTo.select("a", "b");
8+
edit.applyRefactor({
9+
refactorName: "Extract type",
10+
actionName: "Extract to type alias",
11+
actionDescription: "Extract to type alias",
12+
newContent: `type /*RENAME*/NewType = boolean;
13+
14+
function foo(a: number, b?: number, ...c: number[]): NewType {
15+
return false as boolean
16+
}`,
17+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: number, b?: number, ...c: number[]): boolean {
4+
//// return false as /*a*/boolean/*b*/
5+
//// }
6+
7+
goTo.select("a", "b");
8+
edit.applyRefactor({
9+
refactorName: "Extract type",
10+
actionName: "Extract to type alias",
11+
actionDescription: "Extract to type alias",
12+
newContent: `function foo(a: number, b?: number, ...c: number[]): boolean {
13+
type /*RENAME*/NewType = boolean;
14+
15+
return false as NewType
16+
}`,
17+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// interface A<T = /*a*/string/*b*/> {
4+
//// a: boolean
5+
//// b: number
6+
//// c: T
7+
//// }
8+
9+
goTo.select("a", "b");
10+
edit.applyRefactor({
11+
refactorName: "Extract type",
12+
actionName: "Extract to type alias",
13+
actionDescription: "Extract to type alias",
14+
newContent: `type /*RENAME*/NewType = string;
15+
16+
interface A<T = NewType> {
17+
a: boolean
18+
b: number
19+
c: T
20+
}`,
21+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// interface A<T = string> {
4+
//// a: /*a*/boolean/*b*/
5+
//// b: number
6+
//// c: T
7+
//// }
8+
9+
goTo.select("a", "b");
10+
edit.applyRefactor({
11+
refactorName: "Extract type",
12+
actionName: "Extract to type alias",
13+
actionDescription: "Extract to type alias",
14+
newContent: `type /*RENAME*/NewType = boolean;
15+
16+
interface A<T = string> {
17+
a: NewType
18+
b: number
19+
c: T
20+
}`,
21+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<T = /*a*/boolean/*b*/> = string | number | T
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType = boolean;
11+
12+
type A<T = NewType> = string | number | T`,
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<T = boolean> = /*a*/string/*b*/ | number | T
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType = string;
11+
12+
type A<T = boolean> = NewType | number | T`,
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// var x: { a?: /*a*/number/*b*/, b?: string } = { };
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType = number;
11+
12+
var x: { a?: NewType, b?: string } = { };`,
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<T = boolean> = string | number | /*a*/T/*b*/
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType<T> = T;
11+
12+
type A<T = boolean> = string | number | NewType<T>`,
13+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<B, C, D = B> = /*a*/Partial<C | string>/*b*/ & D | C
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType<C> = Partial<C | string>;
11+
12+
type A<B, C, D = B> = NewType<C> & D | C`,
13+
});
14+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<B, C, D = B> = /*a*/Partial<C | string | D>/*b*/ & D | C
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType<C, D> = Partial<C | string | D>;
11+
12+
type A<B, C, D = B> = NewType<C, D> & D | C`,
13+
});
14+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// var x: /*a*/string/*b*/ = '';
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType = string;
11+
12+
var x: NewType = '';`,
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type A<T, U> = () => <T>(v: /*a*/T/*b*/) => (v: T) => <T>(v: T) => U
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to type alias",
9+
actionDescription: "Extract to type alias",
10+
newContent: `type /*RENAME*/NewType<T> = T;
11+
12+
type A<T, U> = () => <T>(v: NewType<T>) => (v: T) => <T>(v: T) => U`,
13+
});

0 commit comments

Comments
 (0)