• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.refactor {
3    const refactorName = "Extract type";
4
5    const extractToTypeAliasAction = {
6        name: "Extract to type alias",
7        description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias),
8        kind: "refactor.extract.type",
9    };
10    const extractToInterfaceAction = {
11        name: "Extract to interface",
12        description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface),
13        kind: "refactor.extract.interface",
14    };
15    const extractToTypeDefAction = {
16        name: "Extract to typedef",
17        description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef),
18        kind: "refactor.extract.typedef"
19    };
20
21    registerRefactor(refactorName, {
22        kinds: [
23            extractToTypeAliasAction.kind,
24            extractToInterfaceAction.kind,
25            extractToTypeDefAction.kind
26        ],
27        getAvailableActions(context): readonly ApplicableRefactorInfo[] {
28            const info = getRangeToExtract(context, context.triggerReason === "invoked");
29            if (!info) return emptyArray;
30
31            if (!isRefactorErrorInfo(info)) {
32                return [{
33                    name: refactorName,
34                    description: getLocaleSpecificMessage(Diagnostics.Extract_type),
35                    actions: info.isJS ?
36                        [extractToTypeDefAction] : append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction)
37                }];
38            }
39
40            if (context.preferences.provideRefactorNotApplicableReason) {
41                return [{
42                    name: refactorName,
43                    description: getLocaleSpecificMessage(Diagnostics.Extract_type),
44                    actions: [
45                        { ...extractToTypeDefAction, notApplicableReason: info.error },
46                        { ...extractToTypeAliasAction, notApplicableReason: info.error },
47                        { ...extractToInterfaceAction, notApplicableReason: info.error },
48                    ]
49                }];
50            }
51
52            return emptyArray;
53        },
54        getEditsForAction(context, actionName): RefactorEditInfo {
55            const { file, } = context;
56            const info = getRangeToExtract(context);
57            Debug.assert(info && !isRefactorErrorInfo(info), "Expected to find a range to extract");
58
59            const name = getUniqueName("NewType", file);
60            const edits = textChanges.ChangeTracker.with(context, changes => {
61                switch (actionName) {
62                    case extractToTypeAliasAction.name:
63                        Debug.assert(!info.isJS, "Invalid actionName/JS combo");
64                        return doTypeAliasChange(changes, file, name, info);
65                    case extractToTypeDefAction.name:
66                        Debug.assert(info.isJS, "Invalid actionName/JS combo");
67                        return doTypedefChange(changes, file, name, info);
68                    case extractToInterfaceAction.name:
69                        Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo");
70                        return doInterfaceChange(changes, file, name, info as InterfaceInfo);
71                    default:
72                        Debug.fail("Unexpected action name");
73                }
74            });
75
76            const renameFilename = file.fileName;
77            const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false);
78            return { edits, renameFilename, renameLocation };
79        }
80    });
81
82    interface TypeAliasInfo {
83        isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements?: readonly TypeElement[];
84    }
85
86    interface InterfaceInfo {
87        isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements: readonly TypeElement[];
88    }
89
90    type ExtractInfo = TypeAliasInfo | InterfaceInfo;
91
92    function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): ExtractInfo | RefactorErrorInfo | undefined {
93        const { file, startPosition } = context;
94        const isJS = isSourceFileJS(file);
95        const current = getTokenAtPosition(file, startPosition);
96        const range = createTextRangeFromSpan(getRefactorContextSpan(context));
97        const cursorRequest = range.pos === range.end && considerEmptySpans;
98
99        const selection = findAncestor(current, (node => node.parent && isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) &&
100            (cursorRequest || nodeOverlapsWithStartEnd(current, file, range.pos, range.end))));
101        if (!selection || !isTypeNode(selection)) return { error: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_type_node) };
102
103        const checker = context.program.getTypeChecker();
104        const firstStatement = Debug.checkDefined(findAncestor(selection, isStatement), "Should find a statement");
105        const typeParameters = collectTypeParameters(checker, selection, firstStatement, file);
106        if (!typeParameters) return { error: getLocaleSpecificMessage(Diagnostics.No_type_could_be_extracted_from_this_type_node) };
107
108        const typeElements = flattenTypeLiteralNodeReference(checker, selection);
109        return { isJS, selection, firstStatement, typeParameters, typeElements };
110    }
111
112    function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined {
113        if (!node) return undefined;
114        if (isIntersectionTypeNode(node)) {
115            const result: TypeElement[] = [];
116            const seen = new Map<string, true>();
117            for (const type of node.types) {
118                const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type);
119                if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && addToSeen(seen, getNameFromPropertyName(type.name) as string))) {
120                    return undefined;
121                }
122
123                addRange(result, flattenedTypeMembers);
124            }
125            return result;
126        }
127        else if (isParenthesizedTypeNode(node)) {
128            return flattenTypeLiteralNodeReference(checker, node.type);
129        }
130        else if (isTypeLiteralNode(node)) {
131            return node.members;
132        }
133        return undefined;
134    }
135
136    function rangeContainsSkipTrivia(r1: TextRange, node: Node, file: SourceFile): boolean {
137        return rangeContainsStartEnd(r1, skipTrivia(file.text, node.pos), node.end);
138    }
139
140    function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined {
141        const result: TypeParameterDeclaration[] = [];
142        return visitor(selection) ? undefined : result;
143
144        function visitor(node: Node): true | undefined {
145            if (isTypeReferenceNode(node)) {
146                if (isIdentifier(node.typeName)) {
147                    const symbol = checker.resolveName(node.typeName.text, node.typeName, SymbolFlags.TypeParameter, /* excludeGlobals */ true);
148                    if (symbol) {
149                        const declaration = cast(first(symbol.declarations), isTypeParameterDeclaration);
150                        if (rangeContainsSkipTrivia(statement, declaration, file) && !rangeContainsSkipTrivia(selection, declaration, file)) {
151                            pushIfUnique(result, declaration);
152                        }
153                    }
154                }
155            }
156            else if (isInferTypeNode(node)) {
157                const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file));
158                if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) {
159                    return true;
160                }
161            }
162            else if ((isTypePredicateNode(node) || isThisTypeNode(node))) {
163                const functionLikeNode = findAncestor(node.parent, isFunctionLike);
164                if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) {
165                    return true;
166                }
167            }
168            else if (isTypeQueryNode(node)) {
169                if (isIdentifier(node.exprName)) {
170                    const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false);
171                    if (symbol && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) {
172                        return true;
173                    }
174                }
175                else {
176                    if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) {
177                        return true;
178                    }
179                }
180            }
181
182            if (file && isTupleTypeNode(node) && (getLineAndCharacterOfPosition(file, node.pos).line === getLineAndCharacterOfPosition(file, node.end).line)) {
183                setEmitFlags(node, EmitFlags.SingleLine);
184            }
185
186            return forEachChild(node, visitor);
187        }
188    }
189
190    function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) {
191        const { firstStatement, selection, typeParameters } = info;
192
193        const newTypeNode = factory.createTypeAliasDeclaration(
194            /* decorators */ undefined,
195            /* modifiers */ undefined,
196            name,
197            typeParameters.map(id => factory.updateTypeParameterDeclaration(id, id.name, id.constraint, /* defaultType */ undefined)),
198            selection
199        );
200        changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true);
201        changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
202    }
203
204    function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) {
205        const { firstStatement, selection, typeParameters, typeElements } = info;
206
207        const newTypeNode = factory.createInterfaceDeclaration(
208            /* decorators */ undefined,
209            /* modifiers */ undefined,
210            name,
211            typeParameters,
212            /* heritageClauses */ undefined,
213            typeElements
214        );
215        setTextRange(newTypeNode, typeElements[0]?.parent);
216        changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true);
217        changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
218    }
219
220    function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: ExtractInfo) {
221        const { firstStatement, selection, typeParameters } = info;
222
223        const node = factory.createJSDocTypedefTag(
224            factory.createIdentifier("typedef"),
225            factory.createJSDocTypeExpression(selection),
226            factory.createIdentifier(name));
227
228        const templates: JSDocTemplateTag[] = [];
229        forEach(typeParameters, typeParameter => {
230            const constraint = getEffectiveConstraintOfTypeParameter(typeParameter);
231            const parameter = factory.createTypeParameterDeclaration(typeParameter.name);
232            const template = factory.createJSDocTemplateTag(
233                factory.createIdentifier("template"),
234                constraint && cast(constraint, isJSDocTypeExpression),
235                [parameter]
236            );
237            templates.push(template);
238        });
239
240        changes.insertNodeBefore(file, firstStatement, factory.createJSDocComment(/* comment */ undefined, factory.createNodeArray(concatenate<JSDocTag>(templates, [node]))), /* blankLineBetween */ true);
241        changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
242    }
243}
244