-
Notifications
You must be signed in to change notification settings - Fork 57
Add analyzer to detect HasFlag() and suggest HasFlagFast() replacement #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d9eaa92
Initial plan
Copilot 28b91e0
Add HasFlagAnalyzer and HasFlagCodeFixProvider with tests
Copilot 8d9fc92
Address code review feedback - add using System; and fix formatting
Copilot bc69ef7
Extract some common code
andrewlock ef7c5c5
Fix compilation
andrewlock b855c8c
Disable analyzers in integration tests
andrewlock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
src/NetEscapades.EnumGenerators/Diagnostics/AnalyzerHelpers.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
|
|
||
| // 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
101
src/NetEscapades.EnumGenerators/Diagnostics/HasFlagAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
78 changes: 78 additions & 0 deletions
78
src/NetEscapades.EnumGenerators/Diagnostics/HasFlagCodeFixProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| using System.Collections.Immutable; | ||
| using System.Composition; | ||
|
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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.