1
+ import {
2
+ Diagnostics ,
3
+ factory ,
4
+ flatMap ,
5
+ getTokenAtPosition ,
6
+ HasJSDoc ,
7
+ hasJSDocNodes ,
8
+ InterfaceDeclaration ,
9
+ isInJSDoc ,
10
+ isJSDocTypedefTag ,
11
+ JSDocPropertyTag ,
12
+ JSDocTypedefTag ,
13
+ JSDocTypeExpression ,
14
+ JSDocTypeLiteral ,
15
+ Node ,
16
+ ParenthesizedTypeNode ,
17
+ PropertySignature ,
18
+ reduceLeft ,
19
+ some ,
20
+ SourceFile ,
21
+ SyntaxKind ,
22
+ textChanges ,
23
+ TypeAliasDeclaration ,
24
+ TypeNode ,
25
+ TypeNodeSyntaxKind ,
26
+ UnionTypeNode ,
27
+ } from "../_namespaces/ts" ;
28
+ import { codeFixAll , createCodeFixAction , registerCodeFix } from "../_namespaces/ts.codefix" ;
29
+
30
+ const fixId = "convertTypedefToType" ;
31
+ const errorCodes = [ Diagnostics . JSDoc_typedef_may_be_converted_to_type . code ] ;
32
+ registerCodeFix ( {
33
+ fixIds : [ fixId ] ,
34
+ errorCodes,
35
+ getCodeActions ( context ) {
36
+ const node = getTokenAtPosition (
37
+ context . sourceFile ,
38
+ context . span . start
39
+ ) ;
40
+ if ( ! node ) return ;
41
+ const changes = textChanges . ChangeTracker . with ( context , t => doChange ( t , node , context . sourceFile ) ) ;
42
+
43
+ if ( changes . length > 0 ) {
44
+ return [
45
+ createCodeFixAction (
46
+ fixId ,
47
+ changes ,
48
+ Diagnostics . JSDoc_typedef_may_be_converted_to_type ,
49
+ fixId ,
50
+ Diagnostics . JSDoc_typedefs_may_be_converted_to_types
51
+ ) ,
52
+ ] ;
53
+ }
54
+ } ,
55
+ getAllCodeActions : context => codeFixAll ( context , errorCodes , ( changes , diag ) => {
56
+ const node = getTokenAtPosition ( diag . file , diag . start ) ;
57
+ if ( node ) doChange ( changes , node , diag . file ) ;
58
+ } )
59
+ } ) ;
60
+
61
+ function doChange ( changes : textChanges . ChangeTracker , node : Node , sourceFile : SourceFile ) {
62
+ if ( containsTypeDefTag ( node ) ) {
63
+ fixSingleTypeDef ( changes , node , sourceFile ) ;
64
+ }
65
+ }
66
+
67
+ function fixSingleTypeDef (
68
+ changes : textChanges . ChangeTracker ,
69
+ typeDefNode : JSDocTypedefTag | undefined ,
70
+ sourceFile : SourceFile ,
71
+ ) {
72
+ if ( ! typeDefNode ) return ;
73
+
74
+ const declaration = createDeclaration ( typeDefNode ) ;
75
+ if ( ! declaration ) return ;
76
+
77
+ const comment = typeDefNode . parent ;
78
+
79
+ changes . replaceNode (
80
+ sourceFile ,
81
+ comment ,
82
+ declaration
83
+ ) ;
84
+ }
85
+
86
+ function createDeclaration ( tag : JSDocTypedefTag ) : InterfaceDeclaration | TypeAliasDeclaration | undefined {
87
+ const { typeExpression } = tag ;
88
+ if ( ! typeExpression ) return ;
89
+ const typeName = tag . name ?. getFullText ( ) . trim ( ) ;
90
+ if ( ! typeName ) return ;
91
+
92
+ // For use case @typedef {object }Foo @property {bar }number
93
+ // But object type can be nested, meaning the value in the k/v pair can be object itself
94
+ if ( typeExpression . kind === SyntaxKind . JSDocTypeLiteral ) {
95
+ return createInterfaceForTypeLiteral ( typeName , typeExpression ) ;
96
+ }
97
+ // for use case @typedef {(number|string|undefined) } Foo or @typedef {number|string|undefined} Foo
98
+ // In this case, we reach the leaf node of AST.
99
+ // Here typeExpression.type is a TypeNode, e.g. a UnionType or a primitive type, such as "number".
100
+ if ( typeExpression . kind === SyntaxKind . JSDocTypeExpression ) {
101
+ return createTypeAliasForTypeExpression ( typeName , typeExpression ) ;
102
+ }
103
+ }
104
+
105
+ function createInterfaceForTypeLiteral (
106
+ typeName : string ,
107
+ typeLiteral : JSDocTypeLiteral
108
+ ) : InterfaceDeclaration | undefined {
109
+ const propertySignatures = createSignatureFromTypeLiteral ( typeLiteral ) ;
110
+ if ( ! propertySignatures || propertySignatures . length === 0 ) return ;
111
+ const interfaceDeclaration = factory . createInterfaceDeclaration (
112
+ [ ] ,
113
+ typeName ,
114
+ [ ] ,
115
+ [ ] ,
116
+ propertySignatures ,
117
+ ) ;
118
+ return interfaceDeclaration ;
119
+ }
120
+
121
+ function createTypeAliasForTypeExpression (
122
+ typeName : string ,
123
+ typeExpression : JSDocTypeExpression
124
+ ) : TypeAliasDeclaration | undefined {
125
+ const typeReference = createTypeReference ( typeExpression . type ) ;
126
+ if ( ! typeReference ) return ;
127
+ const declaration = factory . createTypeAliasDeclaration (
128
+ [ ] ,
129
+ factory . createIdentifier ( typeName ) ,
130
+ [ ] ,
131
+ typeReference
132
+ ) ;
133
+ return declaration ;
134
+ }
135
+
136
+ function createSignatureFromTypeLiteral ( typeLiteral : JSDocTypeLiteral ) : PropertySignature [ ] | undefined {
137
+ const propertyTags = typeLiteral . jsDocPropertyTags ;
138
+ if ( ! propertyTags || propertyTags . length === 0 ) return ;
139
+
140
+ const getSignatures = ( signatures : PropertySignature [ ] , tag : JSDocPropertyTag ) => {
141
+ const name = getPropertyName ( tag ) ;
142
+ const type = tag . typeExpression ?. type ;
143
+ let typeReference ;
144
+
145
+ // Recursively handle nested object type
146
+ if ( type && type . kind === SyntaxKind . JSDocTypeLiteral ) {
147
+ const signatures = createSignatureFromTypeLiteral ( type as JSDocTypeLiteral ) ;
148
+ typeReference = factory . createTypeLiteralNode ( signatures ) ;
149
+ }
150
+ // Leaf node, where type.kind === SyntaxKind.JSDocTypeExpression
151
+ else if ( type ) {
152
+ typeReference = createTypeReference ( type ) ;
153
+ }
154
+ if ( typeReference && name ) {
155
+ const prop = factory . createPropertySignature (
156
+ [ ] ,
157
+ name ,
158
+ // eslint-disable-next-line local/boolean-trivia
159
+ undefined ,
160
+ typeReference
161
+ ) ;
162
+ return [ ...signatures , prop ] ;
163
+ }
164
+ } ;
165
+
166
+ const props = reduceLeft ( propertyTags , getSignatures , [ ] ) ;
167
+ return props ;
168
+ }
169
+
170
+ function getPropertyName ( tag : JSDocPropertyTag ) : string | undefined {
171
+ const { name } = tag ;
172
+ if ( ! name ) return ;
173
+
174
+ let propertyName ;
175
+ // for "@property {string} parent.child" or "@property {string} parent.child.grandchild" in nested object type
176
+ // We'll get "child" in the first example or "grandchild" in the second example as the prop name
177
+ if ( name . kind === SyntaxKind . QualifiedName ) {
178
+ propertyName = name . right . getFullText ( ) . trim ( ) ;
179
+ }
180
+ else {
181
+ propertyName = tag . name . getFullText ( ) . trim ( ) ;
182
+ }
183
+ return propertyName ;
184
+ }
185
+
186
+ // Create TypeReferenceNode when we reach the leaf node of AST
187
+ function createTypeReference ( type : TypeNode ) : TypeNode | undefined {
188
+ let typeReference ;
189
+ if ( type . kind === SyntaxKind . ParenthesizedType ) {
190
+ type = ( type as ParenthesizedTypeNode ) . type ;
191
+ }
192
+ // Create TypeReferenceNode for UnionType
193
+ if ( type . kind === SyntaxKind . UnionType ) {
194
+ const elements = ( type as UnionTypeNode ) . types ;
195
+ const nodes = reduceLeft (
196
+ elements ,
197
+ ( nodeArray , element ) => {
198
+ const node = transformUnionTypeKeyword ( element . kind ) ;
199
+ if ( node ) return [ ...nodeArray , node ] ;
200
+ } ,
201
+ [ ]
202
+ ) ;
203
+
204
+ if ( ! nodes ) return ;
205
+ typeReference = factory . createUnionTypeNode ( nodes ) ;
206
+ }
207
+ //Create TypeReferenceNode for primitive types
208
+ else {
209
+ typeReference = transformUnionTypeKeyword ( type . kind ) ;
210
+ }
211
+ return typeReference ;
212
+ }
213
+
214
+ function transformUnionTypeKeyword ( keyword : TypeNodeSyntaxKind ) : TypeNode | undefined {
215
+ switch ( keyword ) {
216
+ case SyntaxKind . NumberKeyword :
217
+ return factory . createTypeReferenceNode ( "number" ) ;
218
+ case SyntaxKind . StringKeyword :
219
+ return factory . createTypeReferenceNode ( "string" ) ;
220
+ case SyntaxKind . UndefinedKeyword :
221
+ return factory . createTypeReferenceNode ( "undefined" ) ;
222
+ case SyntaxKind . ObjectKeyword :
223
+ return factory . createTypeReferenceNode ( "object" ) ;
224
+ case SyntaxKind . VoidKeyword :
225
+ return factory . createTypeReferenceNode ( "void" ) ;
226
+ default :
227
+ return ;
228
+ }
229
+ }
230
+
231
+ /** @internal */
232
+ export function containsJSDocTypedef ( node : Node ) : node is HasJSDoc {
233
+ return hasJSDocNodes ( node ) && some ( node . jsDoc , node => some ( node . tags , tag => isJSDocTypedefTag ( tag ) ) ) ;
234
+ }
235
+
236
+ /** @internal */
237
+ export function getJSDocTypedefNode ( node : HasJSDoc ) : JSDocTypedefTag {
238
+ const jsDocNodes = node . jsDoc || [ ] ;
239
+
240
+ return flatMap ( jsDocNodes , ( node ) => {
241
+ const tags = node . tags || [ ] ;
242
+ return tags . filter ( ( tag ) => isJSDocTypedefTag ( tag ) ) ;
243
+ } ) [ 0 ] as unknown as JSDocTypedefTag ;
244
+ }
245
+
246
+ /** @internal */
247
+ export function containsTypeDefTag ( node : Node ) : node is JSDocTypedefTag {
248
+ return isInJSDoc ( node ) && isJSDocTypedefTag ( node ) ;
249
+ }
0 commit comments