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: function getRefactorActionsToExtractType(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: function getRefactorEditsToExtractType(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 typeName = node.typeName; 148 const symbol = checker.resolveName(typeName.text, typeName, SymbolFlags.TypeParameter, /* excludeGlobals */ true); 149 for (const decl of symbol?.declarations || emptyArray) { 150 if (isTypeParameterDeclaration(decl) && decl.getSourceFile() === file) { 151 // skip extraction if the type node is in the range of the type parameter declaration. 152 // function foo<T extends { a?: /**/T }>(): void; 153 if (decl.name.escapedText === typeName.escapedText && rangeContainsSkipTrivia(decl, selection, file)) { 154 return true; 155 } 156 157 if (rangeContainsSkipTrivia(statement, decl, file) && !rangeContainsSkipTrivia(selection, decl, file)) { 158 pushIfUnique(result, decl); 159 break; 160 } 161 } 162 } 163 } 164 } 165 else if (isInferTypeNode(node)) { 166 const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file)); 167 if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) { 168 return true; 169 } 170 } 171 else if ((isTypePredicateNode(node) || isThisTypeNode(node))) { 172 const functionLikeNode = findAncestor(node.parent, isFunctionLike); 173 if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) { 174 return true; 175 } 176 } 177 else if (isTypeQueryNode(node)) { 178 if (isIdentifier(node.exprName)) { 179 const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false); 180 if (symbol?.valueDeclaration && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) { 181 return true; 182 } 183 } 184 else { 185 if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) { 186 return true; 187 } 188 } 189 } 190 191 if (file && isTupleTypeNode(node) && (getLineAndCharacterOfPosition(file, node.pos).line === getLineAndCharacterOfPosition(file, node.end).line)) { 192 setEmitFlags(node, EmitFlags.SingleLine); 193 } 194 195 return forEachChild(node, visitor); 196 } 197 } 198 199 function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) { 200 const { firstStatement, selection, typeParameters } = info; 201 202 const newTypeNode = factory.createTypeAliasDeclaration( 203 /* modifiers */ undefined, 204 name, 205 typeParameters.map(id => factory.updateTypeParameterDeclaration(id, id.modifiers, id.name, id.constraint, /* defaultType */ undefined)), 206 selection 207 ); 208 changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); 209 changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace }); 210 } 211 212 function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) { 213 const { firstStatement, selection, typeParameters, typeElements } = info; 214 215 const newTypeNode = factory.createInterfaceDeclaration( 216 /* modifiers */ undefined, 217 name, 218 typeParameters, 219 /* heritageClauses */ undefined, 220 typeElements 221 ); 222 setTextRange(newTypeNode, typeElements[0]?.parent); 223 changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); 224 changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace }); 225 } 226 227 function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: ExtractInfo) { 228 const { firstStatement, selection, typeParameters } = info; 229 230 setEmitFlags(selection, EmitFlags.NoComments | EmitFlags.NoNestedComments); 231 232 const node = factory.createJSDocTypedefTag( 233 factory.createIdentifier("typedef"), 234 factory.createJSDocTypeExpression(selection), 235 factory.createIdentifier(name)); 236 237 const templates: JSDocTemplateTag[] = []; 238 forEach(typeParameters, typeParameter => { 239 const constraint = getEffectiveConstraintOfTypeParameter(typeParameter); 240 const parameter = factory.createTypeParameterDeclaration(/*modifiers*/ undefined, typeParameter.name); 241 const template = factory.createJSDocTemplateTag( 242 factory.createIdentifier("template"), 243 constraint && cast(constraint, isJSDocTypeExpression), 244 [parameter] 245 ); 246 templates.push(template); 247 }); 248 249 changes.insertNodeBefore(file, firstStatement, factory.createJSDocComment(/* comment */ undefined, factory.createNodeArray(concatenate<JSDocTag>(templates, [node]))), /* blankLineBetween */ true); 250 changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); 251 } 252} 253