|
| 1 | +import { |
| 2 | + Diagnostics, |
| 3 | + factory, |
| 4 | + forEach, |
| 5 | + getSynthesizedDeepClone, |
| 6 | + getTokenAtPosition, |
| 7 | + hasJSDocNodes, |
| 8 | + InterfaceDeclaration, |
| 9 | + isJSDocTypedefTag, |
| 10 | + isJSDocTypeLiteral, |
| 11 | + JSDocPropertyLikeTag, |
| 12 | + JSDocTypedefTag, |
| 13 | + JSDocTypeExpression, |
| 14 | + JSDocTypeLiteral, |
| 15 | + mapDefined, |
| 16 | + Node, |
| 17 | + PropertySignature, |
| 18 | + some, |
| 19 | + SourceFile, |
| 20 | + SyntaxKind, |
| 21 | + textChanges, |
| 22 | + TypeAliasDeclaration, |
| 23 | +} from "../_namespaces/ts"; |
| 24 | +import { codeFixAll, createCodeFixAction, registerCodeFix } from "../_namespaces/ts.codefix"; |
| 25 | + |
| 26 | +const fixId = "convertTypedefToType"; |
| 27 | +const errorCodes = [Diagnostics.JSDoc_typedef_may_be_converted_to_TypeScript_type.code]; |
| 28 | +registerCodeFix({ |
| 29 | + fixIds: [fixId], |
| 30 | + errorCodes, |
| 31 | + getCodeActions(context) { |
| 32 | + const node = getTokenAtPosition( |
| 33 | + context.sourceFile, |
| 34 | + context.span.start |
| 35 | + ); |
| 36 | + if (!node) return; |
| 37 | + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, node, context.sourceFile)); |
| 38 | + |
| 39 | + if (changes.length > 0) { |
| 40 | + return [ |
| 41 | + createCodeFixAction( |
| 42 | + fixId, |
| 43 | + changes, |
| 44 | + Diagnostics.Convert_typedef_to_TypeScript_type, |
| 45 | + fixId, |
| 46 | + Diagnostics.Convert_all_typedef_to_TypeScript_types, |
| 47 | + ), |
| 48 | + ]; |
| 49 | + } |
| 50 | + }, |
| 51 | + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => { |
| 52 | + const node = getTokenAtPosition(diag.file, diag.start); |
| 53 | + if (node) doChange(changes, node, diag.file); |
| 54 | + }) |
| 55 | +}); |
| 56 | + |
| 57 | +function doChange(changes: textChanges.ChangeTracker, node: Node, sourceFile: SourceFile) { |
| 58 | + if (isJSDocTypedefTag(node)) { |
| 59 | + fixSingleTypeDef(changes, node, sourceFile); |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +function fixSingleTypeDef( |
| 64 | + changes: textChanges.ChangeTracker, |
| 65 | + typeDefNode: JSDocTypedefTag | undefined, |
| 66 | + sourceFile: SourceFile, |
| 67 | +) { |
| 68 | + if (!typeDefNode) return; |
| 69 | + |
| 70 | + const declaration = createDeclaration(typeDefNode); |
| 71 | + if (!declaration) return; |
| 72 | + |
| 73 | + const comment = typeDefNode.parent; |
| 74 | + |
| 75 | + changes.replaceNode( |
| 76 | + sourceFile, |
| 77 | + comment, |
| 78 | + declaration |
| 79 | + ); |
| 80 | +} |
| 81 | + |
| 82 | +function createDeclaration(tag: JSDocTypedefTag): InterfaceDeclaration | TypeAliasDeclaration | undefined { |
| 83 | + const { typeExpression } = tag; |
| 84 | + if (!typeExpression) return; |
| 85 | + const typeName = tag.name?.getText(); |
| 86 | + if (!typeName) return; |
| 87 | + |
| 88 | + // For use case @typedef {object}Foo @property{bar}number |
| 89 | + // But object type can be nested, meaning the value in the k/v pair can be object itself |
| 90 | + if (typeExpression.kind === SyntaxKind.JSDocTypeLiteral) { |
| 91 | + return createInterfaceForTypeLiteral(typeName, typeExpression); |
| 92 | + } |
| 93 | + // for use case @typedef {(number|string|undefined)} Foo or @typedef {number|string|undefined} Foo |
| 94 | + // In this case, we reach the leaf node of AST. |
| 95 | + if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) { |
| 96 | + return createTypeAliasForTypeExpression(typeName, typeExpression); |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +function createInterfaceForTypeLiteral( |
| 101 | + typeName: string, |
| 102 | + typeLiteral: JSDocTypeLiteral |
| 103 | +): InterfaceDeclaration | undefined { |
| 104 | + const propertySignatures = createSignatureFromTypeLiteral(typeLiteral); |
| 105 | + if (!some(propertySignatures)) return; |
| 106 | + const interfaceDeclaration = factory.createInterfaceDeclaration( |
| 107 | + /*modifiers*/ undefined, |
| 108 | + typeName, |
| 109 | + /*typeParameters*/ undefined, |
| 110 | + /*heritageClauses*/ undefined, |
| 111 | + propertySignatures, |
| 112 | + ); |
| 113 | + return interfaceDeclaration; |
| 114 | +} |
| 115 | + |
| 116 | +function createTypeAliasForTypeExpression( |
| 117 | + typeName: string, |
| 118 | + typeExpression: JSDocTypeExpression |
| 119 | +): TypeAliasDeclaration | undefined { |
| 120 | + const typeReference = getSynthesizedDeepClone(typeExpression.type); |
| 121 | + if (!typeReference) return; |
| 122 | + const declaration = factory.createTypeAliasDeclaration( |
| 123 | + /*modifiers*/ undefined, |
| 124 | + factory.createIdentifier(typeName), |
| 125 | + /*typeParameters*/ undefined, |
| 126 | + typeReference |
| 127 | + ); |
| 128 | + return declaration; |
| 129 | +} |
| 130 | + |
| 131 | +function createSignatureFromTypeLiteral(typeLiteral: JSDocTypeLiteral): PropertySignature[] | undefined { |
| 132 | + const propertyTags = typeLiteral.jsDocPropertyTags; |
| 133 | + if (!some(propertyTags)) return; |
| 134 | + |
| 135 | + const getSignature = (tag: JSDocPropertyLikeTag) => { |
| 136 | + const name = getPropertyName(tag); |
| 137 | + const type = tag.typeExpression?.type; |
| 138 | + const isOptional = tag.isBracketed; |
| 139 | + let typeReference; |
| 140 | + |
| 141 | + // Recursively handle nested object type |
| 142 | + if (type && isJSDocTypeLiteral(type)) { |
| 143 | + const signatures = createSignatureFromTypeLiteral(type); |
| 144 | + typeReference = factory.createTypeLiteralNode(signatures); |
| 145 | + } |
| 146 | + // Leaf node, where type.kind === SyntaxKind.JSDocTypeExpression |
| 147 | + else if (type) { |
| 148 | + typeReference = getSynthesizedDeepClone(type); |
| 149 | + } |
| 150 | + |
| 151 | + if (typeReference && name) { |
| 152 | + const questionToken = isOptional ? factory.createToken(SyntaxKind.QuestionToken) : undefined; |
| 153 | + const prop = factory.createPropertySignature( |
| 154 | + /*modifiers*/ undefined, |
| 155 | + name, |
| 156 | + questionToken, |
| 157 | + typeReference |
| 158 | + ); |
| 159 | + |
| 160 | + return prop; |
| 161 | + } |
| 162 | + }; |
| 163 | + |
| 164 | + const props = mapDefined(propertyTags, getSignature); |
| 165 | + return props; |
| 166 | +} |
| 167 | + |
| 168 | +function getPropertyName(tag: JSDocPropertyLikeTag): string | undefined { |
| 169 | + return tag.name.kind === SyntaxKind.Identifier ? tag.name.text : tag.name.right.text; |
| 170 | +} |
| 171 | + |
| 172 | +/** @internal */ |
| 173 | +export function getJSDocTypedefNode(node: Node): JSDocTypedefTag | undefined { |
| 174 | + if (hasJSDocNodes(node)) { |
| 175 | + return forEach(node.jsDoc, (node) => node.tags?.find(isJSDocTypedefTag)); |
| 176 | + } |
| 177 | + return undefined; |
| 178 | +} |
0 commit comments