Skip to content

Commit 81c2bb3

Browse files
authored
Create an analyzer that explicitly flags naming clash for extensions (#158)
* Extract common functions out of EnumGenerator * Add analyzer to flag duplicate extensions
1 parent bed9e9c commit 81c2bb3

File tree

6 files changed

+447
-22
lines changed

6 files changed

+447
-22
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Collections.Concurrent;
2+
using System.Collections.Immutable;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace NetEscapades.EnumGenerators.Diagnostics;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class DuplicateExtensionClassAnalyzer: DiagnosticAnalyzer
10+
{
11+
public const string DiagnosticId = "NEEG001";
12+
private static readonly DiagnosticDescriptor Rule = new(
13+
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
14+
id: DiagnosticId,
15+
#pragma warning restore RS2008
16+
title: "Duplicate generated extension class",
17+
messageFormat:
18+
"The generated extension class '{1}.{2}' for enum '{0}' clashes with other generated extension classes. Use ExtensionClassNamespace or ExtensionClassName to specify a unique combination.",
19+
category: "Usage",
20+
defaultSeverity: DiagnosticSeverity.Error,
21+
isEnabledByDefault: true,
22+
customTags: "CompilationEnd");
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
25+
ImmutableArray.Create(Rule);
26+
27+
public override void Initialize(AnalysisContext context)
28+
{
29+
// Analyze symbols instead of syntax
30+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
31+
context.EnableConcurrentExecution();
32+
33+
#pragma warning disable RS1012 // 'startContext' does not register any analyzer actions - false positive
34+
context.RegisterCompilationStartAction(startContext =>
35+
#pragma warning restore RS1012 // 'startContext' does not register any analyzer actions - false positive
36+
{
37+
var enumMap = new ConcurrentDictionary<Tuple<string, string>, List<Tuple<Location, string>>>();
38+
39+
startContext.RegisterSymbolAction(symbolContext =>
40+
{
41+
var ct = symbolContext.CancellationToken;
42+
var enumSymbol = (INamedTypeSymbol)symbolContext.Symbol;
43+
if (enumSymbol.TypeKind != TypeKind.Enum)
44+
{
45+
return;
46+
}
47+
48+
Location? location = null;
49+
string? ns = null;
50+
string? name = null;
51+
foreach (var attributeData in enumSymbol.GetAttributes())
52+
{
53+
if (ct.IsCancellationRequested)
54+
{
55+
return;
56+
}
57+
58+
if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name))
59+
{
60+
location = attributeData.ApplicationSyntaxReference?.GetSyntax(ct).GetLocation()
61+
?? enumSymbol.Locations[0];
62+
break;
63+
}
64+
}
65+
66+
if (location is null || ct.IsCancellationRequested)
67+
{
68+
return;
69+
}
70+
71+
// we have the attribute, get the calculated names
72+
ns ??= EnumGenerator.GetEnumExtensionNamespace(enumSymbol);
73+
name ??= EnumGenerator.GetEnumExtensionName(enumSymbol);
74+
75+
enumMap.AddOrUpdate(new(ns, name),
76+
_ => [new(location, enumSymbol.Name)],
77+
(_, list) =>
78+
{
79+
list.Add(new(location, enumSymbol.Name));
80+
return list;
81+
});
82+
}, SymbolKind.NamedType);
83+
84+
startContext.RegisterCompilationEndAction(endContext =>
85+
{
86+
foreach (var kvp in enumMap)
87+
{
88+
var duplicates = kvp.Value;
89+
if (duplicates.Count > 1)
90+
{
91+
foreach (var symbol in duplicates)
92+
{
93+
var ns = kvp.Key.Item1;
94+
var name = kvp.Key.Item2;
95+
var diag = Diagnostic.Create(Rule, symbol.Item1,
96+
symbol.Item2, ns, name);
97+
endContext.ReportDiagnostic(diag);
98+
}
99+
}
100+
}
101+
});
102+
});
103+
}
104+
}

src/NetEscapades.EnumGenerators/EnumGenerator.cs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -143,36 +143,49 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
143143
continue;
144144
}
145145

146-
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
147-
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
146+
TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name);
147+
}
148+
149+
return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
150+
}
151+
152+
internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData, ref string? nameSpace, ref string? name)
153+
{
154+
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
155+
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
156+
{
157+
return false;
158+
}
159+
160+
foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
161+
{
162+
if (namedArgument.Key == "ExtensionClassNamespace"
163+
&& namedArgument.Value.Value?.ToString() is { } ns)
148164
{
165+
nameSpace = ns;
149166
continue;
150167
}
151168

152-
foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
169+
if (namedArgument.Key == "ExtensionClassName"
170+
&& namedArgument.Value.Value?.ToString() is { } n)
153171
{
154-
if (namedArgument.Key == "ExtensionClassNamespace"
155-
&& namedArgument.Value.Value?.ToString() is { } ns)
156-
{
157-
nameSpace = ns;
158-
continue;
159-
}
160-
161-
if (namedArgument.Key == "ExtensionClassName"
162-
&& namedArgument.Value.Value?.ToString() is { } n)
163-
{
164-
name = n;
165-
}
172+
name = n;
166173
}
167174
}
168175

169-
return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
176+
return true;
170177
}
171178

179+
internal static string GetEnumExtensionNamespace(INamedTypeSymbol enumSymbol)
180+
=> enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();
181+
182+
internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
183+
=> enumSymbol.Name + "Extensions";
184+
172185
static EnumToGenerate? TryExtractEnumSymbol(INamedTypeSymbol enumSymbol, string? name, string? nameSpace, bool hasFlags)
173186
{
174-
name ??= enumSymbol.Name + "Extensions";
175-
nameSpace ??= enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();
187+
name ??= GetEnumExtensionName(enumSymbol);
188+
nameSpace ??= GetEnumExtensionNamespace(enumSymbol);
176189

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

src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
1919
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" PrivateAssets="all" />
2021
<PackageReference Include="Polyfill" Version="1.32.1">
2122
<PrivateAssets>all</PrivateAssets>
2223
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ public static class SourceGenerationHelper
2020
2121
""";
2222

23-
public const string Attribute =
24-
$$"""
25-
{{Header}}
26-
#if NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES
23+
public const string AttributeDefinitions =
24+
"""
2725
namespace NetEscapades.EnumGenerators
2826
{
2927
/// <summary>
@@ -97,6 +95,13 @@ public class EnumExtensionsAttribute<T> : System.Attribute
9795
public bool IsInterceptable { get; set; } = true;
9896
}
9997
}
98+
""";
99+
100+
public const string Attribute =
101+
$$"""
102+
{{Header}}
103+
#if NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES
104+
{{AttributeDefinitions}}
100105
#endif
101106
102107
""";

0 commit comments

Comments
 (0)