@@ -53,6 +53,10 @@ export class TypeProcessor {
5353 this . seenTypes = new Map ( ) ;
5454 /** @type {string[] } Collected Swift code lines */
5555 this . swiftLines = [ ] ;
56+ /** @type {Set<string> } */
57+ this . emittedEnumNames = new Set ( ) ;
58+ /** @type {Set<string> } */
59+ this . emittedStructuredTypeNames = new Set ( ) ;
5660
5761 /** @type {Set<string> } */
5862 this . visitedDeclarationKeys = new Set ( ) ;
@@ -92,6 +96,10 @@ export class TypeProcessor {
9296
9397 for ( const [ type , node ] of this . seenTypes ) {
9498 this . seenTypes . delete ( type ) ;
99+ if ( this . isEnumType ( type ) ) {
100+ this . visitEnumType ( type , node ) ;
101+ continue ;
102+ }
95103 const typeString = this . checker . typeToString ( type ) ;
96104 const members = type . getProperties ( ) ;
97105 if ( members ) {
@@ -119,6 +127,8 @@ export class TypeProcessor {
119127 this . visitFunctionDeclaration ( node ) ;
120128 } else if ( ts . isClassDeclaration ( node ) ) {
121129 this . visitClassDecl ( node ) ;
130+ } else if ( ts . isEnumDeclaration ( node ) ) {
131+ this . visitEnumDeclaration ( node ) ;
122132 } else if ( ts . isExportDeclaration ( node ) ) {
123133 this . visitExportDeclaration ( node ) ;
124134 }
@@ -185,6 +195,174 @@ export class TypeProcessor {
185195 }
186196 }
187197
198+ /**
199+ * @param {ts.Type } type
200+ * @returns {boolean }
201+ * @private
202+ */
203+ isEnumType ( type ) {
204+ const symbol = type . getSymbol ( ) ?? type . aliasSymbol ;
205+ if ( ! symbol ) return false ;
206+ return ( symbol . flags & ts . SymbolFlags . Enum ) !== 0 ;
207+ }
208+
209+ /**
210+ * @param {ts.EnumDeclaration } node
211+ * @private
212+ */
213+ visitEnumDeclaration ( node ) {
214+ const name = node . name ?. text ;
215+ if ( ! name ) return ;
216+ this . emitEnumFromDeclaration ( name , node , node ) ;
217+ }
218+
219+ /**
220+ * @param {ts.Type } type
221+ * @param {ts.Node } node
222+ * @private
223+ */
224+ visitEnumType ( type , node ) {
225+ const symbol = type . getSymbol ( ) ?? type . aliasSymbol ;
226+ const name = symbol ?. name ;
227+ if ( ! name ) return ;
228+ const decl = symbol ?. getDeclarations ( ) ?. find ( d => ts . isEnumDeclaration ( d ) ) ;
229+ if ( ! decl || ! ts . isEnumDeclaration ( decl ) ) {
230+ this . diagnosticEngine . print ( "warning" , `Enum declaration not found for type: ${ name } ` , node ) ;
231+ return ;
232+ }
233+ this . emitEnumFromDeclaration ( name , decl , node ) ;
234+ }
235+
236+ /**
237+ * @param {string } enumName
238+ * @param {ts.EnumDeclaration } decl
239+ * @param {ts.Node } diagnosticNode
240+ * @private
241+ */
242+ emitEnumFromDeclaration ( enumName , decl , diagnosticNode ) {
243+ if ( this . emittedEnumNames . has ( enumName ) ) return ;
244+ this . emittedEnumNames . add ( enumName ) ;
245+
246+ const members = decl . members ?? [ ] ;
247+ if ( members . length === 0 ) {
248+ this . diagnosticEngine . print ( "warning" , `Empty enum is not supported: ${ enumName } ` , diagnosticNode ) ;
249+ this . swiftLines . push ( `typealias ${ this . renderIdentifier ( enumName ) } = String` ) ;
250+ this . swiftLines . push ( "" ) ;
251+ return ;
252+ }
253+
254+ /**
255+ * Convert a TypeScript enum member name into a valid Swift identifier.
256+ * @param {string } name
257+ * @returns {string }
258+ */
259+ const toSwiftCaseName = ( name ) => {
260+ const swiftIdentifierRegex = / ^ [ _ \p{ ID_Start} ] [ \p{ ID_Continue} \u{200C} \u{200D} ] * $ / u;
261+ let result = "" ;
262+ for ( const ch of name ) {
263+ const isIdentifierChar = / ^ [ _ \p{ ID_Continue} \u{200C} \u{200D} ] $ / u. test ( ch ) ;
264+ result += isIdentifierChar ? ch : "_" ;
265+ }
266+ if ( ! result ) result = "_case" ;
267+ if ( ! / ^ [ _ \p{ ID_Start} ] $ / u. test ( result [ 0 ] ) ) {
268+ result = "_" + result ;
269+ }
270+ if ( ! swiftIdentifierRegex . test ( result ) ) {
271+ result = result . replace ( / [ ^ _ \p{ ID_Continue} \u{200C} \u{200D} ] / gu, "_" ) ;
272+ if ( ! result ) result = "_case" ;
273+ if ( ! / ^ [ _ \p{ ID_Start} ] $ / u. test ( result [ 0 ] ) ) {
274+ result = "_" + result ;
275+ }
276+ }
277+ if ( isSwiftKeyword ( result ) ) {
278+ result = result + "_" ;
279+ }
280+ return result ;
281+ } ;
282+
283+ /** @type {{ name: string, raw: string }[] } */
284+ const stringMembers = [ ] ;
285+ /** @type {{ name: string, raw: number }[] } */
286+ const intMembers = [ ] ;
287+ let canBeStringEnum = true ;
288+ let canBeIntEnum = true ;
289+ let nextAutoValue = 0 ;
290+
291+ for ( const member of members ) {
292+ const rawMemberName = member . name . getText ( ) ;
293+ const unquotedName = rawMemberName . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) ;
294+ const swiftCaseNameBase = toSwiftCaseName ( unquotedName ) ;
295+
296+ if ( member . initializer && ts . isStringLiteral ( member . initializer ) ) {
297+ stringMembers . push ( { name : swiftCaseNameBase , raw : member . initializer . text } ) ;
298+ canBeIntEnum = false ;
299+ continue ;
300+ }
301+
302+ if ( member . initializer && ts . isNumericLiteral ( member . initializer ) ) {
303+ const rawValue = Number ( member . initializer . text ) ;
304+ if ( ! Number . isInteger ( rawValue ) ) {
305+ canBeIntEnum = false ;
306+ } else {
307+ intMembers . push ( { name : swiftCaseNameBase , raw : rawValue } ) ;
308+ nextAutoValue = rawValue + 1 ;
309+ canBeStringEnum = false ;
310+ continue ;
311+ }
312+ }
313+
314+ if ( ! member . initializer ) {
315+ intMembers . push ( { name : swiftCaseNameBase , raw : nextAutoValue } ) ;
316+ nextAutoValue += 1 ;
317+ canBeStringEnum = false ;
318+ continue ;
319+ }
320+
321+ canBeStringEnum = false ;
322+ canBeIntEnum = false ;
323+ }
324+ const swiftEnumName = this . renderIdentifier ( enumName ) ;
325+ const dedupeNames = ( items ) => {
326+ const seen = new Map ( ) ;
327+ return items . map ( item => {
328+ const count = seen . get ( item . name ) ?? 0 ;
329+ seen . set ( item . name , count + 1 ) ;
330+ if ( count === 0 ) return item ;
331+ return { ...item , name : `${ item . name } _${ count + 1 } ` } ;
332+ } ) ;
333+ } ;
334+
335+ if ( canBeStringEnum && stringMembers . length > 0 ) {
336+ this . swiftLines . push ( `enum ${ swiftEnumName } : String {` ) ;
337+ for ( const { name, raw } of dedupeNames ( stringMembers ) ) {
338+ this . swiftLines . push ( ` case ${ this . renderIdentifier ( name ) } = "${ raw . replaceAll ( "\"" , "\\\\\"" ) } "` ) ;
339+ }
340+ this . swiftLines . push ( "}" ) ;
341+ this . swiftLines . push ( `extension ${ swiftEnumName } : _BridgedSwiftEnumNoPayload {}` ) ;
342+ this . swiftLines . push ( "" ) ;
343+ return ;
344+ }
345+
346+ if ( canBeIntEnum && intMembers . length > 0 ) {
347+ this . swiftLines . push ( `enum ${ swiftEnumName } : Int {` ) ;
348+ for ( const { name, raw } of dedupeNames ( intMembers ) ) {
349+ this . swiftLines . push ( ` case ${ this . renderIdentifier ( name ) } = ${ raw } ` ) ;
350+ }
351+ this . swiftLines . push ( "}" ) ;
352+ this . swiftLines . push ( `extension ${ swiftEnumName } : _BridgedSwiftEnumNoPayload {}` ) ;
353+ this . swiftLines . push ( "" ) ;
354+ return ;
355+ }
356+
357+ this . diagnosticEngine . print (
358+ "warning" ,
359+ `Unsupported enum (only string or int enums are supported): ${ enumName } ` ,
360+ diagnosticNode
361+ ) ;
362+ this . swiftLines . push ( `typealias ${ swiftEnumName } = String` ) ;
363+ this . swiftLines . push ( "" ) ;
364+ }
365+
188366 /**
189367 * Visit a function declaration and render Swift code
190368 * @param {ts.FunctionDeclaration } node - The function node
@@ -332,6 +510,9 @@ export class TypeProcessor {
332510 * @private
333511 */
334512 visitStructuredType ( name , members ) {
513+ if ( this . emittedStructuredTypeNames . has ( name ) ) return ;
514+ this . emittedStructuredTypeNames . add ( name ) ;
515+
335516 const typeName = this . renderIdentifier ( name ) ;
336517 this . swiftLines . push ( `@JSClass struct ${ typeName } {` ) ;
337518
@@ -415,6 +596,13 @@ export class TypeProcessor {
415596 return typeMap [ typeString ] ;
416597 }
417598
599+ const symbol = type . getSymbol ( ) ?? type . aliasSymbol ;
600+ if ( symbol && ( symbol . flags & ts . SymbolFlags . Enum ) !== 0 ) {
601+ const typeName = symbol . name ;
602+ this . seenTypes . set ( type , node ) ;
603+ return this . renderIdentifier ( typeName ) ;
604+ }
605+
418606 if ( this . checker . isArrayType ( type ) || this . checker . isTupleType ( type ) || type . getCallSignatures ( ) . length > 0 ) {
419607 return "JSObject" ;
420608 }
0 commit comments