Skip to content

Commit d86dd76

Browse files
authored
Extend the existing analyzers to support nullable enums (#244)
* Initial implementation for nullables * Add analyzer tests
1 parent c2b35a1 commit d86dd76

11 files changed

Lines changed: 696 additions & 93 deletions

File tree

src/NetEscapades.EnumGenerators.Generators/Diagnostics/AnalyzerHelpers.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ namespace NetEscapades.EnumGenerators.Diagnostics;
66
public static class AnalyzerHelpers
77
{
88
public const string ExtensionTypeNameProperty = nameof(ExtensionTypeNameProperty);
9+
public const string IsNullableProperty = nameof(IsNullableProperty);
10+
11+
/// <summary>
12+
/// If <paramref name="type"/> is <see cref="Nullable{TEnum}"/> where TEnum is an enum,
13+
/// returns the underlying enum type. Otherwise returns null.
14+
/// </summary>
15+
public static bool TryUnwrapNullableEnum(ITypeSymbol? type, [NotNullWhen(true)] out ITypeSymbol? enumType)
16+
{
17+
if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments: [
18+
{ TypeKind: TypeKind.Enum } innerType] })
19+
{
20+
enumType = innerType;
21+
return true;
22+
}
23+
24+
enumType = null;
25+
return false;
26+
}
27+
928
public static (INamedTypeSymbol? enumExtensionsAttr, ExternalEnumDictionary? externalEnumTypes) GetEnumExtensionAttributes(Compilation compilation)
1029
{
1130
var enumExtensionsAttr =

src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagAnalyzer.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,29 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
5050
{
5151
var invocation = (InvocationExpressionSyntax)context.Node;
5252

53-
// Check if this is a member access expression (e.g., value.HasFlag())
54-
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
53+
// Determine the method name and receiver expression
54+
// Handle both regular member access (value.HasFlag()) and conditional access for nullable (value?.HasFlag())
55+
SimpleNameSyntax methodName;
56+
ExpressionSyntax receiverExpression;
57+
58+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
59+
{
60+
methodName = memberAccess.Name;
61+
receiverExpression = memberAccess.Expression;
62+
}
63+
else if (invocation.Expression is MemberBindingExpressionSyntax memberBinding
64+
&& invocation.Parent is ConditionalAccessExpressionSyntax conditionalAccess)
65+
{
66+
methodName = memberBinding.Name;
67+
receiverExpression = conditionalAccess.Expression;
68+
}
69+
else
5570
{
5671
return;
5772
}
5873

5974
// Check if the method name is "HasFlag"
60-
if (memberAccess.Name.Identifier.Text != "HasFlag")
75+
if (methodName.Identifier.Text != "HasFlag")
6176
{
6277
return;
6378
}
@@ -84,12 +99,22 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
8499
}
85100

86101
// Get the type of the receiver (the thing before .HasFlag())
87-
var receiverType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
88-
if (receiverType is null || receiverType.TypeKind != TypeKind.Enum)
102+
var receiverType = context.SemanticModel.GetTypeInfo(receiverExpression).Type;
103+
if (receiverType is null)
89104
{
90105
return;
91106
}
92107

108+
if (receiverType.TypeKind != TypeKind.Enum)
109+
{
110+
if (!AnalyzerHelpers.TryUnwrapNullableEnum(receiverType, out var unwrapped))
111+
{
112+
return;
113+
}
114+
115+
receiverType = unwrapped;
116+
}
117+
93118
if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes, out var extensionType))
94119
{
95120
return;
@@ -98,7 +123,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
98123
// Report the diagnostic
99124
var diagnostic = Diagnostic.Create(
100125
descriptor: Rule,
101-
location: memberAccess.Name.GetLocation(),
126+
location: methodName.GetLocation(),
102127
messageArgs: receiverType.Name,
103128
properties: ImmutableDictionary.CreateRange<string, string?>([
104129
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),

src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagCodeFixProvider.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis;
44
using Microsoft.CodeAnalysis.CodeActions;
55
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
67
using Microsoft.CodeAnalysis.CSharp.Syntax;
78
using Microsoft.CodeAnalysis.Editing;
89
using Microsoft.CodeAnalysis.Simplification;
@@ -41,8 +42,27 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
4142
// Find the node at the diagnostic location
4243
var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
4344

44-
if (node is not IdentifierNameSyntax identifierName
45-
|| identifierName.Parent is not MemberAccessExpressionSyntax memberAccess
45+
if (node is not IdentifierNameSyntax)
46+
{
47+
return Task.CompletedTask;
48+
}
49+
50+
// Handle conditional access case for nullable: value?.HasFlag(flag) → value?.HasFlagFast(flag)
51+
if (node.Parent is MemberBindingExpressionSyntax
52+
&& node.Parent.Parent is InvocationExpressionSyntax bindingInvocation)
53+
{
54+
var newNullableInvocation = SyntaxFactory.InvocationExpression(
55+
SyntaxFactory.MemberBindingExpression(
56+
SyntaxFactory.IdentifierName("HasFlagFast")),
57+
bindingInvocation.ArgumentList)
58+
.WithTriviaFrom(bindingInvocation);
59+
60+
editor.ReplaceNode(bindingInvocation, newNullableInvocation);
61+
return Task.CompletedTask;
62+
}
63+
64+
// Handle regular case: value.HasFlag(flag) → ExtensionType.HasFlagFast(value, flag)
65+
if (node.Parent is not MemberAccessExpressionSyntax memberAccess
4666
|| memberAccess.Parent is not InvocationExpressionSyntax invocation)
4767
{
4868
return Task.CompletedTask;
@@ -51,7 +71,7 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
5171
var newInvocation = generator.InvocationExpression(
5272
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "HasFlagFast"),
5373
[
54-
memberAccess.Expression, // this parameter
74+
memberAccess.Expression, // this parameter
5575
..invocation.ArgumentList.Arguments,
5676
])
5777
.WithTriviaFrom(invocation)

src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendAnalyzer.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,27 +83,47 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
8383
}
8484

8585
var argument = invocation.ArgumentList.Arguments[0];
86-
86+
8787
// Get the type of the argument
8888
var argumentType = context.SemanticModel.GetTypeInfo(argument.Expression).Type;
89-
if (argumentType is null || argumentType.TypeKind != TypeKind.Enum)
89+
if (argumentType is null)
9090
{
9191
return;
9292
}
9393

94+
var isNullable = false;
95+
if (argumentType.TypeKind != TypeKind.Enum)
96+
{
97+
if (!AnalyzerHelpers.TryUnwrapNullableEnum(argumentType, out var unwrapped))
98+
{
99+
return;
100+
}
101+
102+
argumentType = unwrapped;
103+
isNullable = true;
104+
}
105+
94106
if (!AnalyzerHelpers.IsEnumWithExtensions(argumentType, enumExtensionsAttr, externalEnumTypes, out var extensionType))
95107
{
96108
return;
97109
}
98110

99111
// Report the diagnostic
112+
var properties =
113+
isNullable
114+
? ImmutableDictionary.CreateRange<string, string?>([
115+
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
116+
new(AnalyzerHelpers.IsNullableProperty, "true"),
117+
])
118+
: ImmutableDictionary.CreateRange<string, string?>([
119+
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType)
120+
]);
121+
100122
var diagnostic = Diagnostic.Create(
101123
descriptor: Rule,
102124
location: argument.GetLocation(),
103125
messageArgs: argumentType.Name,
104-
properties: ImmutableDictionary.CreateRange<string, string?>([
105-
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
106-
]));
126+
properties: properties);
107127

108128
context.ReportDiagnostic(diagnostic);
109129
}

