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