@@ -157,10 +157,9 @@ export class TypeProcessor {
157157 this . visitEnumType ( type , node ) ;
158158 continue ;
159159 }
160- const typeString = this . checker . typeToString ( type ) ;
161160 const members = type . getProperties ( ) ;
162161 if ( members ) {
163- this . visitStructuredType ( typeString , members ) ;
162+ this . visitStructuredType ( type , node , members ) ;
164163 }
165164 }
166165 } ) ;
@@ -284,6 +283,7 @@ export class TypeProcessor {
284283 if ( fromArg ) args . push ( fromArg ) ;
285284 const annotation = this . renderMacroAnnotation ( "JSGetter" , args ) ;
286285
286+ this . emitDocComment ( decl , { indent : "" } ) ;
287287 this . swiftLines . push ( `${ annotation } var ${ swiftVarName } : ${ swiftType } ` ) ;
288288 this . swiftLines . push ( "" ) ;
289289 }
@@ -338,6 +338,7 @@ export class TypeProcessor {
338338 this . emittedEnumNames . add ( enumName ) ;
339339
340340 const members = decl . members ?? [ ] ;
341+ this . emitDocComment ( decl , { indent : "" } ) ;
341342 if ( members . length === 0 ) {
342343 this . diagnosticEngine . print ( "warning" , `Empty enum is not supported: ${ enumName } ` , diagnosticNode ) ;
343344 this . swiftLines . push ( `typealias ${ this . renderIdentifier ( enumName ) } = String` ) ;
@@ -448,54 +449,194 @@ export class TypeProcessor {
448449 const signature = this . checker . getSignatureFromDeclaration ( node ) ;
449450 if ( ! signature ) return ;
450451
451- const params = this . renderParameters ( signature . getParameters ( ) , node ) ;
452+ const parameters = signature . getParameters ( ) ;
453+ const parameterNameMap = this . buildParameterNameMap ( parameters ) ;
454+ const params = this . renderParameters ( parameters , node ) ;
452455 const returnType = this . visitType ( signature . getReturnType ( ) , node ) ;
453456 const effects = this . renderEffects ( { isAsync : false } ) ;
454457 const swiftFuncName = this . renderIdentifier ( swiftName ) ;
455458
459+ this . emitDocComment ( node , { parameterNameMap } ) ;
456460 this . swiftLines . push ( `${ annotation } func ${ swiftFuncName } (${ params } ) ${ effects } -> ${ returnType } ` ) ;
457461 this . swiftLines . push ( "" ) ;
458462 }
459463
460464 /**
461- * Get the full JSDoc text from a node
462- * @param {ts.Node } node - The node to get the JSDoc text from
463- * @returns {string | undefined } The full JSDoc text
465+ * Convert a JSDoc comment node content to plain text.
466+ * @param {string | ts.NodeArray<ts.JSDocComment> | undefined } comment
467+ * @returns {string }
468+ * @private
464469 */
465- getFullJSDocText ( node ) {
466- const docs = ts . getJSDocCommentsAndTags ( node ) ;
467- const parts = [ ] ;
468- for ( const doc of docs ) {
469- if ( ts . isJSDoc ( doc ) ) {
470- parts . push ( doc . comment ?? "" ) ;
470+ renderJSDocText ( comment ) {
471+ if ( ! comment ) return "" ;
472+ if ( typeof comment === "string" ) return comment ;
473+ let result = "" ;
474+ for ( const part of comment ) {
475+ if ( typeof part === "string" ) {
476+ result += part ;
477+ continue ;
478+ }
479+ // JSDocText/JSDocLink both have a `text` field
480+ // https://github.com/microsoft/TypeScript/blob/main/src/compiler/types.ts
481+ // @ts -ignore
482+ if ( typeof part . text === "string" ) {
483+ // @ts -ignore
484+ result += part . text ;
485+ continue ;
486+ }
487+ if ( typeof part . getText === "function" ) {
488+ result += part . getText ( ) ;
471489 }
472490 }
473- if ( parts . length === 0 ) return undefined ;
474- return parts . join ( "\n" ) ;
491+ return result ;
475492 }
476493
477- /** @returns {string } */
478- renderDefaultJSImportFromArgument ( ) {
479- if ( this . defaultImportFromGlobal ) return "from: .global" ;
480- return "" ;
494+ /**
495+ * Split documentation text into lines suitable for DocC rendering.
496+ * @param {string } text
497+ * @returns {string[] }
498+ * @private
499+ */
500+ splitDocumentationText ( text ) {
501+ if ( ! text ) return [ ] ;
502+ return text . split ( / \r ? \n / ) . map ( line => line . trimEnd ( ) ) ;
481503 }
482504
483505 /**
484- * Render constructor parameters
485- * @param {ts.ConstructorDeclaration } node
486- * @returns {string } Rendered parameters
506+ * @param {string[] } lines
507+ * @returns {boolean }
487508 * @private
488509 */
489- renderConstructorParameters ( node ) {
490- const signature = this . checker . getSignatureFromDeclaration ( node ) ;
491- if ( ! signature ) return "" ;
510+ hasMeaningfulLine ( lines ) {
511+ return lines . some ( line => line . trim ( ) . length > 0 ) ;
512+ }
513+
514+ /**
515+ * Render Swift doc comments from a node's JSDoc, including parameter/return tags.
516+ * @param {ts.Node } node
517+ * @param {{ indent?: string, parameterNameMap?: Map<string, string> } } options
518+ * @private
519+ */
520+ emitDocComment ( node , options = { } ) {
521+ const indent = options . indent ?? "" ;
522+ const parameterNameMap = options . parameterNameMap ?? new Map ( ) ;
523+
524+ /** @type {string[] } */
525+ const descriptionLines = [ ] ;
526+ for ( const doc of ts . getJSDocCommentsAndTags ( node ) ) {
527+ if ( ! ts . isJSDoc ( doc ) ) continue ;
528+ const text = this . renderJSDocText ( doc . comment ) ;
529+ if ( text ) {
530+ descriptionLines . push ( ...this . splitDocumentationText ( text ) ) ;
531+ }
532+ }
533+
534+ /** @type {Array<{ name: string, lines: string[] }> } */
535+ const parameterDocs = [ ] ;
536+ const supportsParameters = (
537+ ts . isFunctionLike ( node ) ||
538+ ts . isMethodSignature ( node ) ||
539+ ts . isCallSignatureDeclaration ( node ) ||
540+ ts . isConstructSignatureDeclaration ( node )
541+ ) ;
542+ /** @type {ts.JSDocReturnTag | undefined } */
543+ let returnTag = undefined ;
544+ if ( supportsParameters ) {
545+ for ( const tag of ts . getJSDocTags ( node ) ) {
546+ if ( ts . isJSDocParameterTag ( tag ) ) {
547+ const tsName = tag . name . getText ( ) ;
548+ const name = parameterNameMap . get ( tsName ) ?? this . renderIdentifier ( tsName ) ;
549+ const text = this . renderJSDocText ( tag . comment ) ;
550+ const lines = this . splitDocumentationText ( text ) ;
551+ parameterDocs . push ( { name, lines } ) ;
552+ } else if ( ! returnTag && ts . isJSDocReturnTag ( tag ) ) {
553+ returnTag = tag ;
554+ }
555+ }
556+ }
557+
558+ const returnLines = returnTag ? this . splitDocumentationText ( this . renderJSDocText ( returnTag . comment ) ) : [ ] ;
559+ const hasDescription = this . hasMeaningfulLine ( descriptionLines ) ;
560+ const hasParameters = parameterDocs . length > 0 ;
561+ const hasReturns = returnTag !== undefined ;
562+
563+ if ( ! hasDescription && ! hasParameters && ! hasReturns ) {
564+ return ;
565+ }
492566
493- return this . renderParameters ( signature . getParameters ( ) , node ) ;
567+ /** @type {string[] } */
568+ const docLines = [ ] ;
569+ if ( hasDescription ) {
570+ docLines . push ( ...descriptionLines ) ;
571+ }
572+
573+ if ( hasDescription && ( hasParameters || hasReturns ) ) {
574+ docLines . push ( "" ) ;
575+ }
576+
577+ if ( hasParameters ) {
578+ docLines . push ( "- Parameters:" ) ;
579+ for ( const param of parameterDocs ) {
580+ const hasParamDescription = this . hasMeaningfulLine ( param . lines ) ;
581+ const [ firstParamLine , ...restParamLines ] = param . lines ;
582+ if ( hasParamDescription ) {
583+ docLines . push ( ` - ${ param . name } : ${ firstParamLine } ` ) ;
584+ for ( const line of restParamLines ) {
585+ docLines . push ( ` ${ line } ` ) ;
586+ }
587+ } else {
588+ docLines . push ( ` - ${ param . name } :` ) ;
589+ }
590+ }
591+ }
592+
593+ if ( hasReturns ) {
594+ const hasReturnDescription = this . hasMeaningfulLine ( returnLines ) ;
595+ const [ firstReturnLine , ...restReturnLines ] = returnLines ;
596+ if ( hasReturnDescription ) {
597+ docLines . push ( `- Returns: ${ firstReturnLine } ` ) ;
598+ for ( const line of restReturnLines ) {
599+ docLines . push ( ` ${ line } ` ) ;
600+ }
601+ } else {
602+ docLines . push ( "- Returns:" ) ;
603+ }
604+ }
605+
606+ const prefix = `${ indent } ///` ;
607+ for ( const line of docLines ) {
608+ if ( line . length === 0 ) {
609+ this . swiftLines . push ( prefix ) ;
610+ } else {
611+ this . swiftLines . push ( `${ prefix } ${ line } ` ) ;
612+ }
613+ }
494614 }
495615
496616 /**
617+ * Build a map from TypeScript parameter names to rendered Swift identifiers.
618+ * @param {ts.Symbol[] } parameters
619+ * @returns {Map<string, string> }
620+ * @private
621+ */
622+ buildParameterNameMap ( parameters ) {
623+ const map = new Map ( ) ;
624+ for ( const parameter of parameters ) {
625+ map . set ( parameter . name , this . renderIdentifier ( parameter . name ) ) ;
626+ }
627+ return map ;
628+ }
629+
630+ /** @returns {string } */
631+ renderDefaultJSImportFromArgument ( ) {
632+ if ( this . defaultImportFromGlobal ) return "from: .global" ;
633+ return "" ;
634+ }
635+
636+ /**
637+ * Visit a property declaration and extract metadata
497638 * @param {ts.PropertyDeclaration | ts.PropertySignature } node
498- * @returns {{ jsName: string, swiftName: string, type: string, isReadonly: boolean, documentation: string | undefined } | null }
639+ * @returns {{ jsName: string, swiftName: string, type: string, isReadonly: boolean } | null }
499640 */
500641 visitPropertyDecl ( node ) {
501642 if ( ! node . name ) return null ;
@@ -515,8 +656,7 @@ export class TypeProcessor {
515656 const type = this . checker . getTypeAtLocation ( node )
516657 const swiftType = this . visitType ( type , node ) ;
517658 const isReadonly = node . modifiers ?. some ( m => m . kind === ts . SyntaxKind . ReadonlyKeyword ) ?? false ;
518- const documentation = this . getFullJSDocText ( node ) ;
519- return { jsName, swiftName, type : swiftType , isReadonly, documentation } ;
659+ return { jsName, swiftName, type : swiftType , isReadonly } ;
520660 }
521661
522662 /**
@@ -551,6 +691,7 @@ export class TypeProcessor {
551691 if ( fromArg ) args . push ( fromArg ) ;
552692 const annotation = this . renderMacroAnnotation ( "JSClass" , args ) ;
553693 const className = this . renderIdentifier ( swiftName ) ;
694+ this . emitDocComment ( node , { indent : "" } ) ;
554695 this . swiftLines . push ( `${ annotation } struct ${ className } {` ) ;
555696
556697 // Process members in declaration order
@@ -600,11 +741,15 @@ export class TypeProcessor {
600741
601742 /**
602743 * Visit a structured type (interface) and render Swift code
603- * @param {string } name
744+ * @param {ts.Type } type
745+ * @param {ts.Node } diagnosticNode
604746 * @param {ts.Symbol[] } members
605747 * @private
606748 */
607- visitStructuredType ( name , members ) {
749+ visitStructuredType ( type , diagnosticNode , members ) {
750+ const symbol = type . getSymbol ( ) ?? type . aliasSymbol ;
751+ const name = symbol ?. name ?? this . checker . typeToString ( type ) ;
752+ if ( ! name ) return ;
608753 if ( this . emittedStructuredTypeNames . has ( name ) ) return ;
609754 this . emittedStructuredTypeNames . add ( name ) ;
610755
@@ -615,17 +760,22 @@ export class TypeProcessor {
615760 if ( jsNameArg ) args . push ( jsNameArg ) ;
616761 const annotation = this . renderMacroAnnotation ( "JSClass" , args ) ;
617762 const typeName = this . renderIdentifier ( swiftName ) ;
763+ const docNode = symbol ?. getDeclarations ( ) ?. [ 0 ] ?? diagnosticNode ;
764+ if ( docNode ) {
765+ this . emitDocComment ( docNode , { indent : "" } ) ;
766+ }
618767 this . swiftLines . push ( `${ annotation } struct ${ typeName } {` ) ;
619768
620769 // Collect all declarations with their positions to preserve order
621770 /** @type {Array<{ decl: ts.Node, symbol: ts.Symbol, position: number }> } */
622771 const allDecls = [ ] ;
623772
624- for ( const symbol of members ) {
625- for ( const decl of symbol . getDeclarations ( ) ?? [ ] ) {
773+ const typeMembers = members ?? type . getProperties ( ) ?? [ ] ;
774+ for ( const memberSymbol of typeMembers ) {
775+ for ( const decl of memberSymbol . getDeclarations ( ) ?? [ ] ) {
626776 const sourceFile = decl . getSourceFile ( ) ;
627777 const pos = sourceFile ? decl . getStart ( ) : 0 ;
628- allDecls . push ( { decl, symbol, position : pos } ) ;
778+ allDecls . push ( { decl, symbol : memberSymbol , position : pos } ) ;
629779 }
630780 }
631781
@@ -854,6 +1004,7 @@ export class TypeProcessor {
8541004 const getterAnnotation = this . renderMacroAnnotation ( "JSGetter" , getterArgs ) ;
8551005
8561006 // Always render getter
1007+ this . emitDocComment ( node , { indent : " " } ) ;
8571008 this . swiftLines . push ( ` ${ getterAnnotation } var ${ swiftName } : ${ type } ` ) ;
8581009
8591010 // Render setter if not readonly
@@ -903,7 +1054,9 @@ export class TypeProcessor {
9031054 const signature = this . checker . getSignatureFromDeclaration ( node ) ;
9041055 if ( ! signature ) return ;
9051056
906- const params = this . renderParameters ( signature . getParameters ( ) , node ) ;
1057+ const parameters = signature . getParameters ( ) ;
1058+ const parameterNameMap = this . buildParameterNameMap ( parameters ) ;
1059+ const params = this . renderParameters ( parameters , node ) ;
9071060 const returnType = this . visitType ( signature . getReturnType ( ) , node ) ;
9081061 const effects = this . renderEffects ( { isAsync : false } ) ;
9091062 const swiftMethodName = this . renderIdentifier ( swiftName ) ;
@@ -912,6 +1065,7 @@ export class TypeProcessor {
9121065 ) ?? false ;
9131066 const staticKeyword = isStatic ? "static " : "" ;
9141067
1068+ this . emitDocComment ( node , { indent : " " , parameterNameMap } ) ;
9151069 this . swiftLines . push ( ` ${ annotation } ${ staticKeyword } func ${ swiftMethodName } (${ params } ) ${ effects } -> ${ returnType } ` ) ;
9161070 }
9171071
@@ -930,8 +1084,14 @@ export class TypeProcessor {
9301084 * @private
9311085 */
9321086 renderConstructor ( node ) {
933- const params = this . renderConstructorParameters ( node ) ;
1087+ const signature = this . checker . getSignatureFromDeclaration ( node ) ;
1088+ if ( ! signature ) return ;
1089+
1090+ const parameters = signature . getParameters ( ) ;
1091+ const parameterNameMap = this . buildParameterNameMap ( parameters ) ;
1092+ const params = this . renderParameters ( parameters , node ) ;
9341093 const effects = this . renderEffects ( { isAsync : false } ) ;
1094+ this . emitDocComment ( node , { indent : " " , parameterNameMap } ) ;
9351095 this . swiftLines . push ( ` @JSFunction init(${ params } ) ${ effects } ` ) ;
9361096 }
9371097
0 commit comments