src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendCodeFixProvider.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis;
44
using Microsoft.CodeAnalysis.CodeActions;
55
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
67
using Microsoft.CodeAnalysis.CSharp.Syntax;
78
using Microsoft.CodeAnalysis.Editing;
89
using Microsoft.CodeAnalysis.Simplification;
@@ -46,16 +47,33 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
4647
return Task.CompletedTask;
4748
}
4849

49-
var generator = editor.Generator;
50+
var isNullable = diagnostic.Properties.ContainsKey(AnalyzerHelpers.IsNullableProperty);
5051

51-
// Create the new expression: enumValue.ToStringFast()
52-
var newInvocation = generator.InvocationExpression(
53-
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
54-
argument.Expression) // this parameter
55-
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
52+
ExpressionSyntax newExpression;
53+
if (isNullable)
54+
{
55+
// sb.Append(nullableValue) → sb.Append(nullableValue?.ToStringFast())
56+
newExpression = SyntaxFactory.ConditionalAccessExpression(
57+
argument.Expression,
58+
SyntaxFactory.InvocationExpression(
59+
SyntaxFactory.MemberBindingExpression(
60+
SyntaxFactory.IdentifierName("ToStringFast"))))
61+
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
62+
}
63+
else
64+
{
65+
var generator = editor.Generator;
66+
67+
// Create the new expression: enumValue.ToStringFast()
68+
// sb.Append(value) → sb.Append(value.ToStringFast())
69+
newExpression = (ExpressionSyntax)generator.InvocationExpression(
70+
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
71+
argument.Expression) // this parameter
72+
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
73+
}
5674

5775
// Create a new argument with the invocation
58-
var newArgument = argument.WithExpression((ExpressionSyntax)newInvocation);
76+
var newArgument = argument.WithExpression(newExpression);
5977

6078
editor.ReplaceNode(argument, newArgument);
6179

0 commit comments

Comments
 (0)