@@ -3,6 +3,14 @@ import SwiftSyntaxBuilder
33import SwiftSyntaxMacros
44
55public struct SplatMacro : MemberMacro {
6+ // Helper struct to avoid large tuple warning
7+ private struct PropertyInfo {
8+ let name : String
9+ let type : TypeSyntax
10+ let doc : String ?
11+ }
12+
13+ // swiftlint:disable:next cyclomatic_complexity
614 public static func expansion(
715 of node: AttributeSyntax ,
816 providingMembersOf declaration: some DeclGroupSyntax ,
@@ -57,20 +65,69 @@ public struct SplatMacro: MemberMacro {
5765 return text
5866 }
5967
60- // Extract properties from target struct
68+ // Helper to trim whitespace using only stdlib
69+ func trimWhitespace( _ text: String ) -> String {
70+ var result = text
71+ // Trim leading whitespace
72+ while result. first? . isWhitespace == true {
73+ result. removeFirst ( )
74+ }
75+ // Trim trailing whitespace
76+ while result. last? . isWhitespace == true {
77+ result. removeLast ( )
78+ }
79+ return result
80+ }
81+
82+ // Helper to extract doc comment from trivia
83+ func extractDocComment( from variable: VariableDeclSyntax ) -> String ? {
84+ let trivia = variable. leadingTrivia
85+ var docLines : [ String ] = [ ]
86+
87+ for piece in trivia {
88+ switch piece {
89+ case . docLineComment( let text) :
90+ // Remove "/// " prefix and trim
91+ let cleaned = trimWhitespace ( text. trimmingPrefix ( " /// " ) )
92+ // Keep empty lines to preserve DocC paragraph structure
93+ docLines. append ( cleaned)
94+ case . docBlockComment( let text) :
95+ // Remove "/**" and "*/" and clean up each line
96+ let lines = text
97+ . trimmingPrefix ( " /**")
98+ .trimmingSuffix("*/" )
99+ . split ( separator: " \n " )
100+ . map { line -> String in
101+ let trimmed = trimWhitespace ( String ( line) )
102+ return trimWhitespace ( trimmed. trimmingPrefix ( " * " ) )
103+ }
104+ // Keep empty lines for DocC structure
105+ docLines. append ( contentsOf: lines)
106+ default :
107+ break
108+ }
109+ }
110+
111+ // Join lines with proper DocC formatting (newline + indentation)
112+ // This preserves DocC callouts like "- Note:", "- Important:", etc.
113+ return docLines. isEmpty ? nil : docLines. joined ( separator: " \n /// " )
114+ }
115+
116+ // Extract properties from target struct with their documentation
61117 let properties = targetStruct. memberBlock. members
62118 . compactMap { $0. decl. as ( VariableDeclSyntax . self) }
63119 . filter { $0. bindings. first? . accessorBlock == nil } // Only stored properties
64- . flatMap { variable -> [ ( String , TypeSyntax ) ] in
65- variable. bindings. compactMap { binding in
120+ . flatMap { variable -> [ PropertyInfo ] in
121+ let docComment = extractDocComment ( from: variable)
122+ return variable. bindings. compactMap { binding in
66123 guard let identifier = binding. pattern. as ( IdentifierPatternSyntax . self) ,
67124 let type = binding. typeAnnotation? . type
68125 else {
69126 return nil
70127 }
71128 // Strip backticks from identifier text to avoid double-backticking
72129 let name = stripBackticks ( identifier. identifier. text)
73- return ( name, type)
130+ return PropertyInfo ( name: name , type: type , doc : docComment )
74131 }
75132 }
76133
@@ -86,19 +143,19 @@ public struct SplatMacro: MemberMacro {
86143 }
87144
88145 // Build parameter list
89- let parameters = properties. map { name , type in
146+ let parameters = properties. map { property in
90147 FunctionParameterSyntax (
91- firstName: . identifier( " ` \( name) ` " ) ,
92- type: type
148+ firstName: . identifier( " ` \( property . name) ` " ) ,
149+ type: property . type
93150 )
94151 }
95152
96153 // Build target struct initializer call arguments
97- let argumentsCallArgs = properties. map { name , _ in
154+ let argumentsCallArgs = properties. map { property in
98155 LabeledExprSyntax (
99- label: . identifier( " ` \( name) ` " ) ,
156+ label: . identifier( " ` \( property . name) ` " ) ,
100157 colon: . colonToken( trailingTrivia: . space) ,
101- expression: DeclReferenceExprSyntax ( baseName: . identifier( " ` \( name) ` " ) )
158+ expression: DeclReferenceExprSyntax ( baseName: . identifier( " ` \( property . name) ` " ) )
102159 )
103160 }
104161
@@ -122,20 +179,159 @@ public struct SplatMacro: MemberMacro {
122179 // Build the try keyword if needed
123180 let tryKeyword = hasThrowingInit ? " try " : " "
124181
125- // Generate parameter documentation
126- let parameterDocs = properties. map { name, type in
127- " /// - \( name) : \( type. trimmed) "
182+ // Extract full documentation from Arguments initializer
183+ let ( argumentsInitDoc, argumentsParamDocs) : ( String ? , [ String : String ] ) = {
184+ let inits = targetStruct. memberBlock. members
185+ . compactMap { $0. decl. as ( InitializerDeclSyntax . self) }
186+
187+ guard let firstInit = inits. first else { return ( nil , [ : ] ) }
188+
189+ let trivia = firstInit. leadingTrivia
190+ var docLines : [ String ] = [ ]
191+
192+ for piece in trivia {
193+ switch piece {
194+ case . docLineComment( let text) :
195+ let cleaned = trimWhitespace ( text. trimmingPrefix ( " /// " ) )
196+ // Keep empty lines to preserve DocC paragraph structure
197+ docLines. append ( cleaned)
198+ case . docBlockComment( let text) :
199+ let lines = text
200+ . trimmingPrefix ( " /**")
201+ .trimmingSuffix("*/" )
202+ . split ( separator: " \n " )
203+ . map { line -> String in
204+ let trimmed = trimWhitespace ( String ( line) )
205+ return trimWhitespace ( trimmed. trimmingPrefix ( " * " ) )
206+ }
207+ // Keep empty lines for DocC structure
208+ docLines. append ( contentsOf: lines)
209+ default :
210+ break
211+ }
212+ }
213+
214+ // Find where "- Parameters:" starts
215+ guard let paramIndex = docLines. firstIndex ( where: { $0. hasPrefix ( " - Parameters: " ) } ) else {
216+ return ( docLines. isEmpty ? nil : docLines. joined ( separator: " \n /// " ) , [ : ] )
217+ }
218+
219+ // Extract summary/discussion (everything before - Parameters:)
220+ let summaryLines = Array ( docLines [ ..< paramIndex] )
221+ let summary = summaryLines. isEmpty ? nil : summaryLines. joined ( separator: " \n /// " )
222+
223+ // Extract parameter docs
224+ var paramDocs : [ String : String ] = [ : ]
225+ var currentParam : String ?
226+ var currentParamLines : [ String ] = [ ]
227+
228+ for line in docLines [ ( paramIndex + 1 ) ... ] {
229+ if line. hasPrefix ( " - " ) {
230+ // Save previous parameter if exists
231+ if let param = currentParam {
232+ paramDocs [ param] = currentParamLines. joined ( separator: " \n /// " )
233+ }
234+ // Start new parameter
235+ let parts = line. dropFirst ( 2 ) . split ( separator: " : " , maxSplits: 1 )
236+ if parts. count == 2 {
237+ currentParam = trimWhitespace ( String ( parts [ 0 ] ) )
238+ currentParamLines = [ trimWhitespace ( String ( parts [ 1 ] ) ) ]
239+ }
240+ } else {
241+ // Continuation of current parameter
242+ currentParamLines. append ( line)
243+ }
244+ }
245+
246+ // Save last parameter
247+ if let param = currentParam {
248+ paramDocs [ param] = currentParamLines. joined ( separator: " \n /// " )
249+ }
250+
251+ return ( summary, paramDocs)
252+ } ( )
253+
254+ // Generate parameter documentation using Arguments init param docs
255+ let parameterDocs = properties. map { property in
256+ if let doc = argumentsParamDocs [ property. name] {
257+ // Use the parameter documentation from Arguments init
258+ return " /// - \( property. name) : \( doc) "
259+ } else if let doc = property. doc {
260+ // Fall back to property documentation
261+ return " /// - \( property. name) : \( doc) "
262+ } else {
263+ // Last resort: just the type
264+ return " /// - \( property. name) : \( property. type. trimmed) "
265+ }
128266 } . joined ( separator: " \n " )
129267
130- // Generate throws documentation if needed
131- let throwsDocs = hasThrowingInit ? " \n /// - Throws: Error if initialization fails. " : " "
268+ // Extract throws documentation from parent struct's init (the one that takes Arguments)
269+ let throwsDocs : String = {
270+ if !hasThrowingInit {
271+ return " "
272+ }
132273
133- // Generate the convenience initializer with comprehensive DocC comments
134- let initializer : DeclSyntax = """
135- /// Initializer accepting `` \( raw: structName) `` properties as individual parameters.
274+ // Find the parent struct's init that takes the Arguments struct
275+ let parentInits = declaration. memberBlock. members
276+ . compactMap { $0. decl. as ( InitializerDeclSyntax . self) }
277+
278+ for parentInit in parentInits {
279+ // Check if it throws
280+ guard parentInit. signature. effectSpecifiers? . throwsClause != nil else {
281+ continue
282+ }
283+
284+ // Extract its documentation
285+ let trivia = parentInit. leadingTrivia
286+ var docLines : [ String ] = [ ]
287+
288+ for piece in trivia {
289+ switch piece {
290+ case . docLineComment( let text) :
291+ let cleaned = trimWhitespace ( text. trimmingPrefix ( " /// " ) )
292+ docLines. append ( cleaned)
293+ case . docBlockComment( let text) :
294+ let lines = text
295+ . trimmingPrefix ( " /**")
296+ .trimmingSuffix("*/" )
297+ . split ( separator: " \n " )
298+ . map { line -> String in
299+ let trimmed = trimWhitespace ( String ( line) )
300+ return trimWhitespace ( trimmed. trimmingPrefix ( " * " ) )
301+ }
302+ docLines. append ( contentsOf: lines)
303+ default :
304+ break
305+ }
306+ }
307+
308+ // Find "- Throws:" line
309+ for line in docLines where line. hasPrefix ( " - Throws: " ) {
310+ let throwsText = trimWhitespace ( String ( line. dropFirst ( " - Throws: " . count) ) )
311+ return " \n /// - Throws: \( throwsText) "
312+ }
313+ }
314+
315+ // Fallback if no throws documentation found
316+ return " \n /// - Throws: Error if initialization fails. "
317+ } ( )
318+
319+ // Use Arguments init doc if available, otherwise use generic description
320+ let summaryDoc : String
321+ if let initDoc = argumentsInitDoc {
322+ summaryDoc = " /// \( initDoc) "
323+ } else {
324+ summaryDoc = """
325+ /// Initializer accepting `` \( structName) `` properties as individual parameters.
136326 ///
137327 /// This initializer provides direct parameter access without explicitly creating
138- /// a `` \( raw: structName) `` instance.
328+ /// a `` \( structName) `` instance.
329+ """
330+ }
331+
332+ // Generate the convenience initializer with comprehensive DocC comments
333+ let initializer : DeclSyntax = """
334+ \( raw: summaryDoc)
139335 ///
140336 /// - Parameters:
141337 \( raw: parameterDocs) \( raw: throwsDocs)
@@ -165,3 +361,16 @@ enum SplatError: Error, CustomStringConvertible {
165361 }
166362 }
167363}
364+
365+ // String helpers for doc comment extraction
366+ extension String {
367+ func trimmingPrefix( _ prefix: String ) -> String {
368+ guard hasPrefix ( prefix) else { return self }
369+ return String ( dropFirst ( prefix. count) )
370+ }
371+
372+ func trimmingSuffix( _ suffix: String ) -> String {
373+ guard hasSuffix ( suffix) else { return self }
374+ return String ( dropLast ( suffix. count) )
375+ }
376+ }
0 commit comments