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
58 changes: 58 additions & 0 deletions src/NetEscapades.EnumGenerators/Diagnostics/AnalyzerHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Microsoft.CodeAnalysis;

namespace NetEscapades.EnumGenerators.Diagnostics;

public static class AnalyzerHelpers
{
public static (INamedTypeSymbol? enumExtensionsAttr, HashSet<INamedTypeSymbol>? externalEnumTypes) GetEnumExtensionAttributes(Compilation compilation)
{
var enumExtensionsAttr =
compilation.GetTypeByMetadataName(Attributes.EnumExtensionsAttribute);
var externalEnumExtensionsAttr =
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 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);
}
}
}

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);
}
}
101 changes: 101 additions & 0 deletions src/NetEscapades.EnumGenerators/Diagnostics/HasFlagAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

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.Compilation);
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);
}
}
52 changes: 4 additions & 48 deletions src/NetEscapades.EnumGenerators/Diagnostics/ToStringAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -31,31 +29,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.Compilation);
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 +115,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 +169,7 @@ private static void AnalyzeInterpolation(SyntaxNodeAnalysisContext context, INam
}
}

if (!IsEnumWithExtensions(expressionType, enumExtensionsAttr, externalEnumTypes))
if (!AnalyzerHelpers.IsEnumWithExtensions(expressionType, enumExtensionsAttr, externalEnumTypes))
{
return;
}
Expand All @@ -203,27 +182,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,6 +1,26 @@
using System;
using Xunit;

#if INTEGRATION_TESTS
using NetEscapades.EnumGenerators.IntegrationTests;
#elif NETSTANDARD_INTEGRATION_TESTS
using NetEscapades.EnumGenerators.NetStandard.IntegrationTests;
#elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS
using NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests;
#elif INTERCEPTOR_TESTS
using NetEscapades.EnumGenerators.Interceptors.IntegrationTests;
#elif NUGET_INTEGRATION_TESTS
using NetEscapades.EnumGenerators.Nuget.IntegrationTests;
#elif NUGET_INTERCEPTOR_TESTS
using NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests;
#elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS
using NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests;
#elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS
using NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests;
#else
#error Unknown integration tests
#endif

namespace NetEscapades.EnumGenerators.Benchmarks;

public class AnalyzerTests
Expand All @@ -24,4 +44,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests;
#error Unknown integration tests
#endif

#pragma warning disable NEEG001
#pragma warning disable NEEG002
#pragma warning disable NEEG003
#pragma warning disable NEEG004
#pragma warning disable NEEG005

public class ExternalFileShareExtensionsTests : ExtensionTests<FileShare, int, ExternalFileShareExtensionsTests>, ITestData<FileShare>
{
public TheoryData<FileShare> ValidEnumValues() => new()
Expand Down
Loading
Loading