diff --git a/SmartEnum.sln b/SmartEnum.sln index f5da9161..7929c76b 100644 --- a/SmartEnum.sln +++ b/SmartEnum.sln @@ -68,6 +68,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.Dapper.UnitTests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.Dapper.IntegrationTests", "test\SmartEnum.Dapper.IntegrationTests\SmartEnum.Dapper.IntegrationTests.csproj", "{ACCA93E9-EE80-490C-81A3-824086E4EA2F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartEnum.SourceGenerator", "src\SmartEnum.SourceGenerator\SmartEnum.SourceGenerator.csproj", "{B37A3D40-E954-4100-AC02-1F549B127487}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartEnum.SourceGenerator.UnitTests", "test\SmartEnum.SourceGenerator.UnitTests\SmartEnum.SourceGenerator.UnitTests.csproj", "{5829243E-F6DF-4A5E-997E-B810F95F11D3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -162,6 +166,14 @@ Global {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Release|Any CPU.Build.0 = Release|Any CPU + {B37A3D40-E954-4100-AC02-1F549B127487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B37A3D40-E954-4100-AC02-1F549B127487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B37A3D40-E954-4100-AC02-1F549B127487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B37A3D40-E954-4100-AC02-1F549B127487}.Release|Any CPU.Build.0 = Release|Any CPU + {5829243E-F6DF-4A5E-997E-B810F95F11D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5829243E-F6DF-4A5E-997E-B810F95F11D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5829243E-F6DF-4A5E-997E-B810F95F11D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5829243E-F6DF-4A5E-997E-B810F95F11D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -190,6 +202,8 @@ Global {7E08FCFA-2318-4D36-BAB5-8AFA41F00CC3} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04} {ADBD5097-87A4-492B-9399-6A4CCC53CD5A} = {79268877-BBEF-4DE2-B8D9-697F21933159} {ACCA93E9-EE80-490C-81A3-824086E4EA2F} = {EF5634F4-4667-4481-934C-D1CFA042AD0B} + {B37A3D40-E954-4100-AC02-1F549B127487} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04} + {5829243E-F6DF-4A5E-997E-B810F95F11D3} = {79268877-BBEF-4DE2-B8D9-697F21933159} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46896DE3-41B8-442F-A6FB-6AC9F11CCBCE} diff --git a/src/SmartEnum.SourceGenerator/Constants.cs b/src/SmartEnum.SourceGenerator/Constants.cs new file mode 100644 index 00000000..dc1f21c0 --- /dev/null +++ b/src/SmartEnum.SourceGenerator/Constants.cs @@ -0,0 +1,17 @@ +namespace Ardalis.SmartEnum.SourceGenerator; + +internal static class Constants +{ + + public static string SmartEnumGeneratorAttribute = """ + namespace Ardalis.SmartEnum + { + [System.AttributeUsage(AttributeTargets.Class)] + public class SmartEnumGeneratorAttribute : System.Attribute + { + + } + } + """; +} + diff --git a/src/SmartEnum.SourceGenerator/CustomSyntaxReceiver.cs b/src/SmartEnum.SourceGenerator/CustomSyntaxReceiver.cs new file mode 100644 index 00000000..5567f421 --- /dev/null +++ b/src/SmartEnum.SourceGenerator/CustomSyntaxReceiver.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Ardalis.SmartEnum.SourceGenerator; + +internal class CustomSyntaxReceiver: ISyntaxReceiver +{ + public List Classes { get; private set; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } cds ) + { + Classes.Add(cds); + } + } +} \ No newline at end of file diff --git a/src/SmartEnum.SourceGenerator/GeneratedClassType.cs b/src/SmartEnum.SourceGenerator/GeneratedClassType.cs new file mode 100644 index 00000000..1b81cd7d --- /dev/null +++ b/src/SmartEnum.SourceGenerator/GeneratedClassType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SmartEnum.SourceGenerator +{ + internal enum GeneratedClassType + { + Pure, + SmartEnum, + SmartFlagEnum, + } +} diff --git a/src/SmartEnum.SourceGenerator/SmartEnum.SourceGenerator.csproj b/src/SmartEnum.SourceGenerator/SmartEnum.SourceGenerator.csproj new file mode 100644 index 00000000..b19334a8 --- /dev/null +++ b/src/SmartEnum.SourceGenerator/SmartEnum.SourceGenerator.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + enable + enable + latest + false + enable + true + true + Generated + + + + + + + + + + + + diff --git a/src/SmartEnum.SourceGenerator/SmartEnumSourceGenerator.cs b/src/SmartEnum.SourceGenerator/SmartEnumSourceGenerator.cs new file mode 100644 index 00000000..f392d71b --- /dev/null +++ b/src/SmartEnum.SourceGenerator/SmartEnumSourceGenerator.cs @@ -0,0 +1,152 @@ +using System.Diagnostics; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +using SmartEnum.SourceGenerator; + +namespace Ardalis.SmartEnum.SourceGenerator; + +[Generator] +public class SmartEnumSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { +#if DEBUG + //attach debugger when debugging + //if (!Debugger.IsAttached) + //{ + // Debugger.Launch(); + //} +#endif + + context.RegisterForSyntaxNotifications(() => new CustomSyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + // add SmartEnum generator attribute + context.AddSource( + "SmartEnumGeneratorAttribute.g.cs", + SourceText.From(Constants.SmartEnumGeneratorAttribute, Encoding.UTF8)); + + // get classes with attributes count > 0 + var syntaxReceiver = (CustomSyntaxReceiver)context.SyntaxReceiver; + var cdss = syntaxReceiver?.Classes; + foreach (var cds in cdss) + { + if (cds == null) + { + continue; + } + + // check if class have SmartEnumGenerator attribute + if (!cds.AttributeLists.Any(x => x.Attributes.Any(xx => xx.Name.ToFullString() == "SmartEnumGenerator"))) + { + continue; + } + + // get class namespace + var cnamespace = ( + (QualifiedNameSyntax)cds.Parent.ChildNodes().FirstOrDefault(x => x.GetType() == typeof(QualifiedNameSyntax)) + ).ToFullString(); + + + // get class name + var cname = cds.Identifier.ToString(); + + // check if class is not partial then skipped it + if (!cds.Modifiers.Any(x => x.ValueText == "partial")) + { + continue; + } + + var ctype = GeneratedClassType.Pure; + var ctypeStr = $" : SmartEnum <{cname}>"; + var smartEnumType = cds.BaseList?.Types.Any(x => x?.ToString() == $"SmartEnum<{cname}>") ?? false; + if(smartEnumType) + { + ctype = GeneratedClassType.SmartEnum; + ctypeStr = $""; + } + + var smartFlagEnumType = cds.BaseList?.Types.Any(x => x?.ToString() == $"SmartFlagEnum<{cname}>") ?? false; + if (smartFlagEnumType) + { + ctype = GeneratedClassType.SmartFlagEnum; + ctypeStr = $""; + } + + + var variables = new List(); + + // get class nodes + foreach (var node in cds.ChildNodes()) + { + // get fields + if (node is FieldDeclarationSyntax { Declaration.Type: IdentifierNameSyntax ids } fds) + { + + var fieldType = ids.Identifier.Text; + if (fieldType != cname) + { + // only field with same type of class supported for now, custom class will supported in features + continue; + } + + var fieldName = fds.Declaration.Variables.First().GetText().ToString(); + variables.Add(new VariableInfo(fieldName, fieldType)); + } + } + + // build generated class + + string ctemplateStart = $$""" + using Ardalis.SmartEnum; + + namespace {{cnamespace}} + { + public sealed partial class {{cname}} {{ctypeStr}} + { + {{(ctype == GeneratedClassType.Pure ? $"public {cname}(string name, int value) : base(name, value) {{}}" : "")}} + + static {{cname}}() + { + """; + + string ctemplateEnd = $$""" + } + } + } + """; + + + var sb = new StringBuilder(); + sb.AppendLine(ctemplateStart); + // TODO another clean way + var idx = 1; + if (ctype == GeneratedClassType.SmartFlagEnum) + { + idx = 2; + } + for (int i = 0; i < variables.Count; i++) + { + sb.AppendLine($"\t\t\t{variables[i].Name} = new {cname}(nameof({variables[i].Name}), {idx});"); + if (ctype == GeneratedClassType.SmartFlagEnum) + { + idx = idx * 2; + } else + { + idx++; + } + + } + sb.AppendLine(ctemplateEnd); + context.AddSource($"{cname}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + + } + + } +} \ No newline at end of file diff --git a/src/SmartEnum.SourceGenerator/VariableInfo.cs b/src/SmartEnum.SourceGenerator/VariableInfo.cs new file mode 100644 index 00000000..e4ed45bd --- /dev/null +++ b/src/SmartEnum.SourceGenerator/VariableInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SmartEnum.SourceGenerator +{ + internal class VariableInfo + { + public string Name { get; private set; } + public string Type { get; private set; } + + public VariableInfo(string name, string type) + { + Name = name; + Type = type; + } + } +} diff --git a/test/SmartEnum.SourceGenerator.UnitTests/Permissions.cs b/test/SmartEnum.SourceGenerator.UnitTests/Permissions.cs new file mode 100644 index 00000000..6aa36db3 --- /dev/null +++ b/test/SmartEnum.SourceGenerator.UnitTests/Permissions.cs @@ -0,0 +1,10 @@ +using Ardalis.SmartEnum; + +namespace SmartEnum.SourceGenerator.UnitTests; + +[SmartEnumGenerator] +public sealed partial class Permissions +{ + public static readonly Permissions Dashboard; + public static readonly Permissions UserManagement; +} diff --git a/test/SmartEnum.SourceGenerator.UnitTests/SmartEnum.SourceGenerator.UnitTests.csproj b/test/SmartEnum.SourceGenerator.UnitTests/SmartEnum.SourceGenerator.UnitTests.csproj new file mode 100644 index 00000000..8ec2f475 --- /dev/null +++ b/test/SmartEnum.SourceGenerator.UnitTests/SmartEnum.SourceGenerator.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.1 + enable + enable + strict + latest + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/SmartEnum.SourceGenerator.UnitTests/SourceGeneratorTests.cs b/test/SmartEnum.SourceGenerator.UnitTests/SourceGeneratorTests.cs new file mode 100644 index 00000000..ca11ab28 --- /dev/null +++ b/test/SmartEnum.SourceGenerator.UnitTests/SourceGeneratorTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SmartEnum.SourceGenerator.UnitTests +{ + public class SourceGeneratorTests + { + [Fact] + public void Subscriptions_CheckNameAndValue() + { + var freeSubscriptions = Subscriptions.Free; + Assert.Equal(nameof(Subscriptions.Free), freeSubscriptions.Name); + Assert.Equal(1, freeSubscriptions.Value); + } + + [Fact] + public void Permissions_CheckValueAndName() + { + var permissions = Permissions.UserManagement; + Assert.Equal(nameof(Permissions.UserManagement), permissions.Name); + Assert.Equal(2, permissions.Value); + } + + + } +} diff --git a/test/SmartEnum.SourceGenerator.UnitTests/Subscriptions.cs b/test/SmartEnum.SourceGenerator.UnitTests/Subscriptions.cs new file mode 100644 index 00000000..b39acab4 --- /dev/null +++ b/test/SmartEnum.SourceGenerator.UnitTests/Subscriptions.cs @@ -0,0 +1,20 @@ +using Ardalis.SmartEnum; + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SmartEnum.SourceGenerator.UnitTests +{ + [SmartEnumGenerator] + public sealed partial class Subscriptions : SmartEnum + { + public static readonly Subscriptions Free; + public static readonly Subscriptions Sliver; + public static readonly Subscriptions Gold; + + public Subscriptions(string name, int value) : base(name, value) + { + } + } +} diff --git a/test/SmartEnum.SourceGenerator.UnitTests/Usings.cs b/test/SmartEnum.SourceGenerator.UnitTests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/test/SmartEnum.SourceGenerator.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file