diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 92ac9b3..6041ea9 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1675,6 +1675,330 @@ 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_ExplicitExternalHandler_TakesPrecedenceOverLocalHandler() + { + 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(); + + private static int GetServiceName() => 0; + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : 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.Diagnostics).IsEmpty(); + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + + [Test] + public async Task ScanForTypesAttribute_NestedExternalHandlerReturnTypeMismatch_ReportsDiagnostic() + { + var source = """ + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(HandlerContainer.Handlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + + public static class HandlerContainer + { + public static class Handlers + { + public static int GetServiceName() => 0; + } + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + await Assert.That(DiagnosticDescriptors.WrongHandlerReturnTypeForCollectionReturn).IsEqualTo(results.Diagnostics.Single().Descriptor); + } + + [Test] + public async Task ScanForTypesAttribute_ClosedGenericExternalHandler() + { + var source = """ + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(GenericHandlers.GetServiceName))] + public static partial string[] GetServiceNames(HandlerContext context); + } + + public class HandlerContext { } + + public static class GenericHandlers + { + public static string GetServiceName(TContext context) => typeof(T).Name; + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : 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( global::GeneratorTests.HandlerContext context) + { + return [ + global::GeneratorTests.GenericHandlers.GetServiceName(context) + ]; + } + } + """; + await Assert.That(results.Diagnostics).IsEmpty(); + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + + [Test] + public async Task ScanForTypesAttribute_ExplicitInstanceHandler_ReportsDiagnostic() + { + var source = """ + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(ExternalHandlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + + public class ExternalHandlers + { + public string GetServiceName() => typeof(T).Name; + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + await Assert.That(DiagnosticDescriptors.CustomHandlerMethodHasIncorrectSignature).IsEqualTo(results.Diagnostics.Single().Descriptor); + } + + [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() { 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 2f6e01b..4df825f 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using ServiceScan.SourceGenerator.Extensions; using ServiceScan.SourceGenerator.Model; namespace ServiceScan.SourceGenerator; @@ -51,7 +52,7 @@ public partial class DependencyInjectionGenerator } var customHandlerMethod = attribute.CustomHandler != null && attribute.CustomHandlerType == CustomHandlerType.Method - ? containingType.GetMembers().OfType().FirstOrDefault(m => m.Name == attribute.CustomHandler) + ? GetCustomHandlerMethod(attribute, containingType, 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..07d971d 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs @@ -129,19 +129,22 @@ private static void AddCollectionItems( { var typeArguments = string.Join(", ", new[] { implementationTypeName } .Concat(matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))); + var handlerTarget = GetCustomHandlerTarget(attribute, implementationTypeName); if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{typeArguments}>({arguments})"); + collectionItems.Add($"{handlerTarget}<{typeArguments}>({arguments})"); else - collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); + collectionItems.Add($"{handlerTarget}({arguments})"); } } else { + var handlerTarget = GetCustomHandlerTarget(attribute, implementationTypeName); + if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{implementationTypeName}>({arguments})"); + collectionItems.Add($"{handlerTarget}<{implementationTypeName}>({arguments})"); else - collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); + collectionItems.Add($"{handlerTarget}({arguments})"); } } } @@ -153,6 +156,7 @@ private static void AddCustomHandlerItems( List customHandlers) { var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var handlerTarget = GetCustomHandlerTarget(attribute, implementationTypeName); if (attribute.CustomHandlerMethodTypeParametersCount > 1 && matchedTypes != null) { @@ -166,8 +170,7 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, - attribute.CustomHandler, - implementationTypeName, + handlerTarget, typeArguments)); } } @@ -175,9 +178,10 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F { customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, - attribute.CustomHandler, - implementationTypeName, - [implementationTypeName])); + handlerTarget, + attribute.CustomHandlerType == CustomHandlerType.Method + ? [implementationTypeName] + : [])); } } @@ -194,7 +198,6 @@ private static void AddTemplateStatementItem( customHandlers.Add(new CustomHandlerModel( Model.CustomHandlerType.Template, statement, - implementationTypeName, [])); } @@ -203,6 +206,16 @@ private static string ExpandTemplate(string template, string typeName) return TypePlaceholderRegex.Replace(template, typeName); } + private static string GetCustomHandlerTarget(AttributeModel attribute, string implementationTypeName) + { + if (attribute.CustomHandlerType == CustomHandlerType.TypeMethod) + return $"{implementationTypeName}.{attribute.CustomHandler}"; + + return attribute.CustomHandlerDeclaringTypeName is null + ? attribute.CustomHandler! + : $"{attribute.CustomHandlerDeclaringTypeName}.{attribute.CustomHandler}"; + } + 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..e8ade47 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -58,7 +58,16 @@ public partial class DependencyInjectionGenerator if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = GetCustomHandlerMethod( + attribute, + method.ContainingType, + context.SemanticModel, + position); + + if (customHandlerMethod == null && attribute.CustomHandlerType == CustomHandlerType.Method) + { + return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + } if (customHandlerMethod != null) { @@ -141,7 +150,16 @@ public partial class DependencyInjectionGenerator } else if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = GetCustomHandlerMethod( + attribute, + method.ContainingType, + context.SemanticModel, + position); + + if (customHandlerMethod == null && attribute.CustomHandlerType == CustomHandlerType.Method) + { + return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + } if (customHandlerMethod != null) { @@ -185,4 +203,25 @@ public partial class DependencyInjectionGenerator var model = MethodModel.Create(method, context.TargetNode); return new MethodWithAttributesModel(model, [.. attributeData]); } + + /// + /// Resolves a custom handler on its explicitly named declaring type, or on the generated method's containing type + /// when no declaring type was specified. + /// + private static IMethodSymbol? GetCustomHandlerMethod( + AttributeModel attribute, + INamedTypeSymbol containingType, + SemanticModel semanticModel, + int position) + { + var handlerType = AttributeModel.GetExplicitHandlerDeclaringType( + attribute.Location, + semanticModel, + "Handler", + "CustomHandler"); + + return handlerType != null + ? handlerType.GetMethod(attribute.CustomHandler!, semanticModel, position, isStatic: true) + : containingType.GetMethod(attribute.CustomHandler!, semanticModel, position); + } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index a1c16a4..41074c2 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -146,17 +146,15 @@ private static string GenerateCustomHandlerStatement(MethodModel method, CustomH { if (handler.CustomHandlerType == CustomHandlerType.Template) { - return handler.HandlerMethodName; + return handler.HandlerTarget; } var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); - if (handler.CustomHandlerType == CustomHandlerType.Method) - { - var genericArguments = string.Join(", ", handler.TypeArguments); - return $"{handler.HandlerMethodName}<{genericArguments}>({arguments});"; - } + var genericArguments = handler.CustomHandlerType == CustomHandlerType.Method + ? $"<{string.Join(", ", handler.TypeArguments)}>" + : ""; - return $"{handler.TypeName}.{handler.HandlerMethodName}({arguments});"; + return $"{handler.HandlerTarget}{genericArguments}({arguments});"; } private static string GenerateMethodSource( 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..d14ae29 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,7 @@ record AttributeModel( string? CustomHandler, CustomHandlerType? CustomHandlerType, int CustomHandlerMethodTypeParametersCount, + string? CustomHandlerDeclaringTypeName, bool AsImplementedInterfaces, bool AsSelf, Location Location, @@ -36,6 +38,9 @@ record AttributeModel( public static AttributeModel Create(AttributeData attribute, IMethodSymbol method, SemanticModel semanticModel) { var position = attribute.ApplicationSyntaxReference?.Span.Start ?? 0; + var syntax = attribute.ApplicationSyntaxReference.SyntaxTree; + var textSpan = attribute.ApplicationSyntaxReference.Span; + var location = Location.Create(syntax, textSpan); var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol; var assemblyNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssemblyNameFilter").Value.Value as string; @@ -71,11 +76,25 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho CustomHandlerType? customHandlerType = null; var customHandlerGenericParameters = 0; + string? customHandlerDeclaringTypeName = null; if (customHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(customHandler, semanticModel, position); + var explicitHandlerType = GetExplicitHandlerDeclaringType(location, semanticModel, "Handler", "CustomHandler"); + IMethodSymbol? customHandlerMethod; + + if (explicitHandlerType != null) + { + customHandlerMethod = explicitHandlerType.GetMethod(customHandler, semanticModel, position, isStatic: true); + customHandlerDeclaringTypeName = explicitHandlerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + else + { + customHandlerMethod = method.ContainingType.GetMethod(customHandler, semanticModel, position); + } - customHandlerType = customHandlerMethod != null ? Model.CustomHandlerType.Method : Model.CustomHandlerType.TypeMethod; + customHandlerType = customHandlerMethod != null || explicitHandlerType != null + ? Model.CustomHandlerType.Method + : Model.CustomHandlerType.TypeMethod; customHandlerGenericParameters = customHandlerMethod?.TypeParameters.Length ?? 0; } @@ -110,10 +129,6 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho _ => "Transient" }; - var syntax = attribute.ApplicationSyntaxReference.SyntaxTree; - var textSpan = attribute.ApplicationSyntaxReference.Span; - var location = Location.Create(syntax, textSpan); - var hasError = assemblyType is { TypeKind: TypeKind.Error } || assignableTo is { TypeKind: TypeKind.Error } || attributeFilterType is { TypeKind: TypeKind.Error }; @@ -136,10 +151,45 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho customHandler, customHandlerType, customHandlerGenericParameters, + customHandlerDeclaringTypeName, asImplementedInterfaces, asSelf, location, hasError, handlerTemplate); } -} \ No newline at end of file + + /// + /// Extracts the declaring type from a nameof(Type.Method) attribute argument. + /// + /// The location of 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, . + internal static INamedTypeSymbol? GetExplicitHandlerDeclaringType( + Location attributeLocation, + SemanticModel semanticModel, + params string[] argumentNames) + { + if (attributeLocation.SourceTree?.GetRoot().FindNode(attributeLocation.SourceSpan) 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..b3f37a5 100644 --- a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs +++ b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs @@ -11,6 +11,5 @@ record ServiceRegistrationModel( record CustomHandlerModel( CustomHandlerType CustomHandlerType, - string HandlerMethodName, - string TypeName, + string HandlerTarget, EquatableArray TypeArguments);