From cb6e3b3467f919bf32be11eb0ec36400de8d534b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Sat, 14 Feb 2026 09:18:32 +0100 Subject: [PATCH 1/2] Replace union generator source --- .../Funzo.SourceGenerators.csproj | 17 +++- .../Generators/Unions/UnionGenerator.cs | 92 +++++++++++++------ .../Helpers/ContainerClass.cs | 3 +- Funzo.SourceGenerators/Helpers/MarkedType.cs | 2 +- Funzo.Test/UnionTests.cs | 1 - 5 files changed, 83 insertions(+), 32 deletions(-) diff --git a/Funzo.SourceGenerators/Funzo.SourceGenerators.csproj b/Funzo.SourceGenerators/Funzo.SourceGenerators.csproj index d57cc3c..2c6231a 100644 --- a/Funzo.SourceGenerators/Funzo.SourceGenerators.csproj +++ b/Funzo.SourceGenerators/Funzo.SourceGenerators.csproj @@ -18,13 +18,26 @@ LICENSE true + true - - + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + True diff --git a/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs b/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs index 054b3e4..c975e36 100644 --- a/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs +++ b/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs @@ -2,10 +2,11 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Sorse; +using Sorse.BuilderInterfaces; using System; using System.Collections.Immutable; using System.Linq; -using System.Text; namespace Funzo.SourceGenerators.Generators.Unions; @@ -24,45 +25,82 @@ internal class UnionGenerator : GeneratorBase var className = $"{classSymbol.Name}"; var typeArguments = type.AttributeTypeArguments; - StringBuilder source = new($@"// + var sorse = WithSorse.CreateNamespaceScope(classSymbol.ContainingNamespace.ToDisplayString(), []); -namespace {classSymbol.ContainingNamespace.ToDisplayString()} -{{"); - - // Add all needed upper classes - foreach (var containingType in type.ContainerClasses) + // TODO: Needs some rework. Feels off + if (!type.ContainerClasses.Any()) + { + sorse.AddClass(className, AddUnionClass(type)); + } + else { - var isStatic = type.Symbol.ContainingType!.IsStatic ? "static " : ""; + var builder = GetClassBuilderFromContainingClasses(type, sorse); - source.AppendLine($"{isStatic}partial class {containingType.Name} {{"); + // Add the main partial class + builder.WithInnerClass(className, AddUnionClass(type)); } - source.AppendLine($@" partial class {className} : global::{FunzoAttributeSources.AttributeNamespace}.Union{typeArguments.OpenGenericPart()} - {{"); + var src = sorse.GetSource(); + + return src; + } - foreach (var typeArgument in typeArguments) + private Action AddUnionClass(MarkedType type) => + builder => { - source.Append($@" - public {classSymbol.Name}({typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} _) : base(_) {{}} - public static implicit operator {className}({typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} _) => new {className}(_); -"); - } + var typeArguments = type.AttributeTypeArguments; + builder.Partial() + .Inherits($"global::{FunzoAttributeSources.AttributeNamespace}.Union{typeArguments.OpenGenericPart()}"); + + foreach (var typeArgument in typeArguments) + { + builder.WithConstructor(c => c.WithBaseCall(["_"]).WithArguments([new(new(typeArgument), "_")])); + builder.WithImplicitConversionOperatorFrom(new(typeArgument), " => new(x);"); + } + + var commonProperties = GetCommonProperties(type.AttributeTypeArguments); + + foreach (var prop in commonProperties) + { + builder.WithProperty(new(prop.Type), prop.Name, p => p.WithComputedValue($" => Match({string.Join(",", Enumerable.Range(0, typeArguments.Length).Select(_ => $"x => x.{prop.Name}"))});")); + } + }; - foreach (var prop in commonProperties) + private IClassBuilder GetClassBuilderFromContainingClasses(MarkedType type, INamespaceScope sorse) + { + // Create the first class in the namespace + var container = type.ContainerClasses.Pop(); + + IClassBuilder builder = null!; + + sorse.AddClass(container.Name, b => + { + builder = b; + AddContainer(b, container); + }); + + // Nest the rest + foreach (var c in type.ContainerClasses) { - source.Append($@" - public {prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {prop.Name} => Match({string.Join(",", Enumerable.Range(0, typeArguments.Length).Select(_ => $"x => x.{prop.Name}"))}); -"); + builder.WithInnerClass(c.Name, b => + { + builder = b; + AddContainer(b, c); + }); + } - // Close all the braces opened for parent classes - source.AppendLine(new string('}', type.ContainerClasses.Count)); + return builder; - source.Append(@" } -}"); + static void AddContainer(IClassBuilder builder, ContainerClass c) + { + builder.Partial(); - var src = source.ToString(); - return src; + if (c.IsStatic) + { + builder.Static(); + } + } } private bool HasErrors(SourceProductionContext context, MarkedType type) diff --git a/Funzo.SourceGenerators/Helpers/ContainerClass.cs b/Funzo.SourceGenerators/Helpers/ContainerClass.cs index c02a2ef..e12380c 100644 --- a/Funzo.SourceGenerators/Helpers/ContainerClass.cs +++ b/Funzo.SourceGenerators/Helpers/ContainerClass.cs @@ -1,6 +1,7 @@ namespace Funzo.SourceGenerators.Helpers; -public sealed class ContainerClass(string name) +public sealed class ContainerClass(string name, bool isStatic) { public string Name { get; } = name; + public bool IsStatic { get; } = isStatic; } diff --git a/Funzo.SourceGenerators/Helpers/MarkedType.cs b/Funzo.SourceGenerators/Helpers/MarkedType.cs index 5821c6d..4ea501b 100644 --- a/Funzo.SourceGenerators/Helpers/MarkedType.cs +++ b/Funzo.SourceGenerators/Helpers/MarkedType.cs @@ -26,7 +26,7 @@ private static Stack GenerateContainerClassesHierarchy(INamedTyp while (container is not null) { - containers.Push(new(container.Name)); + containers.Push(new(container.Name, container.IsStatic)); container = container.ContainingType; } diff --git a/Funzo.Test/UnionTests.cs b/Funzo.Test/UnionTests.cs index 28af8b8..5a0410d 100644 --- a/Funzo.Test/UnionTests.cs +++ b/Funzo.Test/UnionTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace Funzo.Test; From 848dc112933be55282758adaccdab813c37ff398 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Sat, 14 Feb 2026 10:08:04 +0100 Subject: [PATCH 2/2] Use Sorse with results --- .../Generators/ResultSourceGenerator.cs | 35 ++-------- .../Results/Result1AritySourceGenerator.cs | 41 +++++------- .../Results/Result2AritySourceGenerator.cs | 61 ++++++++--------- .../Generators/Results/ResultGenerator.cs | 33 ++++------ .../Generators/Unions/UnionGenerator.cs | 50 +------------- .../Helpers/SorseExtensions.cs | 66 +++++++++++++++++++ 6 files changed, 130 insertions(+), 156 deletions(-) create mode 100644 Funzo.SourceGenerators/Helpers/SorseExtensions.cs diff --git a/Funzo.SourceGenerators/Generators/ResultSourceGenerator.cs b/Funzo.SourceGenerators/Generators/ResultSourceGenerator.cs index 018f1c7..81467c4 100644 --- a/Funzo.SourceGenerators/Generators/ResultSourceGenerator.cs +++ b/Funzo.SourceGenerators/Generators/ResultSourceGenerator.cs @@ -3,10 +3,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System; +using Sorse; using System.Collections.Immutable; using System.Linq; -using System.Text; namespace Funzo.SourceGenerators.Generators; @@ -21,40 +20,16 @@ internal class ResultSourceGenerator : GeneratorBase var is1ArityResult = type.AttributeTypeArguments.Length == 1; + ResultGenerator generator = is1ArityResult ? new Result1AritySourceGenerator(type) : new Result2AritySourceGenerator(type); - var sb = new StringBuilder(); - sb.AppendLine($@"// - -namespace {type.Symbol.ContainingNamespace.ToDisplayString()} -{{"); + var sorse = WithSorse.CreateNamespaceScope(type.Symbol.ContainingNamespace.ToDisplayString(), []); - // Add all needed upper classes - foreach (var containingType in type.ContainerClasses) - { - var isStatic = type.Symbol.ContainingType!.IsStatic ? "static " : ""; - - sb.AppendLine($"{isStatic}partial class {containingType.Name} {{"); - } + sorse.AddClassWithInnerClasses(type, generator.GenerateResult); - sb.AppendLine($@" - {generator.ClassDefinition} - {{ - {generator.OkConstructor} - {generator.OkStaticHelper} - {generator.OkImplicitConverter} - {generator.ErrConstructor} - {generator.ErrStaticHelper} - {generator.ErrImplicitConverter} - }} -}}"); - - // Close all the braces opened for parent classes - sb.AppendLine(new string('}', type.ContainerClasses.Count)); - - var src = sb.ToString(); + var src = sorse.GetSource(); return src; } diff --git a/Funzo.SourceGenerators/Generators/Results/Result1AritySourceGenerator.cs b/Funzo.SourceGenerators/Generators/Results/Result1AritySourceGenerator.cs index 8c1ae78..52acd71 100644 --- a/Funzo.SourceGenerators/Generators/Results/Result1AritySourceGenerator.cs +++ b/Funzo.SourceGenerators/Generators/Results/Result1AritySourceGenerator.cs @@ -1,43 +1,38 @@ using Funzo.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; -using System.Text; +using Sorse.BuilderInterfaces; namespace Funzo.SourceGenerators.Generators.Results; + internal class Result1AritySourceGenerator : ResultGenerator { internal Result1AritySourceGenerator(MarkedType symbolWithAttribute) : base(symbolWithAttribute) { } - internal override string ClassDefinition => $@"partial class {ClassName} : global::Funzo.ResultBase<{ClassName},{ErrDisplayName}>, global::Funzo.IResultBase <{ClassName},{ErrDisplayName}>"; - - internal override string OkConstructor => @$"protected {ClassName}() : base() {{}}"; - - internal override string ErrConstructor => $@"protected {ClassName}({ErrDisplayName} _) : base(_) {{}}"; - - internal override string OkStaticHelper => $@"public static {ClassName} Ok() => new();"; - - internal override string ErrStaticHelper => $@"public static {ClassName} Err({ErrDisplayName} err) => new(err);"; - - internal override string OkImplicitConverter => string.Empty; + protected override void GenerateResultInner(IClassBuilder builder) + { + builder.Inherits($"global::Funzo.ResultBase<{ClassName}, {ErrDisplayName}>") + .Implements($"global::Funzo.IResultBase<{ClassName}, {ErrDisplayName}>") + .WithConstructor(c => c.WithBaseCall([])) + .WithConstructor(c => c.WithArguments([new(new(ErrType), "x")]).WithBaseCall(["x"])) + .WithMethod(ClassName, "Ok", m => m.Static().WithBody(" => new();")) + .WithMethod(ClassName, "Err", m => m.Static().WithArguments([new(new(ErrType), "x")]).WithBody(" => new(x);")) + .WithImplicitConversionOperatorFrom(new(ErrType), " => new(x);"); + + AddConversionsForErrUnions(builder); + } - internal override string ErrImplicitConverter + private void AddConversionsForErrUnions(IClassBuilder builder) { - get + foreach (var type in GetTypesNeedingImplicitConversions(ErrType, ResultParameterType.Err)) { - var implicitConversions = new StringBuilder(); - implicitConversions.AppendLine($@"public static implicit operator {ClassName}({ErrDisplayName} _) => new {ClassName}(_);"); - - if (TryGetImplicitConvertersForUnionType(ErrType, ResultParameterType.Err, out var converters)) - { - implicitConversions.AppendLine(converters); - } - - return implicitConversions.ToString(); + builder.WithImplicitConversionOperatorFrom(new(type), $" => new(x);"); } } private string ErrDisplayName => ErrType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); private ITypeSymbol ErrType => TypeArguments[0]; + } diff --git a/Funzo.SourceGenerators/Generators/Results/Result2AritySourceGenerator.cs b/Funzo.SourceGenerators/Generators/Results/Result2AritySourceGenerator.cs index 31f294e..508c6a4 100644 --- a/Funzo.SourceGenerators/Generators/Results/Result2AritySourceGenerator.cs +++ b/Funzo.SourceGenerators/Generators/Results/Result2AritySourceGenerator.cs @@ -1,55 +1,52 @@ using Funzo.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Sorse; +using Sorse.BuilderInterfaces; using System.Collections.Immutable; using System.Linq; -using System.Text; namespace Funzo.SourceGenerators.Generators.Results; + internal class Result2AritySourceGenerator : ResultGenerator { internal Result2AritySourceGenerator(MarkedType symbolWithAttribute) : base(symbolWithAttribute) { } - internal override string ClassDefinition => $@"partial class {ClassName} : global::Funzo.ResultBase<{ClassName}, {OkDisplayName},{ErrDisplayName}>, global::Funzo.IResultBase<{ClassName}, {OkDisplayName},{ErrDisplayName}>"; - - internal override string OkConstructor => @$"protected {ClassName}({OkDisplayName} _) : base(_) {{}}"; - - internal override string ErrConstructor => $@"protected {ClassName}({ErrDisplayName} _) : base(_) {{}}"; - - internal override string OkStaticHelper => $@"public static {ClassName} Ok({OkDisplayName} ok) => new(ok);"; - - internal override string ErrStaticHelper => $@"public static {ClassName} Err({ErrDisplayName} err) => new(err);"; + protected override void GenerateResultInner(IClassBuilder builder) + { + builder.Inherits($"global::Funzo.ResultBase<{ClassName}, {OkDisplayName}, {ErrDisplayName}>") + .Implements($"global::Funzo.IResultBase<{ClassName}, {OkDisplayName}, {ErrDisplayName}>") + .WithConstructor(c => c.WithAccessModifier(AccessModifier.Protected) + .WithArguments([new(new(OkType), "_")]).WithBaseCall(["_"])) + .WithConstructor(c => c.WithAccessModifier(AccessModifier.Protected) + .WithArguments([new(new(ErrType), "_")]).WithBaseCall(["_"])) + .WithMethod(ClassName, "Ok", m => m.Static().WithArguments([new(new(OkType), "ok")]).WithBody(" => new(ok);")) + .WithMethod(ClassName, "Err", m => m.Static().WithArguments([new(new(ErrType), "err")]).WithBody(" => new(err);")) + .WithImplicitConversionOperatorFrom(new(OkType), " => new(x);") + .WithImplicitConversionOperatorFrom(new(ErrType), " => new(x);"); + + AddConversionsForUnions(builder); + } - internal override string OkImplicitConverter + private void AddConversionsForUnions(IClassBuilder builder) { - get + if (HasCollidingParameters()) { - var implicitConversions = new StringBuilder(); - implicitConversions.AppendLine($@"public static implicit operator {ClassName}({OkDisplayName} _) => new {ClassName}(_);"); + return; + } - if (!HasCollidingParameters() && TryGetImplicitConvertersForUnionType(OkType, ResultParameterType.Ok, out var converters)) - { - implicitConversions.AppendLine(converters); - } + var okUnions = GetTypesNeedingImplicitConversions(OkType, ResultParameterType.Ok); + var errUnions = GetTypesNeedingImplicitConversions(ErrType, ResultParameterType.Err); - return implicitConversions.ToString(); + foreach (var ok in okUnions) + { + builder.WithImplicitConversionOperatorFrom(new(ok), " => new(x);"); } - } - internal override string ErrImplicitConverter - { - get + foreach (var err in errUnions) { - var implicitConversions = new StringBuilder(); - implicitConversions.AppendLine($@"public static implicit operator {ClassName}({ErrDisplayName} _) => new {ClassName}(_);"); - - if (!HasCollidingParameters() && TryGetImplicitConvertersForUnionType(ErrType, ResultParameterType.Err, out var converters)) - { - implicitConversions.AppendLine(converters); - } - - return implicitConversions.ToString(); + builder.WithImplicitConversionOperatorFrom(new(err), " => new(x);"); } } diff --git a/Funzo.SourceGenerators/Generators/Results/ResultGenerator.cs b/Funzo.SourceGenerators/Generators/Results/ResultGenerator.cs index bcfb446..7b23b93 100644 --- a/Funzo.SourceGenerators/Generators/Results/ResultGenerator.cs +++ b/Funzo.SourceGenerators/Generators/Results/ResultGenerator.cs @@ -1,8 +1,8 @@ using Funzo.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Sorse.BuilderInterfaces; using System.Collections.Generic; using System.Collections.Immutable; -using System.Text; namespace Funzo.SourceGenerators.Generators.Results; @@ -17,14 +17,14 @@ protected ResultGenerator(MarkedType symbolWithAttribute) _symbolWithAttribute = symbolWithAttribute; } - internal abstract string ClassDefinition { get; } - internal abstract string OkConstructor { get; } - internal abstract string ErrConstructor { get; } - internal abstract string OkStaticHelper { get; } - internal abstract string ErrStaticHelper { get; } - internal abstract string OkImplicitConverter { get; } - internal abstract string ErrImplicitConverter { get; } + internal void GenerateResult(IClassBuilder builder) + { + builder.Partial(); + + GenerateResultInner(builder); + } + protected abstract void GenerateResultInner(IClassBuilder builder); protected string ClassName => $"{ClassSymbol.Name}"; protected bool TryGetUnionTypes(ITypeSymbol type, out IEnumerable types) @@ -43,24 +43,13 @@ protected bool TryGetUnionTypes(ITypeSymbol type, out IEnumerable t } } - protected bool TryGetImplicitConvertersForUnionType(ITypeSymbol type, ResultParameterType parameterType, out string converters) + protected IEnumerable GetTypesNeedingImplicitConversions(ITypeSymbol type, ResultParameterType parameterType) { if (!TryGetUnionTypes(type, out var unionTypes)) { - converters = string.Empty; - return false; - } - - var sb = new StringBuilder(); - - var ctor = parameterType is ResultParameterType.Ok ? "Ok" : "Err"; - - foreach (var unionType in unionTypes) - { - sb.AppendLine($@"public static implicit operator {ClassName}({unionType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} _) => {ctor}(_);"); + return []; } - converters = sb.ToString(); - return true; + return unionTypes; } } diff --git a/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs b/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs index c975e36..6564dab 100644 --- a/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs +++ b/Funzo.SourceGenerators/Generators/Unions/UnionGenerator.cs @@ -27,18 +27,7 @@ internal class UnionGenerator : GeneratorBase var sorse = WithSorse.CreateNamespaceScope(classSymbol.ContainingNamespace.ToDisplayString(), []); - // TODO: Needs some rework. Feels off - if (!type.ContainerClasses.Any()) - { - sorse.AddClass(className, AddUnionClass(type)); - } - else - { - var builder = GetClassBuilderFromContainingClasses(type, sorse); - - // Add the main partial class - builder.WithInnerClass(className, AddUnionClass(type)); - } + sorse.AddClassWithInnerClasses(type, AddUnionClass(type)); var src = sorse.GetSource(); @@ -66,43 +55,6 @@ private Action AddUnionClass(MarkedType type) => } }; - private IClassBuilder GetClassBuilderFromContainingClasses(MarkedType type, INamespaceScope sorse) - { - // Create the first class in the namespace - var container = type.ContainerClasses.Pop(); - - IClassBuilder builder = null!; - - sorse.AddClass(container.Name, b => - { - builder = b; - AddContainer(b, container); - }); - - // Nest the rest - foreach (var c in type.ContainerClasses) - { - builder.WithInnerClass(c.Name, b => - { - builder = b; - AddContainer(b, c); - }); - - } - - return builder; - - static void AddContainer(IClassBuilder builder, ContainerClass c) - { - builder.Partial(); - - if (c.IsStatic) - { - builder.Static(); - } - } - } - private bool HasErrors(SourceProductionContext context, MarkedType type) { var symbol = type.Symbol; diff --git a/Funzo.SourceGenerators/Helpers/SorseExtensions.cs b/Funzo.SourceGenerators/Helpers/SorseExtensions.cs new file mode 100644 index 0000000..d08db6e --- /dev/null +++ b/Funzo.SourceGenerators/Helpers/SorseExtensions.cs @@ -0,0 +1,66 @@ +using Sorse; +using Sorse.BuilderInterfaces; +using System; +using System.Linq; + +namespace Funzo.SourceGenerators.Helpers; + +internal static class SorseExtensions +{ + internal static void AddClassWithInnerClasses(this INamespaceScope sorse, MarkedType type, Action builderAction) + { + var classSymbol = type.Symbol; + var className = $"{classSymbol.Name}"; + var typeArguments = type.AttributeTypeArguments; + + // TODO: Needs some rework. Feels off + if (!type.ContainerClasses.Any()) + { + sorse.AddClass(className, builderAction); + } + else + { + var builder = GetClassBuilderFromContainingClasses(type, sorse); + + // Add the main partial class + builder.WithInnerClass(className, builderAction); + } + } + + private static IClassBuilder GetClassBuilderFromContainingClasses(MarkedType type, INamespaceScope sorse) + { + // Create the first class in the namespace + var container = type.ContainerClasses.Pop(); + + IClassBuilder builder = null!; + + sorse.AddClass(container.Name, b => + { + builder = b; + AddContainer(b, container); + }); + + // Nest the rest + foreach (var c in type.ContainerClasses) + { + builder.WithInnerClass(c.Name, b => + { + builder = b; + AddContainer(b, c); + }); + + } + + return builder; + + static void AddContainer(IClassBuilder builder, ContainerClass c) + { + builder.Partial(); + + if (c.IsStatic) + { + builder.Static(); + } + } + } +}