Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
60 changes: 60 additions & 0 deletions src/NetEscapades.EnumGenerators/Diagnostics/AnalyzerHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
Comment thread
andrewlock marked this conversation as resolved.
Outdated

namespace NetEscapades.EnumGenerators.Diagnostics;

public class AnalyzerHelpers
Comment thread
andrewlock marked this conversation as resolved.
Outdated
{
public static (INamedTypeSymbol? enumExtensionsAttr, HashSet<INamedTypeSymbol>? externalEnumTypes) GetEnumExtensionAttributes(
CompilationStartAnalysisContext ctx)
{
var enumExtensionsAttr =
ctx.Compilation.GetTypeByMetadataName(Attributes.EnumExtensionsAttribute);
var externalEnumExtensionsAttr =
ctx.Compilation.GetTypeByMetadataName(Attributes.ExternalEnumExtensionsAttribute);

if (enumExtensionsAttr is null)
{
return (enumExtensionsAttr, null);
}

// Collect all enum types that have EnumExtensions<T> attributes
var externalEnumTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
if (externalEnumExtensionsAttr is not null)
{
foreach (var attribute in ctx.Compilation.Assembly.GetAttributes())
{
if (attribute.AttributeClass is { IsGenericType: true } attrClass &&
SymbolEqualityComparer.Default.Equals(attrClass.ConstructedFrom, externalEnumExtensionsAttr) &&
attrClass.TypeArguments is [INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType])
{
externalEnumTypes.Add(enumType);
}
}
Comment thread
andrewlock marked this conversation as resolved.
Outdated
}

return (enumExtensionsAttr, externalEnumTypes);
}

public static bool IsEnumWithExtensions(
ITypeSymbol receiverType,
INamedTypeSymbol enumExtensionsAttr,
HashSet<INamedTypeSymbol> externalEnumTypes)
{
// Check if the enum has the [EnumExtensions] attribute or is referenced in EnumExtensions<T>
// First check if the enum itself has the attribute
foreach (var attributeData in receiverType.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(
attributeData.AttributeClass,
enumExtensionsAttr))
{
return true;
}
}
Comment thread
andrewlock marked this conversation as resolved.

// If not, check if it's in the external enum types (EnumExtensions<T>)
return receiverType is INamedTypeSymbol namedType
&& externalEnumTypes.Contains(namedType);
}
}
103 changes: 103 additions & 0 deletions src/NetEscapades.EnumGenerators/Diagnostics/HasFlagAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
Comment thread
andrewlock marked this conversation as resolved.
Outdated

namespace NetEscapades.EnumGenerators.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class HasFlagAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NEEG005";
public static readonly DiagnosticDescriptor Rule = new(
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
id: DiagnosticId,
#pragma warning restore RS2008
title: "Use HasFlagFast() instead of HasFlag()",
messageFormat: "Use HasFlagFast() instead of HasFlag() for better performance on enum '{0}'",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);

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

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(ctx =>
{
var (enumExtensionsAttr, externalEnumTypes) = AnalyzerHelpers.GetEnumExtensionAttributes(ctx);
if (enumExtensionsAttr is null || externalEnumTypes is null)
{
return;
}

ctx.RegisterSyntaxNodeAction(
c => AnalyzeInvocation(c, enumExtensionsAttr, externalEnumTypes),
SyntaxKind.InvocationExpression);
});
}

private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol enumExtensionsAttr, HashSet<INamedTypeSymbol> externalEnumTypes)
{
var invocation = (InvocationExpressionSyntax)context.Node;

// Check if this is a member access expression (e.g., value.HasFlag())
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
{
return;
}

// Check if the method name is "HasFlag"
if (memberAccess.Name.Identifier.Text != "HasFlag")
{
return;
}

// Check if there is exactly one argument
if (invocation.ArgumentList.Arguments.Count != 1)
{
return;
}

// Get the symbol information for the invocation
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
{
return;
}

// Verify this is the HasFlag() method from System.Enum
if (methodSymbol.Name != "HasFlag" ||
methodSymbol.Parameters.Length != 1 ||
methodSymbol.ContainingType.SpecialType != SpecialType.System_Enum)
{
return;
}

// Get the type of the receiver (the thing before .HasFlag())
var receiverType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
if (receiverType is null || receiverType.TypeKind != TypeKind.Enum)
{
return;
}

if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes))
{
return;
}

// Report the diagnostic
var diagnostic = Diagnostic.Create(
Rule,
memberAccess.Name.GetLocation(),
receiverType.Name);

