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_TypeScript_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_TypeScript_type ,
49+ fixId ,
50+ Diagnostics . JSDoc_typedefs_may_be_converted_to_TypeScript_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