diff --git a/LanguageExt.CodeGen/CodeGenUtil.cs b/LanguageExt.CodeGen/CodeGenUtil.cs index 15119533f..9eb148d7f 100644 --- a/LanguageExt.CodeGen/CodeGenUtil.cs +++ b/LanguageExt.CodeGen/CodeGenUtil.cs @@ -226,6 +226,13 @@ public static SyntaxToken MakeFirstCharUpper(SyntaxToken identifier) return SyntaxFactory.Identifier(id2); } + public static string MakeFirstCharUpper(string identifier) + { + var id = identifier; + var id2 = $"{Char.ToUpper(id[0])}{id.Substring(1)}"; + return id2; + } + public static SyntaxToken MakeFirstCharLower(SyntaxToken identifier) { var id = identifier.ToString(); @@ -233,6 +240,13 @@ public static SyntaxToken MakeFirstCharLower(SyntaxToken identifier) return SyntaxFactory.Identifier(id2); } + public static string MakeFirstCharLower(string identifier) + { + var id = identifier; + var id2 = $"{Char.ToLower(id[0])}{id.Substring(1)}"; + return id2; + } + static bool FirstCharIsUpper(string name) => name.Length > 0 && Char.IsUpper(name[0]); @@ -509,12 +523,21 @@ public static MemberAccessExpressionSyntax PreludeMember(SimpleNameSyntax name) IdentifierName("Prelude")), name); + public static HashSet MethodNames(SyntaxList list) => + new HashSet( + list.Select(MethodName) + .Where(m => !String.IsNullOrWhiteSpace(m)) + .Distinct()); + public static HashSet MemberNames(SyntaxList list) => new HashSet( list.Select(MemberName) .Where(m => !String.IsNullOrWhiteSpace(m)) .Distinct()); + static string MethodName(MemberDeclarationSyntax decl) => + decl is MethodDeclarationSyntax m ? m.Identifier.Text : ""; + static string MemberName(MemberDeclarationSyntax decl) => decl is PropertyDeclarationSyntax p ? p.Identifier.Text : decl is MethodDeclarationSyntax m ? m.Identifier.Text diff --git a/LanguageExt.CodeGen/UnionAttribute.cs b/LanguageExt.CodeGen/UnionAttribute.cs new file mode 100644 index 000000000..6955ec447 --- /dev/null +++ b/LanguageExt.CodeGen/UnionAttribute.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using CodeGeneration.Roslyn; +using LanguageExt.CodeGen; + +namespace LanguageExt +{ + /// + /// Union attribute + /// + [AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = true)] + [CodeGenerationAttribute(typeof(UnionGenerator))] + [Conditional("CodeGeneration")] + public class UnionAttribute : Attribute + { + } +} diff --git a/LanguageExt.CodeGen/UnionGenerator.cs b/LanguageExt.CodeGen/UnionGenerator.cs new file mode 100644 index 000000000..b3ea53e25 --- /dev/null +++ b/LanguageExt.CodeGen/UnionGenerator.cs @@ -0,0 +1,257 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CodeGeneration.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Microsoft.CodeAnalysis.CSharp; +using System.ComponentModel; + +namespace LanguageExt.CodeGen +{ + public class UnionGenerator : ICodeGenerator + { + public UnionGenerator(AttributeData attributeData) + { + } + + public Task> GenerateAsync( + TransformationContext context, + IProgress progress, + CancellationToken cancellationToken) + { + if (context.ProcessingNode is InterfaceDeclarationSyntax applyTo) + { + var cases = applyTo.Members + .Where(m => m is MethodDeclarationSyntax) + .Select(m => m as MethodDeclarationSyntax) + .Select(m => MakeCaseClass(applyTo, m)) + .ToList(); + + var staticCtorClass = MakeStaticConstructorClass(applyTo); + + return Task.FromResult(SyntaxFactory.List().AddRange(cases).Add(staticCtorClass)); + } + else + { + return Task.FromResult(SyntaxFactory.List()); + } + } + + ClassDeclarationSyntax MakeStaticConstructorClass(InterfaceDeclarationSyntax applyTo) + { + var @class = ClassDeclaration(applyTo.Identifier) + .WithModifiers( + TokenList(new[] { Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword) })); + + var returnType = ParseTypeName($"{applyTo.Identifier}{applyTo.TypeParameterList}"); + + var cases = applyTo.Members + .Where(m => m is MethodDeclarationSyntax) + .Select(m => m as MethodDeclarationSyntax) + .Select(m => MakeCaseCtorFunction(applyTo, returnType, m)) + .ToList(); + + return @class.WithMembers(List(cases)); + } + + MemberDeclarationSyntax MakeCaseCtorFunction(InterfaceDeclarationSyntax applyTo, TypeSyntax returnType, MethodDeclarationSyntax method) + { + var typeParamList = applyTo.TypeParameterList; + if (method.TypeParameterList != null) + { + typeParamList = typeParamList.AddParameters(method.TypeParameterList.Parameters.ToArray()); + } + + var thisType = ParseTypeName($"{method.Identifier.Text}{typeParamList}"); + + var args = CodeGenUtil.Interleave( + method.ParameterList + .Parameters + .Select(p => (SyntaxNodeOrToken)Argument(IdentifierName(p.Identifier.Text))) + .ToArray(), + Token(SyntaxKind.CommaToken)); + + var @case = MethodDeclaration(returnType, method.Identifier) + .WithModifiers( + TokenList( + new[]{ + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword)})) + .WithParameterList(method.ParameterList) + .WithExpressionBody( + ArrowExpressionClause( + ObjectCreationExpression(thisType) + .WithArgumentList( + ArgumentList( + SeparatedList(args))))) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)); + + if(typeParamList != null) + { + @case = @case.WithTypeParameterList(typeParamList); + } + return @case; + } + + static ClassDeclarationSyntax MakeCaseClass(InterfaceDeclarationSyntax applyTo, MethodDeclarationSyntax method) + { + var modifiers = SyntaxFactory.TokenList( + Enumerable.Concat( + applyTo.Modifiers.Where(t => !t.IsKind(SyntaxKind.PartialKeyword)).AsEnumerable(), + new[] { SyntaxFactory.Token(SyntaxKind.PartialKeyword) })); + + + var @class = ClassDeclaration(method.Identifier.Text).WithModifiers(modifiers); + + var typeParamList = applyTo.TypeParameterList; + if(method.TypeParameterList != null) + { + typeParamList = typeParamList.AddParameters(method.TypeParameterList.Parameters.ToArray()); + } + + if (typeParamList != null) + { + @class = @class.WithTypeParameterList(typeParamList); + } + + if (applyTo.ConstraintClauses != null) + { + @class = @class.WithConstraintClauses(applyTo.ConstraintClauses); + } + + var returnType = ParseTypeName($"{applyTo.Identifier}{applyTo.TypeParameterList}"); + var thisType = ParseTypeName($"{method.Identifier.Text}{typeParamList}"); + var thisRecordType = ParseTypeName($"LanguageExt.Record<{method.Identifier.Text}{typeParamList}>"); + + var ctor = MakeConstructor(@class, returnType, method); + + var fields = method.ParameterList + .Parameters + .Select(p => MakeField(returnType, p)) + .ToList(); + + var impl = applyTo.Members + .Where(m => m is MethodDeclarationSyntax) + .Select(m => m as MethodDeclarationSyntax) + .Select(m => MakeExplicitInterfaceImpl(returnType, m)) + .ToList(); + + fields.AddRange(impl); + fields.Add(ctor); + + @class = @class.WithMembers(List(fields)); + + @class = @class.WithBaseList( + BaseList( + SeparatedList( + new SyntaxNodeOrToken[]{ + SimpleBaseType(thisRecordType), + Token(SyntaxKind.CommaToken), + SimpleBaseType(returnType) + }))); + + return @class; + } + + static MemberDeclarationSyntax MakeExplicitInterfaceImpl(TypeSyntax returnType, MethodDeclarationSyntax @case) + { + var method = MethodDeclaration(@case.ReturnType, @case.Identifier) + .WithExplicitInterfaceSpecifier( + ExplicitInterfaceSpecifier(ParseName(returnType.ToString()))) + .WithParameterList(@case.ParameterList) + .WithExpressionBody( + ArrowExpressionClause( + ThrowExpression( + ObjectCreationExpression( + QualifiedName( + IdentifierName("System"), + IdentifierName("NotSupportedException"))) + .WithArgumentList(ArgumentList())))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + + if(@case.TypeParameterList != null) + { + method = method.WithTypeParameterList(@case.TypeParameterList); + } + return method; + } + + static MemberDeclarationSyntax MakeConstructor(ClassDeclarationSyntax @class, TypeSyntax returnType, MethodDeclarationSyntax method) + { + var assignments = method.ParameterList + .Parameters + .Select(p => + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(CodeGenUtil.MakeFirstCharUpper(p.Identifier.Text)), + IdentifierName(p.Identifier.Text)))); + + return ConstructorDeclaration(Identifier(method.Identifier.Text)) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithParameterList(method.ParameterList) + .WithBody(Block(List(assignments))); + } + + static MemberDeclarationSyntax MakeField(TypeSyntax returnType, ParameterSyntax p) + { + var fieldName = CodeGenUtil.MakeFirstCharUpper(p.Identifier.Text); + + var field = FieldDeclaration( + VariableDeclaration(p.Type) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier(fieldName))))) + .WithModifiers( + TokenList( + new[]{ + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.ReadOnlyKeyword)})) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); ; + + return field; + } + } +} + +//public interface Option +//{ +// Option Some(A value); +// Option None(); +//} + +//public class Some : Option, Record> +//{ +// public readonly A Value; +// public Some(A value) +// { +// Value = value; +// } + +// [EditorBrowsable(EditorBrowsableState.Never)] +// Option Option.None() => new None(); +// [EditorBrowsable(EditorBrowsableState.Never)] +// Option Option.Some(A value) => new Some(value); +//} + +//public class None : Option, Record> +//{ +// public None() +// { +// } + +// [EditorBrowsable(EditorBrowsableState.Never)] +// Option Option.None() => new None(); +// [EditorBrowsable(EditorBrowsableState.Never)] +// Option Option.Some(A value) => new Some(value); +//} + +//public static partial class Option +//{ +// public static Option Some(A value) => new Some(value); +// public static Option None() => new None(); +//} diff --git a/Samples/TestBed/TestCodeGen.cs b/Samples/TestBed/TestCodeGen.cs index 0c121a576..fa01759b5 100644 --- a/Samples/TestBed/TestCodeGen.cs +++ b/Samples/TestBed/TestCodeGen.cs @@ -139,6 +139,13 @@ public TestWith4(string @new, string @class, string @static, string @while) While = @while; } } + + [Union] + public interface Maybe + { + Maybe Just(A value); + Maybe Nothing(); + } }