context.ReportDiagnostic(diagnostic);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Collections.Immutable;
using System.Composition;
Comment thread
andrewlock marked this conversation as resolved.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace NetEscapades.EnumGenerators.Diagnostics;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(HasFlagCodeFixProvider)), Shared]
public class HasFlagCodeFixProvider : CodeFixProvider
{
private const string Title = "Replace with HasFlagFast()";

public sealed override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(HasFlagAnalyzer.DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Find the node at the diagnostic location
var node = root.FindNode(diagnosticSpan);

// Check if this is a HasFlag invocation
if (node is IdentifierNameSyntax identifierName)
{
// Register a code action for HasFlag() replacement
context.RegisterCodeFix(
CodeAction.Create(
title: Title,
createChangedDocument: c => ReplaceHasFlagWithHasFlagFast(context.Document, identifierName, c),
equivalenceKey: Title),
context.Diagnostics);
}
}

private static async Task<Document> ReplaceHasFlagWithHasFlagFast(
Document document,
IdentifierNameSyntax identifierName,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
{
return document;
}

// Create the new identifier with "HasFlagFast"
var newIdentifier = SyntaxFactory.IdentifierName("HasFlagFast")
.WithTriviaFrom(identifierName);

// Create new member access with the new identifier
var memberAccess = identifierName.Parent as MemberAccessExpressionSyntax;
if (memberAccess is null)
{
return document;
}

var newMemberAccess = memberAccess.WithName(newIdentifier);

// Replace the old member access with the new one
var newRoot = root.ReplaceNode(memberAccess, newMemberAccess);

return document.WithSyntaxRoot(newRoot);
}
}
50 changes: 4 additions & 46 deletions src/NetEscapades.EnumGenerators/Diagnostics/ToStringAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,12 @@ public override void Initialize(AnalysisContext context)
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(ctx =>
{
var enumExtensionsAttr =
ctx.Compilation.GetTypeByMetadataName(Attributes.EnumExtensionsAttribute);
var externalEnumExtensionsAttr =
ctx.Compilation.GetTypeByMetadataName(Attributes.ExternalEnumExtensionsAttribute);

if (enumExtensionsAttr is null)
var (enumExtensionsAttr, externalEnumTypes) = AnalyzerHelpers.GetEnumExtensionAttributes(ctx);
if (enumExtensionsAttr is null || externalEnumTypes is null)
{
return;
}

// Collect all enum types that have EnumExtensions<T> attributes
var externalEnumTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
if (externalEnumExtensionsAttr is not null)
{
foreach (var attribute in ctx.Compilation.Assembly.GetAttributes())
{
if (attribute.AttributeClass is { IsGenericType: true } attrClass &&
SymbolEqualityComparer.Default.Equals(attrClass.ConstructedFrom, externalEnumExtensionsAttr) &&
attrClass.TypeArguments is [INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType])
{
externalEnumTypes.Add(enumType);
}
}
}

ctx.RegisterSyntaxNodeAction(
c => AnalyzeInvocation(c, enumExtensionsAttr, externalEnumTypes),
SyntaxKind.InvocationExpression);
Expand Down Expand Up @@ -136,7 +117,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
return;
}

if (!IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes))
if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes))
{
return;
}
Expand Down Expand Up @@ -190,7 +171,7 @@ private static void AnalyzeInterpolation(SyntaxNodeAnalysisContext context, INam
}
}

if (!IsEnumWithExtensions(expressionType, enumExtensionsAttr, externalEnumTypes))
if (!AnalyzerHelpers.IsEnumWithExtensions(expressionType, enumExtensionsAttr, externalEnumTypes))
{
return;
}
Expand All @@ -203,27 +184,4 @@ private static void AnalyzeInterpolation(SyntaxNodeAnalysisContext context, INam

context.ReportDiagnostic(diagnostic);
}

private static bool IsEnumWithExtensions(
ITypeSymbol receiverType,
INamedTypeSymbol enumExtensionsAttr,
HashSet<INamedTypeSymbol> externalEnumTypes)
{
// Check if the enum has the [EnumExtensions] attribute or is referenced in EnumExtensions<T>
// First check if the enum itself has the attribute
foreach (var attributeData in receiverType.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(
attributeData.AttributeClass,
enumExtensionsAttr))
{
return true;
}
}

// If not, check if it's in the external enum types (EnumExtensions<T>)
return receiverType is INamedTypeSymbol namedType
&& externalEnumTypes.Contains(namedType);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
dotnet_diagnostic.NEEG001.severity = error
dotnet_diagnostic.NEEG002.severity = error
dotnet_diagnostic.NEEG003.severity = error
dotnet_diagnostic.NEEG004.severity = error
dotnet_diagnostic.NEEG004.severity = error
dotnet_diagnostic.NEEG005.severity = error
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using NetEscapades.EnumGenerators.IntegrationTests;
using Xunit;

namespace NetEscapades.EnumGenerators.Benchmarks;
Expand All @@ -24,4 +25,14 @@ public void Neeg004Testing()
_ = $"Some value: {EnumInSystem.First:G} <-";
#pragma warning restore NEEG004
}

[Fact]
public void Neeg005Testing()
{
#pragma warning disable NEEG005
var test = FlagsEnum.First;
_ = test.HasFlag(FlagsEnum.Second);
_ = $"Some value: {test.HasFlag(FlagsEnum.Second)} <-";
#pragma warning restore NEEG005
}
}
Loading
Loading