// 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 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(this IRegistrationBase registration)"); stringBuilder.AppendLine($"{INDENT}{{"); stringBuilder.AppendLine($"{INDENT}{INDENT}FactoryBuilder factoryBuilder = new();"); stringBuilder.AppendLine($"{INDENT}{INDENT}registration.AddGeneratedFactory(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 types) { string? classNamespace = typeof(FactoryGenerator).Namespace; context.AddSource($"{BUILDER_CLASS_NAME}.g.cs", GenerateBuilderClassSourceCode(classNamespace, types)); } private void GenerateFactory(SourceProductionContext context, ImmutableArray 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 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(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;"); stringBuilder.AppendLine(); if (typeNamespace is not null) { stringBuilder.AppendLine($"namespace {typeNamespace};"); stringBuilder.AppendLine(); } stringBuilder.AppendLine($"public class Generated{typeName}(IocContainer container) : {typeName}"); stringBuilder.AppendLine("{"); ImmutableArray members = typeSymbol.GetMembers(); 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 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 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 IEnumerable GetNamespacesOfTypes(ImmutableArray types) => types.OfType() .Select(s => s.ContainingNamespace.IsGlobalNamespace ? null : s.ContainingNamespace.ToString()) .OfType() .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 GetParameterConstraints(ITypeParameterSymbol typeParameterSymbol) { List 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; } }