You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
15 KiB
358 lines
15 KiB
// Author: Simon.Gockner
|
|
// Created: 2025-12-01
|
|
// Copyright(c) 2025 SimonG. All Rights Reserved.
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace LightweightIocContainer.FactoryGenerator;
|
|
|
|
[Generator]
|
|
public class FactoryGenerator : IIncrementalGenerator
|
|
{
|
|
private const string EXTENSION_CLASS_NAME = "FactoryExtensions";
|
|
private const string BUILDER_CLASS_NAME = "FactoryBuilder";
|
|
|
|
private const string GENERATED_FILE_HEADER = "//---GENERATED by FactoryGenerator! DO NOT EDIT!---";
|
|
private const string INDENT = " ";
|
|
|
|
private const string CLEAR_MULTITON_INSTANCE_METHOD_NAME = "ClearMultitonInstance";
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
string? classNamespace = typeof(FactoryGenerator).Namespace;
|
|
context.RegisterPostInitializationOutput(c => c.AddSource($"{EXTENSION_CLASS_NAME}.g.cs", GenerateFactoryExtensionsClass(classNamespace, EXTENSION_CLASS_NAME)));
|
|
|
|
IncrementalValuesProvider<ITypeSymbol?> syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(IsCallToGenerateFactory, GetTypeArgument);
|
|
|
|
context.RegisterSourceOutput(syntaxProvider.Collect(), GenerateTypeDependentClasses);
|
|
context.RegisterSourceOutput(syntaxProvider.Collect(), GenerateFactory);
|
|
}
|
|
|
|
private string GenerateFactoryExtensionsClass(string? classNamespace, string className)
|
|
{
|
|
StringBuilder stringBuilder = new();
|
|
|
|
stringBuilder.AppendLine(GENERATED_FILE_HEADER);
|
|
stringBuilder.AppendLine();
|
|
|
|
stringBuilder.AppendLine("using LightweightIocContainer.Interfaces.Factories;");
|
|
stringBuilder.AppendLine("using LightweightIocContainer.Interfaces.Registrations;");
|
|
stringBuilder.AppendLine();
|
|
|
|
if (!string.IsNullOrEmpty(classNamespace))
|
|
{
|
|
stringBuilder.AppendLine($"namespace {classNamespace};");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
stringBuilder.AppendLine($"public static class {className}");
|
|
stringBuilder.AppendLine("{");
|
|
|
|
stringBuilder.AppendLine($"{INDENT}public static IRegistrationBase WithGeneratedFactory<TFactory>(this IRegistrationBase registration)");
|
|
stringBuilder.AppendLine($"{INDENT}{{");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}FactoryBuilder factoryBuilder = new();");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}registration.AddGeneratedFactory<TFactory>(factoryBuilder);");
|
|
stringBuilder.AppendLine();
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}return registration;");
|
|
stringBuilder.AppendLine($"{INDENT}}}");
|
|
|
|
stringBuilder.AppendLine("}");
|
|
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
private bool IsCallToGenerateFactory(SyntaxNode node, CancellationToken cancellationToken)
|
|
{
|
|
if (!node.IsKind(SyntaxKind.GenericName) || node is not GenericNameSyntax genericNameSyntax)
|
|
return false;
|
|
|
|
if (!genericNameSyntax.ToString().StartsWith("WithGeneratedFactory"))
|
|
return false;
|
|
|
|
if (genericNameSyntax.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private ITypeSymbol? GetTypeArgument(GeneratorSyntaxContext syntaxContext, CancellationToken cancellationToken)
|
|
{
|
|
if (syntaxContext.Node is not GenericNameSyntax genericNameSyntax)
|
|
return null;
|
|
|
|
if (genericNameSyntax.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax identifierNameSyntax)
|
|
return null;
|
|
|
|
if (syntaxContext.SemanticModel.GetSymbolInfo(identifierNameSyntax).Symbol is not ITypeSymbol typeSymbol)
|
|
return null;
|
|
|
|
return typeSymbol;
|
|
}
|
|
|
|
private void GenerateTypeDependentClasses(SourceProductionContext context, ImmutableArray<ITypeSymbol?> types)
|
|
{
|
|
string? classNamespace = typeof(FactoryGenerator).Namespace;
|
|
context.AddSource($"{BUILDER_CLASS_NAME}.g.cs", GenerateBuilderClassSourceCode(classNamespace, types));
|
|
}
|
|
|
|
private void GenerateFactory(SourceProductionContext context, ImmutableArray<ITypeSymbol?> types)
|
|
{
|
|
foreach (ISymbol? symbol in types.Distinct(SymbolEqualityComparer.IncludeNullability))
|
|
{
|
|
if (symbol is not ITypeSymbol typeSymbol)
|
|
continue;
|
|
|
|
context.AddSource($"Generated{typeSymbol.Name}.g.cs", GenerateFactorySourceCode(typeSymbol));
|
|
}
|
|
}
|
|
|
|
private string GenerateBuilderClassSourceCode(string? classNamespace, ImmutableArray<ITypeSymbol?> types)
|
|
{
|
|
StringBuilder stringBuilder = new();
|
|
|
|
stringBuilder.AppendLine(GENERATED_FILE_HEADER);
|
|
stringBuilder.AppendLine();
|
|
|
|
stringBuilder.AppendLine("using LightweightIocContainer.Interfaces.Factories;");
|
|
|
|
foreach (string typeNamespace in GetNamespacesOfTypes(types))
|
|
stringBuilder.AppendLine($"using {typeNamespace};");
|
|
|
|
stringBuilder.AppendLine();
|
|
|
|
if (classNamespace is not null)
|
|
{
|
|
stringBuilder.AppendLine($"namespace {classNamespace};");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
stringBuilder.AppendLine("public class FactoryBuilder : IFactoryBuilder");
|
|
stringBuilder.AppendLine("{");
|
|
stringBuilder.AppendLine($"{INDENT}public TFactory Create<TFactory>(IocContainer container)");
|
|
stringBuilder.AppendLine($"{INDENT}{{");
|
|
|
|
foreach (ISymbol? symbol in types.Distinct(SymbolEqualityComparer.IncludeNullability))
|
|
{
|
|
if (symbol is not ITypeSymbol type)
|
|
continue;
|
|
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}if (typeof(TFactory) == typeof({type.Name}))");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}{{");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}{INDENT}return (TFactory) (object) new Generated{type.Name}(container);");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}}}");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}throw new Exception(\"Invalid type.\");");
|
|
|
|
stringBuilder.AppendLine($"{INDENT}}}");
|
|
stringBuilder.AppendLine("}");
|
|
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
private string GenerateFactorySourceCode(ITypeSymbol typeSymbol)
|
|
{
|
|
string typeName = typeSymbol.Name;
|
|
string? typeNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace ? null : typeSymbol.ContainingNamespace.ToString();
|
|
|
|
StringBuilder stringBuilder = new();
|
|
|
|
stringBuilder.AppendLine(GENERATED_FILE_HEADER);
|
|
stringBuilder.AppendLine();
|
|
|
|
stringBuilder.AppendLine("#nullable enable");
|
|
stringBuilder.AppendLine();
|
|
|
|
stringBuilder.AppendLine("using LightweightIocContainer;");
|
|
|
|
ImmutableArray<ISymbol> members = typeSymbol.GetMembers();
|
|
|
|
List<string?> namespaces = [];
|
|
foreach (ISymbol? member in members)
|
|
{
|
|
if (member is not IMethodSymbol method)
|
|
continue;
|
|
|
|
if (!method.ReturnsVoid)
|
|
{
|
|
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
|
|
{
|
|
namespaces.AddRange(namedTypeSymbol.TypeArguments.Select(GetNamespaceOfType));
|
|
|
|
if (method.ReturnType.Name != "Task")
|
|
namespaces.Add(GetNamespaceOfType(method.ReturnType));
|
|
}
|
|
else
|
|
namespaces.Add(GetNamespaceOfType(method.ReturnType));
|
|
}
|
|
|
|
namespaces.AddRange(method.Parameters.Select(p => GetNamespaceOfType(p.Type)));
|
|
}
|
|
|
|
foreach (string @namespace in namespaces.Distinct().OfType<string>().OrderBy(n => n))
|
|
stringBuilder.AppendLine($"using {@namespace};");
|
|
|
|
stringBuilder.AppendLine();
|
|
|
|
if (typeNamespace is not null)
|
|
{
|
|
stringBuilder.AppendLine($"namespace {typeNamespace};");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
stringBuilder.AppendLine($"public class Generated{typeName}(IocContainer container) : {typeName}");
|
|
stringBuilder.AppendLine("{");
|
|
|
|
foreach (ISymbol? member in members)
|
|
{
|
|
if (member is not IMethodSymbol method)
|
|
continue;
|
|
|
|
if (!method.ReturnsVoid) //create method
|
|
{
|
|
stringBuilder.Append($"{INDENT}public {method.ReturnType.Name}");
|
|
|
|
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedReturnType)
|
|
stringBuilder.Append(GetGenericArguments(namedReturnType));
|
|
|
|
stringBuilder.Append($" {method.Name}");
|
|
|
|
if (method.IsGenericMethod)
|
|
stringBuilder.Append($"<{string.Join(", ", method.TypeParameters.Select(p => p.Name))}>");
|
|
|
|
stringBuilder.Append($"({string.Join(", ", method.Parameters.Select(GetParameterText))})");
|
|
|
|
if (method.IsGenericMethod)
|
|
{
|
|
foreach (ITypeParameterSymbol typeParameter in method.TypeParameters)
|
|
{
|
|
List<string> parameterConstraints = GetParameterConstraints(typeParameter);
|
|
if (parameterConstraints.Count == 0)
|
|
continue;
|
|
|
|
stringBuilder.Append($" where {typeParameter.Name} : {string.Join(", ", parameterConstraints)}");
|
|
}
|
|
}
|
|
|
|
stringBuilder.AppendLine();
|
|
stringBuilder.AppendLine($"{INDENT}{{");
|
|
|
|
foreach (IParameterSymbol parameter in method.Parameters)
|
|
{
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}object? {parameter.Name}Value = {parameter.Name};");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}if ({parameter.Name}Value is null)");
|
|
stringBuilder.AppendLine($"{INDENT}{INDENT}{INDENT}{parameter.Name}Value = new NullParameter(typeof({parameter.Type.Name}));");
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
|
|
{
|
|
if (method.ReturnType.Name == "Task")
|
|
stringBuilder.Append($"{INDENT}{INDENT}return container.ResolveAsync{GetGenericArguments(namedTypeSymbol)}(");
|
|
else
|
|
stringBuilder.Append($"{INDENT}{INDENT}return container.Resolve<{method.ReturnType.Name}{GetGenericArguments(namedTypeSymbol)}>(");
|
|
}
|
|
else
|
|
stringBuilder.Append($"{INDENT}{INDENT}return container.Resolve<{method.ReturnType.Name}>(");
|
|
|
|
stringBuilder.Append(string.Join(", ", method.Parameters.Select(p => $"{p.Name}Value")));
|
|
stringBuilder.AppendLine(");");
|
|
|
|
stringBuilder.AppendLine($"{INDENT}}}");
|
|
}
|
|
else if (method is { Name: CLEAR_MULTITON_INSTANCE_METHOD_NAME, IsGenericMethod: true })
|
|
{
|
|
stringBuilder.Append($"{INDENT}public void {method.Name}<{string.Join(", ", method.TypeParameters.Select(p => p.Name))}>()");
|
|
|
|
foreach (ITypeParameterSymbol typeParameter in method.TypeParameters)
|
|
{
|
|
List<string> parameterConstraints = GetParameterConstraints(typeParameter);
|
|
if (parameterConstraints.Count == 0)
|
|
continue;
|
|
|
|
stringBuilder.Append($" where {typeParameter.Name} : {string.Join(", ", parameterConstraints)}");
|
|
}
|
|
|
|
stringBuilder.AppendLine($" => container.ClearMultitonInstances<{string.Join(", ", method.TypeArguments.Select(a => a.Name))}>();");
|
|
}
|
|
|
|
if (members.IndexOf(member) < members.Length - 1) //only append empty line if not the last member
|
|
stringBuilder.AppendLine();
|
|
}
|
|
|
|
stringBuilder.AppendLine("}");
|
|
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
private string? GetNamespaceOfType(ITypeSymbol s) => s.ContainingNamespace.IsGlobalNamespace ? null : s.ContainingNamespace.ToString();
|
|
private IEnumerable<string> GetNamespacesOfTypes(ImmutableArray<ITypeSymbol?> types) =>
|
|
types.OfType<ITypeSymbol>()
|
|
.Select(GetNamespaceOfType)
|
|
.OfType<string>()
|
|
.Distinct();
|
|
|
|
private string GetParameterText(IParameterSymbol parameter)
|
|
{
|
|
StringBuilder stringBuilder = new();
|
|
stringBuilder.Append(parameter.Type.Name);
|
|
|
|
if (parameter.Type is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
|
|
stringBuilder.Append(GetGenericArguments(namedTypeSymbol));
|
|
|
|
if (parameter.NullableAnnotation == NullableAnnotation.Annotated)
|
|
stringBuilder.Append("?");
|
|
|
|
stringBuilder.Append($" {parameter.Name}");
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
private string GetGenericArguments(INamedTypeSymbol namedTypeSymbol) => $"<{string.Join(", ", namedTypeSymbol.TypeArguments.Select(GetGenericArgument))}>";
|
|
private string GetGenericArgument(ITypeSymbol argument)
|
|
{
|
|
StringBuilder stringBuilder = new();
|
|
stringBuilder.Append(argument.Name);
|
|
|
|
if (argument is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
|
|
stringBuilder.Append(GetGenericArguments(namedTypeSymbol));
|
|
|
|
if (argument.NullableAnnotation == NullableAnnotation.Annotated)
|
|
stringBuilder.Append("?");
|
|
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
private List<string> GetParameterConstraints(ITypeParameterSymbol typeParameterSymbol)
|
|
{
|
|
List<string> constraints = [];
|
|
if (typeParameterSymbol.HasReferenceTypeConstraint)
|
|
constraints.Add("class");
|
|
|
|
if (typeParameterSymbol.HasValueTypeConstraint)
|
|
constraints.Add("struct");
|
|
|
|
if (typeParameterSymbol.HasConstructorConstraint)
|
|
constraints.Add("new()");
|
|
|
|
if (typeParameterSymbol.HasUnmanagedTypeConstraint)
|
|
constraints.Add("unmanaged");
|
|
|
|
if (typeParameterSymbol.HasNotNullConstraint)
|
|
constraints.Add("notnull");
|
|
|
|
foreach (ITypeSymbol constraintType in typeParameterSymbol.ConstraintTypes)
|
|
{
|
|
if (constraintType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
|
|
constraints.Add($"{constraintType.Name}{GetGenericArguments(namedTypeSymbol)}");
|
|
else
|
|
constraints.Add(constraintType.Name);
|
|
}
|
|
|
|
return constraints;
|
|
}
|
|
} |