Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace NetEscapades.EnumGenerators.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DuplicateExtensionClassAnalyzer: DiagnosticAnalyzer
{
public const string DiagnosticId = "NEEG001";
private static readonly DiagnosticDescriptor Rule = new(
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
id: DiagnosticId,
#pragma warning restore RS2008
title: "Duplicate generated extension class",
messageFormat:
"The generated extension class '{1}.{2}' for enum '{0}' clashes with other generated extension classes. Use ExtensionClassNamespace or ExtensionClassName to specify a unique combination.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
customTags: "CompilationEnd");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
// Analyze symbols instead of syntax
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

#pragma warning disable RS1012 // 'startContext' does not register any analyzer actions - false positive
context.RegisterCompilationStartAction(startContext =>
#pragma warning restore RS1012 // 'startContext' does not register any analyzer actions - false positive
{
var enumMap = new ConcurrentDictionary<Tuple<string, string>, List<Tuple<Location, string>>>();

startContext.RegisterSymbolAction(symbolContext =>
{
var ct = symbolContext.CancellationToken;
var enumSymbol = (INamedTypeSymbol)symbolContext.Symbol;
if (enumSymbol.TypeKind != TypeKind.Enum)
{
return;
}

Location? location = null;
string? ns = null;
string? name = null;
foreach (var attributeData in enumSymbol.GetAttributes())
{
if (ct.IsCancellationRequested)
{
return;
}

if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name))
{
location = attributeData.ApplicationSyntaxReference?.GetSyntax(ct).GetLocation()
?? enumSymbol.Locations[0];
break;
}
}

if (location is null || ct.IsCancellationRequested)
{
return;
}

// we have the attribute, get the calculated names
ns ??= EnumGenerator.GetEnumExtensionNamespace(enumSymbol);
name ??= EnumGenerator.GetEnumExtensionName(enumSymbol);

enumMap.AddOrUpdate(new(ns, name),
_ => [new(location, enumSymbol.Name)],
(_, list) =>
{
list.Add(new(location, enumSymbol.Name));
return list;
});
}, SymbolKind.NamedType);

startContext.RegisterCompilationEndAction(endContext =>
{
foreach (var kvp in enumMap)
{
var duplicates = kvp.Value;
if (duplicates.Count > 1)
{
foreach (var symbol in duplicates)
{
var ns = kvp.Key.Item1;
var name = kvp.Key.Item2;
var diag = Diagnostic.Create(Rule, symbol.Item1,
symbol.Item2, ns, name);
endContext.ReportDiagnostic(diag);
}
}
}
});
});
}
}
49 changes: 31 additions & 18 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,36 +143,49 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
continue;
}

if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name);
}

return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
}

internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData, ref string? nameSpace, ref string? name)
{
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
{
return false;
}

foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
{
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;
}
}

return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
return true;
}

internal static string GetEnumExtensionNamespace(INamedTypeSymbol enumSymbol)
=> enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();

internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
=> enumSymbol.Name + "Extensions";

static EnumToGenerate? TryExtractEnumSymbol(INamedTypeSymbol enumSymbol, string? name, string? nameSpace, bool hasFlags)
{
name ??= enumSymbol.Name + "Extensions";
nameSpace ??= enumSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : enumSymbol.ContainingNamespace.ToString();
name ??= GetEnumExtensionName(enumSymbol);
nameSpace ??= GetEnumExtensionNamespace(enumSymbol);

string fullyQualifiedName = enumSymbol.ToString();
string underlyingType = enumSymbol.EnumUnderlyingType?.ToString() ?? "int";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Polyfill" Version="1.32.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
13 changes: 9 additions & 4 deletions src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ public static class SourceGenerationHelper

""";

public const string Attribute =
$$"""
{{Header}}
#if NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES
public const string AttributeDefinitions =
"""
namespace NetEscapades.EnumGenerators
{
/// <summary>
Expand Down Expand Up @@ -97,6 +95,13 @@ public class EnumExtensionsAttribute<T> : System.Attribute
public bool IsInterceptable { get; set; } = true;
}
}
""";

public const string Attribute =
$$"""
{{Header}}
#if NETESCAPADES_ENUMGENERATORS_EMBED_ATTRIBUTES
{{AttributeDefinitions}}
#endif

""";
Expand Down
Loading
Loading