Skip to content

Commit 6ff64d1

Browse files
authored
Handle enums nested in generic types (#159)
Fixes #115
1 parent 81c2bb3 commit 6ff64d1

File tree

6 files changed

+493
-9
lines changed

6 files changed

+493
-9
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace NetEscapades.EnumGenerators.Diagnostics;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class EnumInGenericTypeAnalyzer : DiagnosticAnalyzer
11+
{
12+
public const string DiagnosticId = "NEEG002";
13+
public static readonly DiagnosticDescriptor Rule = new(
14+
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
15+
id: DiagnosticId,
16+
#pragma warning restore RS2008
17+
title: "Enum in generic type not supported",
18+
messageFormat: "The enum '{0}' is nested inside a generic type. [EnumExtension] attribute is not supported.",
19+
category: "Usage",
20+
defaultSeverity: DiagnosticSeverity.Warning,
21+
isEnabledByDefault: true);
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
24+
=> ImmutableArray.Create(Rule);
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
context.EnableConcurrentExecution();
30+
context.RegisterSyntaxNodeAction(AnalyzeEnumDeclaration, SyntaxKind.EnumDeclaration);
31+
}
32+
33+
private static void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context)
34+
{
35+
var enumDeclaration = (EnumDeclarationSyntax)context.Node;
36+
37+
// Check if enum has [EnumExtensions] attribute and capture its location
38+
AttributeSyntax? enumExtensionsAttribute = null;
39+
foreach (var attributeList in enumDeclaration.AttributeLists)
40+
{
41+
foreach (var attribute in attributeList.Attributes)
42+
{
43+
// Check attribute name syntactically first
44+
var attributeName = attribute.Name.ToString();
45+
if (attributeName == "EnumExtensions" || attributeName == "EnumExtensionsAttribute")
46+
{
47+
// Verify with semantic model if needed for precision
48+
var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute);
49+
if (symbolInfo.Symbol is IMethodSymbol method &&
50+
method.ContainingType.ToDisplayString() == Attributes.EnumExtensionsAttribute)
51+
{
52+
enumExtensionsAttribute = attribute;
53+
break;
54+
}
55+
}
56+
}
57+
58+
if (enumExtensionsAttribute is not null)
59+
{
60+
break;
61+
}
62+
}
63+
64+
if (enumExtensionsAttribute is null)
65+
{
66+
return;
67+
}
68+
69+
// Get the enum symbol
70+
var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclaration);
71+
if (enumSymbol is null)
72+
{
73+
return;
74+
}
75+
76+
// Check if nested in generic type
77+
if (SymbolHelpers.IsNestedInGenericType(enumSymbol))
78+
{
79+
var diagnostic = Diagnostic.Create(
80+
Rule,
81+
enumExtensionsAttribute.GetLocation(),
82+
enumSymbol.Name);
83+
84+
context.ReportDiagnostic(diagnostic);
85+
}
86+
}
87+
88+
}

src/NetEscapades.EnumGenerators/EnumGenerator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
129129

130130
ct.ThrowIfCancellationRequested();
131131

132+
// Skip enums in generic types as they won't be valid - error will be raised from analyzer
133+
if (SymbolHelpers.IsNestedInGenericType(enumSymbol))
134+
{
135+
return null;
136+
}
137+
132138
var hasFlags = false;
133139
string? nameSpace = null;
134140
string? name = null;
@@ -253,4 +259,5 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
253259
names: members,
254260
isDisplayAttributeUsed: displayNames?.Count > 0);
255261
}
262+
256263
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace NetEscapades.EnumGenerators;
4+
5+
internal static class SymbolHelpers
6+
{
7+
public static bool IsNestedInGenericType(INamedTypeSymbol enumSymbol)
8+
{
9+
var containingType = enumSymbol.ContainingType;
10+
while (containingType is not null)
11+
{
12+
if (containingType.IsGenericType)
13+
{
14+
return true;
15+
}
16+
containingType = containingType.ContainingType;
17+
}
18+
return false;
19+
}
20+
}

tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Linq;
22
using System.Threading.Tasks;
3+
using FluentAssertions;
34
using Microsoft.CodeAnalysis;
45
using Microsoft.CodeAnalysis.CSharp;
56
using NetEscapades.EnumGenerators.Interceptors;
@@ -583,4 +584,45 @@ public enum MyEnum
583584
Assert.Empty(diagnostics);
584585
return Verifier.Verify(output, Settings());
585586
}
587+
588+
[Fact]
589+
public void DoesNotGenerateWithoutAttribute()
590+
{
591+
const string input =
592+
"""
593+
public enum MyEnum
594+
{
595+
First,
596+
Second,
597+
}
598+
""";
599+
var (diagnostics, output) = TestHelpers.GetGeneratedOutput(Generators(), new(stages: [], input));
600+
601+
diagnostics.Should().BeEmpty();
602+
output.Should().BeEmpty();
603+
}
604+
605+
[Fact]
606+
public void DoesNotGenerateInNestedGenericClass()
607+
{
608+
const string input =
609+
"""
610+
using NetEscapades.EnumGenerators;
611+
612+
public class Nested<T>
613+
{
614+
[EnumExtensions]
615+
public enum MyEnum
616+
{
617+
First,
618+
Second,
619+
}
620+
}
621+
""";
622+
623+
var (diagnostics, output) = TestHelpers.GetGeneratedOutput(Generators(), new(stages: [], input));
624+
625+
diagnostics.Should().BeEmpty();
626+
output.Should().BeEmpty();
627+
}
586628
}

0 commit comments

Comments
 (0)