Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 143 additions & 1 deletion ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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::GeneratorTests.MyService1>(),
global::External.ExternalHandlers.GetServiceName<global::GeneratorTests.MyService2>()
];
}
}
""";
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<global::GeneratorTests.MyService1>()
];
}
}
""";
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<TRequest> { }
public class MyCommand { }
public class MyCommandHandler : ICommandHandler<MyCommand> { }
""";

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<global::GeneratorTests.MyCommandHandler, global::GeneratorTests.MyCommand>(services);
return services;
}
}
""";
await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected);
}

[Test]
public async Task ScanForTypesAttribute_ReturnsTypeArray_MultipleAttributes()
{
Expand Down Expand Up @@ -1849,4 +1991,4 @@ public class MyService : IService { }

await Assert.That(DiagnosticDescriptors.CantUseBothHandlerAndHandlerTemplate).IsEqualTo(results.Diagnostics.Single().Descriptor);
}
}
}
8 changes: 7 additions & 1 deletion ServiceScan.SourceGenerator.Tests/TestServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
public interface IExternalService;
public class ExternalService1 : IExternalService { }
public class ExternalService2 : IExternalService { }
public static class ExternalHandlers
{
public static string GetServiceName<T>() => typeof(T).Name;

public static void Register<THandler, TRequest>(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
}

// Shouldn't be added as type is not accessible from other assembly
internal class InternalExternalService2 : IExternalService { }
internal class InternalExternalService2 : IExternalService { }
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,7 +51,8 @@ public partial class DependencyInjectionGenerator
}

var customHandlerMethod = attribute.CustomHandler != null && attribute.CustomHandlerType == CustomHandlerType.Method
? containingType.GetMembers().OfType<IMethodSymbol>().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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,15 @@ 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})");
}
}
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})");
}
Expand Down Expand Up @@ -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));
}
}
Expand All @@ -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]));
}
}
Expand All @@ -203,6 +207,16 @@ private static string ExpandTemplate(string template, string typeName)
return TypePlaceholderRegex.Replace(template, typeName);
}

/// <summary>
/// Formats a custom handler invocation, prefixing the method with a type name when the handler is declared on
/// another type.
/// </summary>
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<INamedTypeSymbol> GetSuitableInterfaces(ITypeSymbol type)
{
return type.AllInterfaces.Where(x => !ExcludedInterfaces.Contains(x.ToDisplayString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -185,4 +187,21 @@ public partial class DependencyInjectionGenerator
var model = MethodModel.Create(method, context.TargetNode);
return new MethodWithAttributesModel(model, [.. attributeData]);
}

/// <summary>
/// Resolves a static custom handler method declared on another type using the metadata name captured from
/// <c>nameof(Type.Method)</c>.
/// </summary>
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);
}
}
3 changes: 2 additions & 1 deletion ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion ServiceScan.SourceGenerator/GenerateAttributeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// - <c>nameof(OtherType.Method)</c> for a static generic method on another type.
/// - Static method name in found types.
/// This property is incompatible with <see cref="HandlerTemplate"/>.
/// </summary>
Expand Down Expand Up @@ -198,4 +199,3 @@ internal class ScanForTypesAttribute : Attribute
}
""";
}

48 changes: 47 additions & 1 deletion ServiceScan.SourceGenerator/Model/AttributeModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using ServiceScan.SourceGenerator.Extensions;

namespace ServiceScan.SourceGenerator.Model;
Expand All @@ -25,6 +26,8 @@ record AttributeModel(
string? CustomHandler,
CustomHandlerType? CustomHandlerType,
int CustomHandlerMethodTypeParametersCount,
string? CustomHandlerDeclaringTypeMetadataName,
string? CustomHandlerDeclaringTypeName,
bool AsImplementedInterfaces,
bool AsSelf,
Location Location,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -136,10 +149,43 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
customHandler,
customHandlerType,
customHandlerGenericParameters,
customHandlerDeclaringTypeMetadataName,
customHandlerDeclaringTypeName,
asImplementedInterfaces,
asSelf,
location,
hasError,
handlerTemplate);
}
}

/// <summary>
/// Extracts the declaring type from a <c>nameof(Type.Method)</c> attribute argument.
/// </summary>
/// <param name="attribute">The attribute containing the handler argument.</param>
/// <param name="semanticModel">The semantic model used to resolve the type symbol.</param>
/// <param name="argumentNames">The supported attribute argument names to inspect.</param>
/// <returns>The declaring type when the handler is specified as <c>nameof(Type.Method)</c>; otherwise, <see langword="null"/>.</returns>
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;
}
}
Loading