diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index fbf3545..5b8cade 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1554,6 +1554,148 @@ public static partial class ServicesExtensions await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); } + [Test] + public async Task ScanForTypesAttribute_ReturnsCollection_WithExternalHandler() + { + var source = """ + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(External.ExternalHandlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService1 : IService { } + public class MyService2 : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial string[] GetServiceNames() + { + return [ + global::External.ExternalHandlers.GetServiceName(), + global::External.ExternalHandlers.GetServiceName() + ]; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + + [Test] + public async Task ScanForTypesAttribute_ReturnsCollection_WithAliasedExternalHandler() + { + var source = """ + using ServiceScan.SourceGenerator; + using Handlers = External.ExternalHandlers; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(Handlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService1 : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial string[] GetServiceNames() + { + return [ + global::External.ExternalHandlers.GetServiceName() + ]; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + + [Test] + public async Task ScanForTypesAttribute_WithExternalHandlerAndMatchedGenericArguments() + { + var source = """ + using Microsoft.Extensions.DependencyInjection; + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(ICommandHandler<>), Handler = nameof(External.ExternalHandlers.Register))] + public static partial IServiceCollection RegisterHandlers(this IServiceCollection services); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface ICommandHandler { } + public class MyCommand { } + public class MyCommandHandler : ICommandHandler { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection RegisterHandlers(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + global::External.ExternalHandlers.Register(services); + return services; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + [Test] public async Task ScanForTypesAttribute_ReturnsTypeArray_MultipleAttributes() { @@ -1849,4 +1991,4 @@ public class MyService : IService { } await Assert.That(DiagnosticDescriptors.CantUseBothHandlerAndHandlerTemplate).IsEqualTo(results.Diagnostics.Single().Descriptor); } -} \ No newline at end of file +} diff --git a/ServiceScan.SourceGenerator.Tests/TestServices.cs b/ServiceScan.SourceGenerator.Tests/TestServices.cs index 4987319..16d3eb7 100644 --- a/ServiceScan.SourceGenerator.Tests/TestServices.cs +++ b/ServiceScan.SourceGenerator.Tests/TestServices.cs @@ -3,6 +3,12 @@ public interface IExternalService; public class ExternalService1 : IExternalService { } public class ExternalService2 : IExternalService { } +public static class ExternalHandlers +{ + public static string GetServiceName() => typeof(T).Name; + + public static void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } +} // Shouldn't be added as type is not accessible from other assembly -internal class InternalExternalService2 : IExternalService { } \ No newline at end of file +internal class InternalExternalService2 : IExternalService { } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 678df2c..6cb9b8a 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; +using ServiceScan.SourceGenerator.Extensions; using ServiceScan.SourceGenerator.Model; namespace ServiceScan.SourceGenerator; @@ -50,7 +51,8 @@ public partial class DependencyInjectionGenerator } var customHandlerMethod = attribute.CustomHandler != null && attribute.CustomHandlerType == CustomHandlerType.Method - ? containingType.GetMembers().OfType().FirstOrDefault(m => m.Name == attribute.CustomHandler) + ? containingType.GetMethod(attribute.CustomHandler, semanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, compilation, semanticModel, position) : null; foreach (var type in assemblies.SelectMany(GetTypesFromAssembly)) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs index c866dee..b14d437 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs @@ -131,7 +131,7 @@ private static void AddCollectionItems( .Concat(matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))); if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{typeArguments}>({arguments})"); + collectionItems.Add(FormatCustomHandlerInvocation(attribute.CustomHandlerDeclaringTypeName, attribute.CustomHandler, typeArguments, arguments)); else collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); } @@ -139,7 +139,7 @@ private static void AddCollectionItems( else { if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{implementationTypeName}>({arguments})"); + collectionItems.Add(FormatCustomHandlerInvocation(attribute.CustomHandlerDeclaringTypeName, attribute.CustomHandler, implementationTypeName, arguments)); else collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); } @@ -167,7 +167,9 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, attribute.CustomHandler, - implementationTypeName, + attribute.CustomHandlerType == CustomHandlerType.Method + ? attribute.CustomHandlerDeclaringTypeName + : implementationTypeName, typeArguments)); } } @@ -176,7 +178,9 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, attribute.CustomHandler, - implementationTypeName, + attribute.CustomHandlerType == CustomHandlerType.Method + ? attribute.CustomHandlerDeclaringTypeName + : implementationTypeName, [implementationTypeName])); } } @@ -203,6 +207,16 @@ private static string ExpandTemplate(string template, string typeName) return TypePlaceholderRegex.Replace(template, typeName); } + /// + /// Formats a custom handler invocation, prefixing the method with a type name when the handler is declared on + /// another type. + /// + private static string FormatCustomHandlerInvocation(string? typeName, string methodName, string typeArguments, string arguments) + { + var target = typeName is null ? "" : $"{typeName}."; + return $"{target}{methodName}<{typeArguments}>({arguments})"; + } + private static IEnumerable GetSuitableInterfaces(ITypeSymbol type) { return type.AllInterfaces.Where(x => !ExcludedInterfaces.Contains(x.ToDisplayString())); diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 6d13549..bfde66a 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -58,7 +58,8 @@ public partial class DependencyInjectionGenerator if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, context.SemanticModel.Compilation, context.SemanticModel, position); if (customHandlerMethod != null) { @@ -141,7 +142,8 @@ public partial class DependencyInjectionGenerator } else if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, context.SemanticModel.Compilation, context.SemanticModel, position); if (customHandlerMethod != null) { @@ -185,4 +187,21 @@ public partial class DependencyInjectionGenerator var model = MethodModel.Create(method, context.TargetNode); return new MethodWithAttributesModel(model, [.. attributeData]); } + + /// + /// Resolves a static custom handler method declared on another type using the metadata name captured from + /// nameof(Type.Method). + /// + private static IMethodSymbol? GetExternalCustomHandlerMethod( + AttributeModel attribute, + Compilation compilation, + SemanticModel semanticModel, + int position) + { + var handlerType = attribute.CustomHandlerDeclaringTypeMetadataName is null + ? null + : compilation.GetTypeByMetadataName(attribute.CustomHandlerDeclaringTypeMetadataName); + + return handlerType?.GetMethod(attribute.CustomHandler!, semanticModel, position, isStatic: true); + } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index f5be124..729e6d9 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -166,7 +166,8 @@ private static string GenerateCustomHandlingSource(MethodModel method, Equatable { var genericArguments = string.Join(", ", h.TypeArguments); var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); - return $" {h.HandlerMethodName}<{genericArguments}>({arguments});"; + var target = h.TypeName is null ? "" : $"{h.TypeName}."; + return $" {target}{h.HandlerMethodName}<{genericArguments}>({arguments});"; } else { diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index da25145..07765b9 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -124,6 +124,7 @@ internal class ScanForTypesAttribute : Attribute /// Sets this property to invoke a custom method for each type found. /// This property should point to one of the following: /// - Name of a generic method in the current type. + /// - nameof(OtherType.Method) for a static generic method on another type. /// - Static method name in found types. /// This property is incompatible with . /// @@ -198,4 +199,3 @@ internal class ScanForTypesAttribute : Attribute } """; } - diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 6d51878..daf599f 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -1,5 +1,6 @@ using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using ServiceScan.SourceGenerator.Extensions; namespace ServiceScan.SourceGenerator.Model; @@ -25,6 +26,8 @@ record AttributeModel( string? CustomHandler, CustomHandlerType? CustomHandlerType, int CustomHandlerMethodTypeParametersCount, + string? CustomHandlerDeclaringTypeMetadataName, + string? CustomHandlerDeclaringTypeName, bool AsImplementedInterfaces, bool AsSelf, Location Location, @@ -71,10 +74,20 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho CustomHandlerType? customHandlerType = null; var customHandlerGenericParameters = 0; + string? customHandlerDeclaringTypeMetadataName = null; + string? customHandlerDeclaringTypeName = null; if (customHandler != null) { + var explicitHandlerType = GetExplicitHandlerDeclaringType(attribute, semanticModel, "Handler", "CustomHandler"); var customHandlerMethod = method.ContainingType.GetMethod(customHandler, semanticModel, position); + if (customHandlerMethod == null && explicitHandlerType != null) + { + customHandlerMethod = explicitHandlerType.GetMethod(customHandler, semanticModel, position, isStatic: true); + customHandlerDeclaringTypeMetadataName = explicitHandlerType.ToFullMetadataName(); + customHandlerDeclaringTypeName = explicitHandlerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + customHandlerType = customHandlerMethod != null ? Model.CustomHandlerType.Method : Model.CustomHandlerType.TypeMethod; customHandlerGenericParameters = customHandlerMethod?.TypeParameters.Length ?? 0; } @@ -136,10 +149,43 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho customHandler, customHandlerType, customHandlerGenericParameters, + customHandlerDeclaringTypeMetadataName, + customHandlerDeclaringTypeName, asImplementedInterfaces, asSelf, location, hasError, handlerTemplate); } -} \ No newline at end of file + + /// + /// Extracts the declaring type from a nameof(Type.Method) attribute argument. + /// + /// The attribute containing the handler argument. + /// The semantic model used to resolve the type symbol. + /// The supported attribute argument names to inspect. + /// The declaring type when the handler is specified as nameof(Type.Method); otherwise, . + private static INamedTypeSymbol? GetExplicitHandlerDeclaringType(AttributeData attribute, SemanticModel semanticModel, params string[] argumentNames) + { + if (attribute.ApplicationSyntaxReference?.GetSyntax() is not AttributeSyntax attributeSyntax) + return null; + + var handlerArgument = attributeSyntax.ArgumentList?.Arguments + .FirstOrDefault(a => a.NameEquals?.Name.Identifier.ValueText is { } name && argumentNames.Contains(name)); + + if (handlerArgument?.Expression is not InvocationExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier.ValueText: "nameof" }, + ArgumentList.Arguments: [{ Expression: MemberAccessExpressionSyntax memberAccessExpression }] + }) + { + return null; + } + + var symbol = semanticModel.GetSymbolInfo(memberAccessExpression.Expression).Symbol; + if (symbol is IAliasSymbol aliasSymbol) + symbol = aliasSymbol.Target; + + return symbol as INamedTypeSymbol; + } +} diff --git a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs index 973a34a..c52e0a1 100644 --- a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs +++ b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs @@ -12,5 +12,5 @@ record ServiceRegistrationModel( record CustomHandlerModel( CustomHandlerType CustomHandlerType, string HandlerMethodName, - string TypeName, + string? TypeName, EquatableArray TypeArguments);