Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add source generator #355 #356

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions SmartEnum.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
17 changes: 17 additions & 0 deletions src/SmartEnum.SourceGenerator/Constants.cs
Original file line number Diff line number Diff line change
@@ -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
{

}
}
""";
}

17 changes: 17 additions & 0 deletions src/SmartEnum.SourceGenerator/CustomSyntaxReceiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Ardalis.SmartEnum.SourceGenerator;

internal class CustomSyntaxReceiver: ISyntaxReceiver
{
public List<ClassDeclarationSyntax> Classes { get; private set; } = new List<ClassDeclarationSyntax>();

public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 } cds )
{
Classes.Add(cds);
}
}
}
13 changes: 13 additions & 0 deletions src/SmartEnum.SourceGenerator/GeneratedClassType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace SmartEnum.SourceGenerator
{
internal enum GeneratedClassType
{
Pure,
SmartEnum,
SmartFlagEnum,
}
}
24 changes: 24 additions & 0 deletions src/SmartEnum.SourceGenerator/SmartEnum.SourceGenerator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

</Project>
152 changes: 152 additions & 0 deletions src/SmartEnum.SourceGenerator/SmartEnumSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -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<VariableInfo>();

// 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));

}

}
}
18 changes: 18 additions & 0 deletions src/SmartEnum.SourceGenerator/VariableInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
10 changes: 10 additions & 0 deletions test/SmartEnum.SourceGenerator.UnitTests/Permissions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Features>strict</Features>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\SmartEnum.SourceGenerator\SmartEnum.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\..\src\SmartEnum\SmartEnum.csproj" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions test/SmartEnum.SourceGenerator.UnitTests/SourceGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}


}
}
20 changes: 20 additions & 0 deletions test/SmartEnum.SourceGenerator.UnitTests/Subscriptions.cs
Original file line number Diff line number Diff line change
@@ -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<Subscriptions>
{
public static readonly Subscriptions Free;
public static readonly Subscriptions Sliver;
public static readonly Subscriptions Gold;

public Subscriptions(string name, int value) : base(name, value)
{
}
}
}
1 change: 1 addition & 0 deletions test/SmartEnum.SourceGenerator.UnitTests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;