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

Switch to ForAttributeWithMetadataName #41

Merged
merged 3 commits into from
Dec 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 30 additions & 45 deletions .github/workflows/BuildAndPack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
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;
Expand All @@ -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