Skip to content

Commit

Permalink
Switch to ForAttributeWithMetadataName (#41)
Browse files Browse the repository at this point in the history
* Switch to ForAttributeWithMetadataName

* Update target frameworks

* Update github action with latest sdks
  • Loading branch information
andrewlock authored Dec 17, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 0952fee commit 8f30633
Showing 13 changed files with 625 additions and 247 deletions.
75 changes: 30 additions & 45 deletions .github/workflows/BuildAndPack.yml
Original file line number Diff line number Diff line change
@@ -32,22 +32,17 @@ jobs:
name: ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
include-prerelease: true
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '2.1.x'
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: |
7.0.x
6.0.x
5.0.x
3.1.x
2.1.x
- name: Cache .nuke/temp, ~/.nuget/packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
.nuke/temp
@@ -69,22 +64,17 @@ jobs:
name: windows-latest
runs-on: windows-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
include-prerelease: true
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '2.1.x'
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: |
7.0.x
6.0.x
5.0.x
3.1.x
2.1.x
- name: Cache .nuke/temp, ~/.nuget/packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
.nuke/temp
@@ -106,22 +96,17 @@ jobs:
name: macOS-latest
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
include-prerelease: true
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.x'
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '2.1.x'
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: |
7.0.x
6.0.x
5.0.x
3.1.x
2.1.x
- name: Cache .nuke/temp, ~/.nuget/packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
.nuke/temp
241 changes: 73 additions & 168 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -18,217 +17,123 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"EnumExtensionsAttribute.g.cs", SourceText.From(SourceGenerationHelper.Attribute, Encoding.UTF8)));

IncrementalValuesProvider<EnumDeclarationSyntax> enumDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null)!;
IncrementalValuesProvider<EnumToGenerate?> enumsToGenerate = context.SyntaxProvider
.ForAttributeWithMetadataName(
EnumExtensionsAttribute,
predicate: (node, _) => node is EnumDeclarationSyntax,
transform: GetTypeToGenerate)
.Where(static m => m is not null);

IncrementalValueProvider<(Compilation, ImmutableArray<EnumDeclarationSyntax>)> compilationAndEnums
= context.CompilationProvider.Combine(enumDeclarations.Collect());

context.RegisterSourceOutput(compilationAndEnums,
static (spc, source) => Execute(source.Item1, source.Item2, spc));
context.RegisterSourceOutput(enumsToGenerate,
static (spc, enumToGenerate) => Execute(in enumToGenerate, spc));
}

static bool IsSyntaxTargetForGeneration(SyntaxNode node)
=> node is EnumDeclarationSyntax m && m.AttributeLists.Count > 0;

static EnumDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
static void Execute(in EnumToGenerate? enumToGenerate, SourceProductionContext context)
{
// we know the node is a EnumDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var enumDeclarationSyntax = (EnumDeclarationSyntax)context.Node;

// loop through all the attributes on the method
foreach (AttributeListSyntax attributeListSyntax in enumDeclarationSyntax.AttributeLists)
if (enumToGenerate is { } eg)
{
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
{
// weird, we couldn't get the symbol, ignore it
continue;
}

INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
string fullName = attributeContainingTypeSymbol.ToDisplayString();

// Is the attribute the [EnumExtensions] attribute?
if (fullName == EnumExtensionsAttribute)
{
// return the enum
return enumDeclarationSyntax;
}
}
StringBuilder sb = new StringBuilder();
var result = SourceGenerationHelper.GenerateExtensionClass(sb, in eg);
context.AddSource(eg.Name + "_EnumExtensions.g.cs", SourceText.From(result, Encoding.UTF8));
}

// we didn't find the attribute we were looking for
return null;
}

static void Execute(Compilation compilation, ImmutableArray<EnumDeclarationSyntax> enums, SourceProductionContext context)
static EnumToGenerate? GetTypeToGenerate(GeneratorAttributeSyntaxContext context, CancellationToken ct)
{
if (enums.IsDefaultOrEmpty)
INamedTypeSymbol? enumSymbol = context.TargetSymbol as INamedTypeSymbol;
if (enumSymbol is null)
{
// nothing to do yet
return;
// nothing to do if this type isn't available
return null;
}

IEnumerable<EnumDeclarationSyntax> distinctEnums = enums.Distinct();
ct.ThrowIfCancellationRequested();

List<EnumToGenerate> enumsToGenerate = GetTypesToGenerate(compilation, distinctEnums, context.CancellationToken);
if (enumsToGenerate.Count > 0)
string name = enumSymbol.Name + "Extensions";
string nameSpace = enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();
var hasFlags = false;

foreach (AttributeData attributeData in enumSymbol.GetAttributes())
{
StringBuilder sb = new StringBuilder();
foreach (var enumToGenerate in enumsToGenerate)
if (attributeData.AttributeClass?.Name == "HasFlagsAttribute" &&
attributeData.AttributeClass.ToDisplayString() == HasFlagsAttribute)
{
sb.Clear();
var result = SourceGenerationHelper.GenerateExtensionClass(sb, enumToGenerate);
context.AddSource(enumToGenerate.Name + "_EnumExtensions.g.cs", SourceText.From(result, Encoding.UTF8));
hasFlags = true;
continue;
}
}
}

