@@ -83,7 +83,7 @@ internal static J Parse(string code, IReadOnlyDictionary<string, object> capture
8383 IReadOnlyDictionary < string , string > dependencies , ScaffoldKind ? scaffoldKind )
8484 {
8585 // Compute preamble first — it affects the scaffold shape and must be part of the cache key
86- var preamble = BuildTypePreamble ( captures ) ;
86+ var preamble = BuildScaffoldPreamble ( captures ) ;
8787 var cacheKey = BuildCacheKey ( code , preamble , usings , context , dependencies , scaffoldKind ) ;
8888 if ( GlobalCache . TryGetValue ( cacheKey , out var cached ) )
8989 return cached ;
@@ -93,7 +93,7 @@ internal static J Parse(string code, IReadOnlyDictionary<string, object> capture
9393 return result ;
9494 }
9595
96- private static J ParseInternal ( string code , IReadOnlyList < string > preamble ,
96+ private static J ParseInternal ( string code , ScaffoldPreamble preamble ,
9797 IReadOnlyList < string > usings , IReadOnlyList < string > context ,
9898 IReadOnlyDictionary < string , string > dependencies , ScaffoldKind ? scaffoldKind )
9999 {
@@ -128,25 +128,74 @@ private static J ParseInternal(string code, IReadOnlyList<string> preamble,
128128 }
129129
130130 /// <summary>
131- /// Build typed field declarations for captures that have a Type.
132- /// These are emitted as class fields on the scaffold class so they are in scope
133- /// inside the method body. This avoids mixing preamble statements with the template
134- /// code, so <see cref="ExtractTemplateNode"/> doesn't need to skip anything.
135- /// Dispatches on <see cref="CaptureKind"/> to generate the right scaffold form.
131+ /// Collects field declarations, type parameters, and where clauses from captures
132+ /// for scaffold generation.
136133 /// </summary>
137- private static List < string > BuildTypePreamble ( IReadOnlyDictionary < string , object > captures )
134+ private sealed record ScaffoldPreamble (
135+ IReadOnlyList < string > Fields ,
136+ IReadOnlyList < string > TypeParameterNames ,
137+ IReadOnlyList < string > WhereClauses ) ;
138+
139+ /// <summary>
140+ /// Build the scaffold preamble from captures: field declarations for expression captures,
141+ /// type parameter names and where clauses from captures with <see cref="ICapture.TypeParameters"/>.
142+ /// </summary>
143+ private static ScaffoldPreamble BuildScaffoldPreamble ( IReadOnlyDictionary < string , object > captures )
138144 {
139145 const System . Reflection . BindingFlags bindingFlags =
140146 System . Reflection . BindingFlags . Instance |
141147 System . Reflection . BindingFlags . Public |
142148 System . Reflection . BindingFlags . NonPublic ;
143149
144- var preamble = new List < string > ( ) ;
150+ var fields = new List < string > ( ) ;
151+ var typeParamNames = new List < string > ( ) ;
152+ var whereClauses = new List < string > ( ) ;
153+ // Track bounds per type parameter name for conflict detection
154+ var typeParamBounds = new Dictionary < string , string ? > ( ) ;
155+
145156 foreach ( var kvp in captures )
146157 {
147158 var kind = kvp . Value . GetType ( ) . GetProperty ( "Kind" , bindingFlags ) ? . GetValue ( kvp . Value ) ;
148159 var placeholder = Placeholder . ToPlaceholder ( kvp . Key ) ;
149160
161+ // Collect type parameters from captures that declare them
162+ if ( kvp . Value is ICapture { TypeParameters : { } typeParams } )
163+ {
164+ foreach ( var tp in typeParams )
165+ {
166+ // Each entry is either "TName" (unbounded) or "TName : Bound1, Bound2"
167+ var colonIdx = tp . IndexOf ( ':' ) ;
168+ string name ;
169+ string ? bounds ;
170+ if ( colonIdx >= 0 )
171+ {
172+ name = tp [ ..colonIdx ] . Trim ( ) ;
173+ bounds = tp [ ( colonIdx + 1 ) ..] . Trim ( ) ;
174+ }
175+ else
176+ {
177+ name = tp . Trim ( ) ;
178+ bounds = null ;
179+ }
180+
181+ if ( typeParamBounds . TryGetValue ( name , out var existingBounds ) )
182+ {
183+ // Same name already declared — check for conflicts
184+ if ( ! string . Equals ( existingBounds , bounds , StringComparison . Ordinal ) )
185+ throw new InvalidOperationException (
186+ $ "Conflicting bounds for type parameter '{ name } ': " +
187+ $ "'{ existingBounds ?? "(none)" } ' vs '{ bounds ?? "(none)" } '") ;
188+ }
189+ else
190+ {
191+ typeParamBounds [ name ] = bounds ;
192+ typeParamNames . Add ( name ) ;
193+ if ( bounds != null )
194+ whereClauses . Add ( $ "where { name } : { bounds } ") ;
195+ }
196+ }
197+ }
198+
150199 if ( kind is CaptureKind captureKind )
151200 {
152201 switch ( captureKind )
@@ -156,7 +205,7 @@ private static List<string> BuildTypePreamble(IReadOnlyDictionary<string, object
156205 // Always emit a field declaration for expression captures so Roslyn
157206 // knows the placeholder is a variable, not a type. Without this,
158207 // `__plh_x__ * __plh_y__` is misparsed as a pointer declaration.
159- preamble . Add ( $ "{ ( string . IsNullOrEmpty ( captureType ) ? "object" : captureType ) } { placeholder } ;") ;
208+ fields . Add ( $ "{ ( string . IsNullOrEmpty ( captureType ) ? "object" : captureType ) } { placeholder } ;") ;
160209 break ;
161210 case CaptureKind . Type :
162211 // TODO: emit scaffold that places placeholder in a type position
@@ -172,18 +221,18 @@ private static List<string> BuildTypePreamble(IReadOnlyDictionary<string, object
172221 var captureType = kvp . Value . GetType ( ) . GetProperty ( "Type" ) ? . GetValue ( kvp . Value ) as string ;
173222 if ( ! string . IsNullOrEmpty ( captureType ) )
174223 {
175- preamble . Add ( $ "{ captureType } { placeholder } ;") ;
224+ fields . Add ( $ "{ captureType } { placeholder } ;") ;
176225 }
177226 }
178227 }
179- return preamble ;
228+ return new ScaffoldPreamble ( fields , typeParamNames , whereClauses ) ;
180229 }
181230
182231 /// <summary>
183232 /// Build a parseable C# source from the template code.
184233 /// The scaffold shape is controlled by <paramref name="scaffoldKind"/>.
185234 /// </summary>
186- private static string BuildScaffold ( string code , IReadOnlyList < string > preamble ,
235+ private static string BuildScaffold ( string code , ScaffoldPreamble preamble ,
187236 IReadOnlyList < string > usings , IReadOnlyList < string > context , ScaffoldKind ? scaffoldKind )
188237 {
189238 var sb = new System . Text . StringBuilder ( ) ;
@@ -201,10 +250,23 @@ private static string BuildScaffold(string code, IReadOnlyList<string> preamble,
201250 sb . AppendLine ( c ) ;
202251 }
203252
204- sb . AppendLine ( "class __T__ {" ) ;
253+ // Emit class declaration with type parameters if any captures declare them
254+ sb . Append ( "class __T__" ) ;
255+ if ( preamble . TypeParameterNames . Count > 0 )
256+ {
257+ sb . Append ( '<' ) ;
258+ sb . Append ( string . Join ( ", " , preamble . TypeParameterNames ) ) ;
259+ sb . Append ( '>' ) ;
260+ }
261+ if ( preamble . WhereClauses . Count > 0 )
262+ {
263+ sb . Append ( ' ' ) ;
264+ sb . Append ( string . Join ( " " , preamble . WhereClauses ) ) ;
265+ }
266+ sb . AppendLine ( " {" ) ;
205267
206268 // Typed capture declarations as class fields — in scope for all scaffold kinds
207- foreach ( var decl in preamble )
269+ foreach ( var decl in preamble . Fields )
208270 {
209271 sb . Append ( " " ) ;
210272 sb . AppendLine ( decl ) ;
@@ -575,7 +637,7 @@ private static Block AutoFormatSyntheticBlock(Block blk, CompilationUnit cu, J o
575637 return blk . WithStatements ( formattedStmts ) . WithPrefix ( preservedBlockPrefix ) ;
576638 }
577639
578- private static string BuildCacheKey ( string code , IReadOnlyList < string > preamble ,
640+ private static string BuildCacheKey ( string code , ScaffoldPreamble preamble ,
579641 IReadOnlyList < string > usings , IReadOnlyList < string > context ,
580642 IReadOnlyDictionary < string , string > dependencies , ScaffoldKind ? scaffoldKind = null )
581643 {
@@ -589,10 +651,22 @@ private static string BuildCacheKey(string code, IReadOnlyList<string> preamble,
589651 sb . Append ( "code:" ) ;
590652 sb . Append ( code ) ;
591653
592- if ( preamble . Count > 0 )
654+ if ( preamble . Fields . Count > 0 )
593655 {
594656 sb . Append ( "|preamble:" ) ;
595- sb . Append ( string . Join ( "," , preamble ) ) ;
657+ sb . Append ( string . Join ( "," , preamble . Fields ) ) ;
658+ }
659+
660+ if ( preamble . TypeParameterNames . Count > 0 )
661+ {
662+ sb . Append ( "|typeParams:" ) ;
663+ sb . Append ( string . Join ( "," , preamble . TypeParameterNames ) ) ;
664+ }
665+
666+ if ( preamble . WhereClauses . Count > 0 )
667+ {
668+ sb . Append ( "|where:" ) ;
669+ sb . Append ( string . Join ( "," , preamble . WhereClauses ) ) ;
596670 }
597671
598672 if ( usings . Count > 0 )
0 commit comments