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 ( tagNode : JSDocTypedefTag ) : InterfaceDeclaration | TypeAliasDeclaration | undefined {
67
+ const typeName = tagNode . name ?. getFullText ( ) . trim ( ) ;
68
+ const typeExpression = tagNode . typeExpression ;
69
+ if ( ! typeName || ! typeExpression ) return ;
70
+
71
+ // for use case @typedef {object }Foo @property {bar }number
72
+ if ( typeExpression . kind === SyntaxKind . JSDocTypeLiteral ) {
73
+ return createDeclarationFromTypeLiteral ( typeName , typeExpression ) ;
74
+ }
75
+ // for use case @typedef {(number|string|undefined) } Foo or @typedef {number|string|undefined} Foo
76
+ else if ( typeExpression . kind === SyntaxKind . JSDocTypeExpression ) {
77
+ return createDeclarationFromTypeExpression ( typeName , typeExpression ) ;
78
+ }
79
+ }
80
+
81
+ function createDeclarationFromTypeLiteral ( typeName : string , typeExpression : JSDocTypeLiteral ) : InterfaceDeclaration | undefined {
82
+ if ( ! typeName || ! typeExpression ) return ;
83
+ const propertyTags = typeExpression . jsDocPropertyTags ;
84
+ if ( ! propertyTags ) return ;
85
+
86
+ const propertySignatures = createPropertySignatures ( propertyTags as JSDocPropertyTag [ ] ) ;
87
+
88
+ if ( ! propertySignatures || propertySignatures . length === 0 ) return ;
89
+ const interfaceDeclaration = factory . createInterfaceDeclaration (
90
+ [ ] ,
91
+ typeName ,
92
+ [ ] ,
93
+ [ ] ,
94
+ propertySignatures ,
95
+ ) ;
96
+ return interfaceDeclaration ;
97
+ }
98
+
99
+ function createDeclarationFromTypeExpression ( typeName : string , typeExpression : JSDocTypeExpression ) : TypeAliasDeclaration | undefined {
100
+ if ( ! typeName || ! typeExpression ) return ;
101
+
102
+ let type = typeExpression . type ;
103
+ if ( type . kind === SyntaxKind . ParenthesizedType ) {
104
+ type = ( type as ParenthesizedTypeNode ) . type ;
105
+ }
106
+
107
+ if ( type . kind === SyntaxKind . UnionType ) {
108
+ return createTypeAliasForUnionType ( type as UnionTypeNode , typeName ) ;
109
+ }
110
+ }
111
+
112
+ function createPropertySignatures ( tags : JSDocPropertyTag [ ] ) : PropertySignature [ ] | undefined {
113
+ const props = tags . reduce ( ( signatures : PropertySignature [ ] , tag : JSDocPropertyTag ) => {
114
+ const propertyName = tag . name . getFullText ( ) . trim ( ) ;
115
+ const propertyType = ( tag . typeExpression ?. type ) ?. getFullText ( ) . trim ( ) ;
116
+ if ( propertyName && propertyType ) {
117
+ const prop = factory . createPropertySignature (
118
+ [ ] ,
119
+ propertyName ,
120
+ // eslint-disable-next-line local/boolean-trivia
121
+ undefined ,
122
+ factory . createTypeReferenceNode ( propertyType )
123
+ ) ;
124
+ return [ ...signatures , prop ] ;
125
+ }
126
+
127
+ } , [ ] ) ;
128
+ return props ;
129
+ }
130
+
131
+ function createTypeAliasForUnionType ( type : UnionTypeNode , typeName : string ) : TypeAliasDeclaration | undefined {
132
+ const elements = type . types ;
133
+ const nodes = elements . reduce ( ( nodeArray , element ) => {
134
+ const node = transformUnionTypeKeyword ( element . kind ) ;
135
+ if ( node ) return [ ...nodeArray , node ] ;
136
+ } , [ ] ) ;
137
+ if ( ! nodes ) return ;
138
+ const typeReference = factory . createUnionTypeNode ( nodes ) ;
139
+ const unionDeclaration = factory . createTypeAliasDeclaration (
140
+ [ ] ,
141
+ factory . createIdentifier ( typeName ) ,
142
+ [ ] ,
143
+ typeReference
144
+ ) ;
145
+ return unionDeclaration ;
146
+ }
147
+
148
+ function transformUnionTypeKeyword ( keyword : TypeNodeSyntaxKind ) : TypeNode | undefined {
149
+ switch ( keyword ) {
150
+ case SyntaxKind . NumberKeyword :
151
+ return factory . createTypeReferenceNode ( "number" ) ;
152
+ case SyntaxKind . StringKeyword :
153
+ return factory . createTypeReferenceNode ( "string" ) ;
154
+ case SyntaxKind . UndefinedKeyword :
155
+ return factory . createTypeReferenceNode ( "undefined" ) ;
156
+ case SyntaxKind . ObjectKeyword :
157
+ return factory . createTypeReferenceNode ( "object" ) ;
158
+ case SyntaxKind . VoidKeyword :
159
+ return factory . createTypeReferenceNode ( "void" ) ;
160
+ default :
161
+ return ;
162
+ }
163
+ }
164
+
165
+ export function _containsJSDocTypedef ( node : Node ) : node is HasJSDoc {
166
+ if ( hasJSDocNodes ( node ) ) {
167
+ const jsDocNodes = node . jsDoc || [ ] ;
168
+ return jsDocNodes . some ( ( node ) => {
169
+ const tags = node . tags || [ ] ;
170
+ return tags . some ( ( tag ) => isJSDocTypedefTag ( tag ) ) ;
171
+ } ) ;
172
+ }
173
+ return false ;
174
+ }
175
+
176
+ export function getJSDocTypedefNode ( node : HasJSDoc ) : JSDocTypedefTag {
177
+ const jsDocNodes = node . jsDoc || [ ] ;
178
+
179
+ return flatMap ( jsDocNodes , ( node ) => {
180
+ const tags = node . tags || [ ] ;
181
+ return tags . filter ( ( tag ) => isJSDocTypedefTag ( tag ) ) ;
182
+ } ) [ 0 ] as unknown as JSDocTypedefTag ;
183
+ }
184
+
185
+ export function containsTypeDefTag ( node : Node ) : node is JSDocTypedefTag {
186
+ return isInJSDoc ( node ) && isJSDocTypedefTag ( node ) ;
187
+ }
0 commit comments