static List<EnumToGenerate> GetTypesToGenerate(Compilation compilation, IEnumerable<EnumDeclarationSyntax> enums, CancellationToken ct)
{
var enumsToGenerate = new List<EnumToGenerate>();
INamedTypeSymbol? enumAttribute = compilation.GetTypeByMetadataName(EnumExtensionsAttribute);
if (enumAttribute == null)
{
// nothing to do if this type isn't available
return enumsToGenerate;
}

INamedTypeSymbol? displayAttribute = compilation.GetTypeByMetadataName(DisplayAttribute);
INamedTypeSymbol? hasFlagsAttribute = compilation.GetTypeByMetadataName(HasFlagsAttribute);
foreach (var enumDeclarationSyntax in enums)
{
// stop if we're asked to
ct.ThrowIfCancellationRequested();

SemanticModel semanticModel = compilation.GetSemanticModel(enumDeclarationSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(enumDeclarationSyntax) is not INamedTypeSymbol enumSymbol)
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
attributeData.AttributeClass.ToDisplayString() != EnumExtensionsAttribute)
{
// report diagnostic, something went wrong
continue;
}

string name = enumSymbol.Name + "Extensions";
string nameSpace = enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();
var hasFlags = false;

foreach (AttributeData attributeData in enumSymbol.GetAttributes())
foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
{
if (hasFlagsAttribute is not null && hasFlagsAttribute.Equals(attributeData.AttributeClass, SymbolEqualityComparer.Default))
{
hasFlags = true;
continue;
}

if (!enumAttribute.Equals(attributeData.AttributeClass, SymbolEqualityComparer.Default))
if (namedArgument.Key == "ExtensionClassNamespace"
&& namedArgument.Value.Value?.ToString() is { } ns)
{
nameSpace = ns;
continue;
}

foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
if (namedArgument.Key == "ExtensionClassName"
&& namedArgument.Value.Value?.ToString() is { } n)
{
if (namedArgument.Key == "ExtensionClassNamespace"
&& namedArgument.Value.Value?.ToString() is { } ns)
{
nameSpace = ns;
continue;
}

if (namedArgument.Key == "ExtensionClassName"
&& namedArgument.Value.Value?.ToString() is { } n)
{
name = n;
}
name = n;
}
}
}

string fullyQualifiedName = enumSymbol.ToString();
string underlyingType = enumSymbol.EnumUnderlyingType?.ToString() ?? "int";
string fullyQualifiedName = enumSymbol.ToString();
string underlyingType = enumSymbol.EnumUnderlyingType?.ToString() ?? "int";

var enumMembers = enumSymbol.GetMembers();
var members = new List<KeyValuePair<string, EnumValueOption>>(enumMembers.Length);
var displayNames = new HashSet<string>();
var isDisplayNameTheFirstPresence = false;
var enumMembers = enumSymbol.GetMembers();
var members = new List<(string, EnumValueOption)>(enumMembers.Length);
HashSet<string>? displayNames = null;
var isDisplayNameTheFirstPresence = false;

foreach (var member in enumMembers)
foreach (var member in enumMembers)
{
if (member is not IFieldSymbol field
|| field.ConstantValue is null)
{
if (member is not IFieldSymbol field
|| field.ConstantValue is null)
continue;
}

string? displayName = null;
foreach (var attribute in member.GetAttributes())
{

if (attribute.AttributeClass?.Name != "DisplayAttribute" ||
attribute.AttributeClass.ToDisplayString() != DisplayAttribute)
{
continue;
}

string? displayName = null;
if (displayAttribute is not null)
foreach (var namedArgument in attribute.NamedArguments)
{
foreach (var attribute in member.GetAttributes())
if (namedArgument.Key == "Name" && namedArgument.Value.Value?.ToString() is { } dn)
{
if(!displayAttribute.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default))
{
continue;
}

foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Key == "Name" && namedArgument.Value.Value?.ToString() is { } dn)
{
displayName = dn;
isDisplayNameTheFirstPresence = displayNames.Add(displayName);
break;
}
}
displayName = dn;
displayNames ??= new();
isDisplayNameTheFirstPresence = displayNames.Add(displayName);
break;
}
}

members.Add(new KeyValuePair<string, EnumValueOption>(member.Name, new EnumValueOption(displayName, isDisplayNameTheFirstPresence)));
}

enumsToGenerate.Add(new EnumToGenerate(
name: name,
fullyQualifiedName: fullyQualifiedName,
ns: nameSpace,
underlyingType: underlyingType,
isPublic: enumSymbol.DeclaredAccessibility == Accessibility.Public,
hasFlags: hasFlags,
names: members,
isDisplayAttributeUsed: displayNames.Count > 0));
}

return enumsToGenerate;
}

static string GetNamespace(EnumDeclarationSyntax enumDeclarationSyntax)
{
// determine the namespace the class is declared in, if any
string nameSpace = string.Empty;
SyntaxNode? potentialNamespaceParent = enumDeclarationSyntax.Parent;
while (potentialNamespaceParent != null &&
potentialNamespaceParent is not NamespaceDeclarationSyntax
&& potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
{
potentialNamespaceParent = potentialNamespaceParent.Parent;
}

if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
{
nameSpace = namespaceParent.Name.ToString();
while (true)
{
if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent)
{
break;
}

namespaceParent = parent;
nameSpace = $"{namespaceParent.Name}.{nameSpace}";
}
members.Add((member.Name, new EnumValueOption(displayName, isDisplayNameTheFirstPresence)));
}

return nameSpace;
return new EnumToGenerate(
name: name,
fullyQualifiedName: fullyQualifiedName,
ns: nameSpace,
underlyingType: underlyingType,
isPublic: enumSymbol.DeclaredAccessibility == Accessibility.Public,
hasFlags: hasFlags,
names: members,
isDisplayAttributeUsed: displayNames?.Count > 0);
}
}
Loading

0 comments on commit 8f30633

Please sign in to comment.