A lightweight IOC Container that is powerful enough to do all the things you need it to do.
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.

339 lines
14 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.Name == "Task")
{
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
namespaces.AddRange(namedTypeSymbol.TypeArguments.Select(GetNamespaceOfType));
}
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.Name == "Task")
{
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
stringBuilder.Append($"<{string.Join(", ", namedTypeSymbol.TypeArguments.Select(a => a.Name))}>");
}
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.Name == "Task")
{
stringBuilder.Append($"{INDENT}{INDENT}return container.ResolveAsync");
if (method.ReturnType is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
stringBuilder.Append($"<{string.Join(", ", namedTypeSymbol.TypeArguments.Select(a => a.Name))}>");
stringBuilder.Append("(");
}
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.NullableAnnotation == NullableAnnotation.Annotated)
stringBuilder.Append("?");
stringBuilder.Append($" {parameter.Name}");
return stringBuilder.ToString();
}
private List<string> GetParameterConstraints(ITypeParameterSymbol typeParameterSymbol)
{
List<string> constraints = [];
foreach (ITypeSymbol constraintType in typeParameterSymbol.ConstraintTypes)
constraints.Add(constraintType.Name);
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");
return constraints;
}
}