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
Expand Up @@ -6,6 +6,25 @@ namespace NetEscapades.EnumGenerators.Diagnostics;
public static class AnalyzerHelpers
{
public const string ExtensionTypeNameProperty = nameof(ExtensionTypeNameProperty);
public const string IsNullableProperty = nameof(IsNullableProperty);

/// <summary>
/// If <paramref name="type"/> is <see cref="Nullable{TEnum}"/> where TEnum is an enum,
/// returns the underlying enum type. Otherwise returns null.
/// </summary>
public static bool TryUnwrapNullableEnum(ITypeSymbol? type, [NotNullWhen(true)] out ITypeSymbol? enumType)
{
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments: [
{ TypeKind: TypeKind.Enum } innerType] })
{
enumType = innerType;
return true;
}

enumType = null;
return false;
}

public static (INamedTypeSymbol? enumExtensionsAttr, ExternalEnumDictionary? externalEnumTypes) GetEnumExtensionAttributes(Compilation compilation)
{
var enumExtensionsAttr =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,29 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
{
var invocation = (InvocationExpressionSyntax)context.Node;

// Check if this is a member access expression (e.g., value.HasFlag())
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
// Determine the method name and receiver expression
// Handle both regular member access (value.HasFlag()) and conditional access for nullable (value?.HasFlag())
SimpleNameSyntax methodName;
ExpressionSyntax receiverExpression;

if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
methodName = memberAccess.Name;
receiverExpression = memberAccess.Expression;
}
else if (invocation.Expression is MemberBindingExpressionSyntax memberBinding
&& invocation.Parent is ConditionalAccessExpressionSyntax conditionalAccess)
{
methodName = memberBinding.Name;
receiverExpression = conditionalAccess.Expression;
}
else
{
return;
}

// Check if the method name is "HasFlag"
if (memberAccess.Name.Identifier.Text != "HasFlag")
if (methodName.Identifier.Text != "HasFlag")
{
return;
}
Expand All @@ -84,12 +99,22 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
}

// 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)
var receiverType = context.SemanticModel.GetTypeInfo(receiverExpression).Type;
if (receiverType is null)
{
return;
}

if (receiverType.TypeKind != TypeKind.Enum)
{
if (!AnalyzerHelpers.TryUnwrapNullableEnum(receiverType, out var unwrapped))
{
return;
}

receiverType = unwrapped;
}

if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes, out var extensionType))
{
return;
Expand All @@ -98,7 +123,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
// Report the diagnostic
var diagnostic = Diagnostic.Create(
descriptor: Rule,
location: memberAccess.Name.GetLocation(),
location: methodName.GetLocation(),
messageArgs: receiverType.Name,
properties: ImmutableDictionary.CreateRange<string, string?>([
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification;
Expand Down Expand Up @@ -41,8 +42,27 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
// Find the node at the diagnostic location
var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);

if (node is not IdentifierNameSyntax identifierName
|| identifierName.Parent is not MemberAccessExpressionSyntax memberAccess
if (node is not IdentifierNameSyntax)
{
return Task.CompletedTask;
}

// Handle conditional access case for nullable: value?.HasFlag(flag) → value?.HasFlagFast(flag)
if (node.Parent is MemberBindingExpressionSyntax
&& node.Parent.Parent is InvocationExpressionSyntax bindingInvocation)
{
var newNullableInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberBindingExpression(
SyntaxFactory.IdentifierName("HasFlagFast")),
bindingInvocation.ArgumentList)
.WithTriviaFrom(bindingInvocation);

editor.ReplaceNode(bindingInvocation, newNullableInvocation);
return Task.CompletedTask;
}

// Handle regular case: value.HasFlag(flag) → ExtensionType.HasFlagFast(value, flag)
if (node.Parent is not MemberAccessExpressionSyntax memberAccess
|| memberAccess.Parent is not InvocationExpressionSyntax invocation)
{
return Task.CompletedTask;
Expand All @@ -51,7 +71,7 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
var newInvocation = generator.InvocationExpression(
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "HasFlagFast"),
[
memberAccess.Expression, // this parameter
memberAccess.Expression, // this parameter
..invocation.ArgumentList.Arguments,
])
.WithTriviaFrom(invocation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,47 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
}

var argument = invocation.ArgumentList.Arguments[0];

// Get the type of the argument
var argumentType = context.SemanticModel.GetTypeInfo(argument.Expression).Type;
if (argumentType is null || argumentType.TypeKind != TypeKind.Enum)
if (argumentType is null)
{
return;
}

var isNullable = false;
if (argumentType.TypeKind != TypeKind.Enum)
{
if (!AnalyzerHelpers.TryUnwrapNullableEnum(argumentType, out var unwrapped))
{
return;
}

argumentType = unwrapped;
isNullable = true;
}

if (!AnalyzerHelpers.IsEnumWithExtensions(argumentType, enumExtensionsAttr, externalEnumTypes, out var extensionType))
{
return;
}

// Report the diagnostic
var properties =
isNullable
? ImmutableDictionary.CreateRange<string, string?>([
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
new(AnalyzerHelpers.IsNullableProperty, "true"),
])
: ImmutableDictionary.CreateRange<string, string?>([
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType)
]);

var diagnostic = Diagnostic.Create(
descriptor: Rule,
location: argument.GetLocation(),
messageArgs: argumentType.Name,
properties: ImmutableDictionary.CreateRange<string, string?>([
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
]));
properties: properties);

context.ReportDiagnostic(diagnostic);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification;
Expand Down Expand Up @@ -46,16 +47,33 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
return Task.CompletedTask;
}

var generator = editor.Generator;
var isNullable = diagnostic.Properties.ContainsKey(AnalyzerHelpers.IsNullableProperty);

// Create the new expression: enumValue.ToStringFast()
var newInvocation = generator.InvocationExpression(
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
argument.Expression) // this parameter
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
ExpressionSyntax newExpression;
if (isNullable)
{
// sb.Append(nullableValue) → sb.Append(nullableValue?.ToStringFast())
newExpression = SyntaxFactory.ConditionalAccessExpression(
argument.Expression,
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberBindingExpression(
SyntaxFactory.IdentifierName("ToStringFast"))))
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
}
else
{
var generator = editor.Generator;

// Create the new expression: enumValue.ToStringFast()
// sb.Append(value) → sb.Append(value.ToStringFast())
newExpression = (ExpressionSyntax)generator.InvocationExpression(
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
argument.Expression) // this parameter
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
}

// Create a new argument with the invocation
var newArgument = argument.WithExpression((ExpressionSyntax)newInvocation);
var newArgument = argument.WithExpression(newExpression);

editor.ReplaceNode(argument, newArgument);

Expand Down
Loading
Loading