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