1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Collections . Immutable ;
4+ using System . Linq ;
5+ using System . Text ;
6+ using Microsoft . CodeAnalysis ;
7+ using Microsoft . CodeAnalysis . CSharp ;
8+ using Microsoft . CodeAnalysis . CSharp . Syntax ;
9+ using Microsoft . CodeAnalysis . Text ;
10+
111namespace LocalStack . Client . Generators ;
212
313/// <summary>
@@ -13,14 +23,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
1323 var isNet8OrAbove = context . CompilationProvider
1424 . Select ( ( compilation , _ ) => IsTargetFrameworkNet8OrAbove ( compilation ) ) ;
1525
16- // Discover AWS service clients in the consumer's compilation
17- var awsClients = context . SyntaxProvider
18- . CreateSyntaxProvider (
19- predicate : static ( s , _ ) => IsAwsClientClass ( s ) ,
20- transform : static ( ctx , _ ) => GetAwsClientInfo ( ctx ) )
21- . Where ( static info => info is not null )
22- . Select ( static ( info , _ ) => info ! )
23- . Collect ( ) ;
26+ // Discover AWS service clients from compilation metadata (referenced assemblies)
27+ var awsClients = context . CompilationProvider
28+ . Select ( ( compilation , _ ) => FindAwsClientsInMetadata ( compilation ) . ToImmutableArray ( ) ) ;
2429
2530 // Combine framework check with discovered clients
2631 var generationInput = isNet8OrAbove
@@ -87,54 +92,67 @@ private static IEnumerable<string> GetPreprocessorSymbols(CSharpCompilationOptio
8792 return new [ ] { "NET8_0_OR_GREATER" } ;
8893 }
8994
90- private static bool IsAwsClientClass ( SyntaxNode syntaxNode )
95+ private static IEnumerable < AwsClientInfo > FindAwsClientsInMetadata ( Compilation compilation )
9196 {
92- // Look for class declarations that might be AWS clients
93- if ( syntaxNode is not ClassDeclarationSyntax classDecl )
94- return false ;
97+ // Find the AmazonServiceClient base type
98+ var baseSym = compilation . GetTypeByMetadataName ( "Amazon.Runtime.AmazonServiceClient" ) ;
99+ if ( baseSym is null )
100+ {
101+ // AWS SDK not referenced, no clients to find
102+ yield break ;
103+ }
95104
96- // Quick syntactic check: class name ends with "Client"
97- return classDecl . Identifier . ValueText . EndsWith ( "Client" , StringComparison . Ordinal ) ;
105+ foreach ( var reference in compilation . References )
106+ {
107+ if ( compilation . GetAssemblyOrModuleSymbol ( reference ) is IAssemblySymbol assembly )
108+ {
109+ foreach ( var client in GetAwsClientsFromAssembly ( assembly . GlobalNamespace , baseSym ) )
110+ {
111+ yield return client ;
112+ }
113+ }
114+ }
98115 }
99116
100- private static AwsClientInfo ? GetAwsClientInfo ( GeneratorSyntaxContext context )
117+ private static IEnumerable < AwsClientInfo > GetAwsClientsFromAssembly ( INamespaceSymbol namespaceSymbol , INamedTypeSymbol baseType )
101118 {
102- var classDecl = ( ClassDeclarationSyntax ) context . Node ;
103- var semanticModel = context . SemanticModel ;
104-
105- // Get the symbol for this class
106- if ( semanticModel . GetDeclaredSymbol ( classDecl ) is not INamedTypeSymbol classSymbol )
107- return null ;
108-
109- // Check if it inherits from AmazonServiceClient
110- if ( ! InheritsFromAmazonServiceClient ( classSymbol ) )
111- return null ;
112-
113- // Try to find the corresponding ClientConfig type
114- var configType = FindClientConfigType ( classSymbol ) ;
115- if ( configType == null )
116- return null ;
117-
118- // Find the corresponding service interface
119- var serviceInterface = FindServiceInterface ( classSymbol ) ;
120-
121- return new AwsClientInfo (
122- ClientType : classSymbol ,
123- ConfigType : configType ,
124- ServiceInterface : serviceInterface ) ;
119+ foreach ( var member in namespaceSymbol . GetMembers ( ) )
120+ {
121+ if ( member is INamespaceSymbol nestedNamespace )
122+ {
123+ foreach ( var client in GetAwsClientsFromAssembly ( nestedNamespace , baseType ) )
124+ {
125+ yield return client ;
126+ }
127+ }
128+ else if ( member is INamedTypeSymbol typeSymbol && InheritsFromAmazonServiceClient ( typeSymbol , baseType ) )
129+ {
130+ // Try to find the corresponding ClientConfig type
131+ var configType = FindClientConfigType ( typeSymbol ) ;
132+ if ( configType != null )
133+ {
134+ // Find the corresponding service interface
135+ var serviceInterface = FindServiceInterface ( typeSymbol ) ;
136+
137+ yield return new AwsClientInfo (
138+ clientType : typeSymbol ,
139+ configType : configType ,
140+ serviceInterface : serviceInterface ) ;
141+ }
142+ }
143+ }
125144 }
126145
127- private static bool InheritsFromAmazonServiceClient ( INamedTypeSymbol classSymbol )
146+ private static bool InheritsFromAmazonServiceClient ( INamedTypeSymbol typeSymbol , INamedTypeSymbol baseType )
128147 {
129- var baseType = classSymbol . BaseType ;
130- while ( baseType != null )
148+ var current = typeSymbol . BaseType ;
149+ while ( current != null )
131150 {
132- if ( string . Equals ( baseType . Name , "AmazonServiceClient" , StringComparison . Ordinal ) &&
133- string . Equals ( baseType . ContainingNamespace . ToDisplayString ( ) , "Amazon.Runtime" , StringComparison . Ordinal ) )
151+ if ( SymbolEqualityComparer . Default . Equals ( current , baseType ) )
134152 {
135153 return true ;
136154 }
137- baseType = baseType . BaseType ;
155+ current = current . BaseType ;
138156 }
139157 return false ;
140158 }
@@ -146,7 +164,7 @@ private static bool InheritsFromAmazonServiceClient(INamedTypeSymbol classSymbol
146164 if ( ! clientName . EndsWith ( "Client" , StringComparison . Ordinal ) )
147165 return null ;
148166
149- var configName = clientName [ .. ^ 6 ] + "Config" ; // Remove "Client", add "Config"
167+ var configName = clientName . Substring ( 0 , clientName . Length - 6 ) + "Config" ; // Remove "Client", add "Config"
150168 var containingNamespace = clientSymbol . ContainingNamespace ;
151169
152170 // Look for the config type in the same namespace
@@ -160,7 +178,7 @@ private static bool InheritsFromAmazonServiceClient(INamedTypeSymbol classSymbol
160178 if ( ! clientName . EndsWith ( "Client" , StringComparison . Ordinal ) )
161179 return null ;
162180
163- var interfaceName = "I" + clientName [ .. ^ 6 ] ; // Remove "Client", add "I" prefix
181+ var interfaceName = "I" + clientName . Substring ( 0 , clientName . Length - 6 ) ; // Remove "Client", add "I" prefix
164182 var containingNamespace = clientSymbol . ContainingNamespace ;
165183
166184 // Look for the interface in the same namespace
@@ -171,28 +189,39 @@ private static void GenerateAccessors(SourceProductionContext context, Immutable
171189 {
172190 var sourceBuilder = new StringBuilder ( ) ;
173191
174- // Generate file header
192+ // Generate file header with single namespace
175193 sourceBuilder . AppendLine ( "// <auto-generated/>" ) ;
176194 sourceBuilder . AppendLine ( "// Generated by LocalStack.Client.Generators" ) ;
177195 sourceBuilder . AppendLine ( "#nullable enable" ) ;
178196 sourceBuilder . AppendLine ( ) ;
179197 sourceBuilder . AppendLine ( "using System;" ) ;
180198 sourceBuilder . AppendLine ( "using System.Diagnostics.CodeAnalysis;" ) ;
181199 sourceBuilder . AppendLine ( "using System.Runtime.CompilerServices;" ) ;
200+ sourceBuilder . AppendLine ( "using Amazon;" ) ;
182201 sourceBuilder . AppendLine ( "using Amazon.Runtime;" ) ;
202+ sourceBuilder . AppendLine ( "using Amazon.Runtime.Internal;" ) ;
183203 sourceBuilder . AppendLine ( "using LocalStack.Client.Utils;" ) ;
184204 sourceBuilder . AppendLine ( ) ;
205+ sourceBuilder . AppendLine ( "namespace LocalStack.Client.Generated" ) ;
206+ sourceBuilder . AppendLine ( "{" ) ;
185207
186208 // Generate accessor for each client
187- foreach ( var client in clients )
209+ for ( int i = 0 ; i < clients . Length ; i ++ )
188210 {
189- GenerateAccessorClass ( sourceBuilder , client ) ;
190- sourceBuilder . AppendLine ( ) ;
211+ if ( i > 0 )
212+ {
213+ sourceBuilder . AppendLine ( ) ;
214+ }
215+ GenerateAccessorClass ( sourceBuilder , clients [ i ] ) ;
191216 }
192217
218+ sourceBuilder . AppendLine ( ) ;
219+
193220 // Generate module initializer
194221 GenerateModuleInitializer ( sourceBuilder , clients ) ;
195222
223+ sourceBuilder . AppendLine ( "}" ) ; // Close namespace
224+
196225 // Add the generated source
197226 context . AddSource ( "AwsAccessors.g.cs" , SourceText . From ( sourceBuilder . ToString ( ) , Encoding . UTF8 ) ) ;
198227 }
@@ -203,98 +232,112 @@ private static void GenerateAccessorClass(StringBuilder builder, AwsClientInfo c
203232 var configTypeName = client . ConfigType . ToDisplayString ( ) ;
204233 var accessorClassName = client . ClientType . Name + "_Accessor" ;
205234
206- builder . AppendLine ( "namespace LocalStack.Client.Generated;" ) ;
207- builder . AppendLine ( ) ;
208-
209- // Add DynamicDependency attributes for trimming safety
210- builder . AppendLine ( $ "[DynamicDependency(\" serviceMetadata\" , typeof({ clientTypeName } ))]") ;
211- builder . AppendLine ( $ "[DynamicDependency(\" .ctor\" , typeof({ configTypeName } ))]") ;
212- builder . AppendLine ( $ "internal sealed class { accessorClassName } : IAwsAccessor") ;
213- builder . AppendLine ( "{" ) ;
235+ builder . AppendLine ( $ " internal sealed class { accessorClassName } : IAwsAccessor") ;
236+ builder . AppendLine ( " {" ) ;
214237
215238 // Type properties
216- builder . AppendLine ( $ " public Type ClientType => typeof({ clientTypeName } );") ;
217- builder . AppendLine ( $ " public Type ConfigType => typeof({ configTypeName } );") ;
239+ builder . AppendLine ( $ " public Type ClientType => typeof({ clientTypeName } );") ;
240+ builder . AppendLine ( $ " public Type ConfigType => typeof({ configTypeName } );") ;
218241 builder . AppendLine ( ) ;
219242
220243 // UnsafeAccessor methods
221- builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = \" serviceMetadata\" )]" ) ;
222- builder . AppendLine ( $ " private static extern ref IServiceMetadata GetServiceMetadataField({ clientTypeName } ? instance);") ;
244+ builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = \" serviceMetadata\" )]" ) ;
245+ builder . AppendLine ( $ " private static extern ref IServiceMetadata GetServiceMetadataField({ clientTypeName } ? instance);") ;
223246 builder . AppendLine ( ) ;
224247
225- builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.Constructor)]" ) ;
226- builder . AppendLine ( $ " private static extern { configTypeName } CreateConfig();") ;
248+ builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.Constructor)]" ) ;
249+ builder . AppendLine ( $ " private static extern { configTypeName } CreateConfig();") ;
227250 builder . AppendLine ( ) ;
228251
229- builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.Constructor)]" ) ;
230- builder . AppendLine ( $ " private static extern { clientTypeName } CreateClient(AWSCredentials credentials, { configTypeName } config);") ;
252+ builder . AppendLine ( " [UnsafeAccessor(UnsafeAccessorKind.Constructor)]" ) ;
253+ builder . AppendLine ( $ " private static extern { clientTypeName } CreateClient(AWSCredentials credentials, { configTypeName } config);") ;
231254 builder . AppendLine ( ) ;
232255
233- // Interface implementations
234- builder . AppendLine ( " public IServiceMetadata GetServiceMetadata()" ) ;
235- builder . AppendLine ( " => GetServiceMetadataField(null);" ) ;
256+ // Interface implementations - add DynamicDependency to the methods that need them
257+ builder . AppendLine ( $ " [DynamicDependency(\" serviceMetadata\" , typeof({ clientTypeName } ))]") ;
258+ builder . AppendLine ( " public IServiceMetadata GetServiceMetadata()" ) ;
259+ builder . AppendLine ( " {" ) ;
260+ builder . AppendLine ( " ref var metadata = ref GetServiceMetadataField(null);" ) ;
261+ builder . AppendLine ( " return metadata;" ) ;
262+ builder . AppendLine ( " }" ) ;
236263 builder . AppendLine ( ) ;
237264
238- builder . AppendLine ( " public ClientConfig CreateClientConfig()" ) ;
239- builder . AppendLine ( " => CreateConfig();" ) ;
265+ builder . AppendLine ( $ " [DynamicDependency(\" .ctor\" , typeof({ configTypeName } ))]") ;
266+ builder . AppendLine ( " public ClientConfig CreateClientConfig()" ) ;
267+ builder . AppendLine ( " => CreateConfig();" ) ;
240268 builder . AppendLine ( ) ;
241269
242- builder . AppendLine ( " public AmazonServiceClient CreateClient(AWSCredentials credentials, ClientConfig clientConfig)" ) ;
243- builder . AppendLine ( $ " => CreateClient(credentials, ({ configTypeName } )clientConfig);") ;
270+ builder . AppendLine ( " public AmazonServiceClient CreateClient(AWSCredentials credentials, ClientConfig clientConfig)" ) ;
271+ builder . AppendLine ( $ " => CreateClient(credentials, ({ configTypeName } )clientConfig);") ;
244272 builder . AppendLine ( ) ;
245273
246- builder . AppendLine ( " public void SetRegion(ClientConfig clientConfig, RegionEndpoint regionEndpoint)" ) ;
247- builder . AppendLine ( " {" ) ;
248- builder . AppendLine ( " // TODO: Generate UnsafeAccessor for region field" ) ;
249- builder . AppendLine ( " throw new NotImplementedException(\" SetRegion will be implemented in next iteration\" );" ) ;
250- builder . AppendLine ( " }" ) ;
274+ builder . AppendLine ( " public void SetRegion(ClientConfig clientConfig, RegionEndpoint regionEndpoint)" ) ;
275+ builder . AppendLine ( " {" ) ;
276+ builder . AppendLine ( " // TODO: Generate UnsafeAccessor for region field" ) ;
277+ builder . AppendLine ( " throw new NotImplementedException(\" SetRegion will be implemented in next iteration\" );" ) ;
278+ builder . AppendLine ( " }" ) ;
251279 builder . AppendLine ( ) ;
252280
253- builder . AppendLine ( " public bool TrySetForcePathStyle(ClientConfig clientConfig, bool value)" ) ;
254- builder . AppendLine ( " {" ) ;
255- builder . AppendLine ( " // TODO: Generate UnsafeAccessor for ForcePathStyle property" ) ;
256- builder . AppendLine ( " return false; // Will be implemented in next iteration" ) ;
257- builder . AppendLine ( " }" ) ;
281+ builder . AppendLine ( " public bool TrySetForcePathStyle(ClientConfig clientConfig, bool value)" ) ;
282+ builder . AppendLine ( " {" ) ;
283+ builder . AppendLine ( " // TODO: Generate UnsafeAccessor for ForcePathStyle property" ) ;
284+ builder . AppendLine ( " return false; // Will be implemented in next iteration" ) ;
285+ builder . AppendLine ( " }" ) ;
258286
259- builder . AppendLine ( "}" ) ;
287+ builder . AppendLine ( " }" ) ;
260288 }
261289
262290 private static void GenerateModuleInitializer ( StringBuilder builder , ImmutableArray < AwsClientInfo > clients )
263291 {
264- builder . AppendLine ( "namespace LocalStack.Client.Generated;" ) ;
265- builder . AppendLine ( ) ;
266- builder . AppendLine ( "internal static class GeneratedModuleInitializer" ) ;
267- builder . AppendLine ( "{" ) ;
268- builder . AppendLine ( " [ModuleInitializer]" ) ;
269- builder . AppendLine ( " public static void RegisterGeneratedAccessors()" ) ;
292+ builder . AppendLine ( " internal static class GeneratedModuleInitializer" ) ;
270293 builder . AppendLine ( " {" ) ;
294+ builder . AppendLine ( " [ModuleInitializer]" ) ;
295+ builder . AppendLine ( " public static void RegisterGeneratedAccessors()" ) ;
296+ builder . AppendLine ( " {" ) ;
271297
272298 foreach ( var client in clients )
273299 {
274300 var clientTypeName = client . ClientType . ToDisplayString ( ) ;
275301 var accessorClassName = client . ClientType . Name + "_Accessor" ;
276302
277- builder . AppendLine ( $ " AwsAccessorRegistry.Register<{ clientTypeName } >(new { accessorClassName } ());") ;
303+ builder . AppendLine ( $ " AwsAccessorRegistry.Register<{ clientTypeName } >(new { accessorClassName } ());") ;
278304
279305 if ( client . ServiceInterface != null )
280306 {
281307 var interfaceTypeName = client . ServiceInterface . ToDisplayString ( ) ;
282- builder . AppendLine ( $ " AwsAccessorRegistry.RegisterInterface<{ interfaceTypeName } , { clientTypeName } >();") ;
308+ builder . AppendLine ( $ " AwsAccessorRegistry.RegisterInterface<{ interfaceTypeName } , { clientTypeName } >();") ;
283309 }
284310 }
285311
312+ builder . AppendLine ( " }" ) ;
286313 builder . AppendLine ( " }" ) ;
287- builder . AppendLine ( "}" ) ;
288314 }
289315}
290316
291317/// <summary>
292318/// Information about a discovered AWS client and its related types.
293319/// </summary>
294- /// <param name="ClientType">The AWS service client type (e.g., AmazonS3Client)</param>
295- /// <param name="ConfigType">The corresponding client configuration type (e.g., AmazonS3Config)</param>
296- /// <param name="ServiceInterface">The service interface, if found (e.g., IAmazonS3)</param>
297- internal sealed record AwsClientInfo (
298- INamedTypeSymbol ClientType ,
299- INamedTypeSymbol ConfigType ,
300- INamedTypeSymbol ? ServiceInterface ) ;
320+ internal sealed class AwsClientInfo
321+ {
322+ public AwsClientInfo ( INamedTypeSymbol clientType , INamedTypeSymbol configType , INamedTypeSymbol ? serviceInterface )
323+ {
324+ ClientType = clientType ;
325+ ConfigType = configType ;
326+ ServiceInterface = serviceInterface ;
327+ }
328+
329+ /// <summary>
330+ /// The AWS service client type (e.g., AmazonS3Client)
331+ /// </summary>
332+ public INamedTypeSymbol ClientType { get ; }
333+
334+ /// <summary>
335+ /// The corresponding client configuration type (e.g., AmazonS3Config)
336+ /// </summary>
337+ public INamedTypeSymbol ConfigType { get ; }
338+
339+ /// <summary>
340+ /// The service interface, if found (e.g., IAmazonS3)
341+ /// </summary>
342+ public INamedTypeSymbol ? ServiceInterface { get ; }
343+ }
0 commit comments