diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/AnalyzerHelpers.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/AnalyzerHelpers.cs
index 6be407c..8b92888 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/AnalyzerHelpers.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/AnalyzerHelpers.cs
@@ -6,6 +6,25 @@ namespace NetEscapades.EnumGenerators.Diagnostics;
public static class AnalyzerHelpers
{
public const string ExtensionTypeNameProperty = nameof(ExtensionTypeNameProperty);
+ public const string IsNullableProperty = nameof(IsNullableProperty);
+
+ ///
+ /// If is where TEnum is an enum,
+ /// returns the underlying enum type. Otherwise returns null.
+ ///
+ 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 =
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagAnalyzer.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagAnalyzer.cs
index 0027c15..be42ed4 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagAnalyzer.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagAnalyzer.cs
@@ -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;
}
@@ -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;
@@ -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([
new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagCodeFixProvider.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagCodeFixProvider.cs
index 82decda..c0f39fa 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagCodeFixProvider.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/HasFlagCodeFixProvider.cs
@@ -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;
@@ -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;
@@ -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)
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendAnalyzer.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendAnalyzer.cs
index 3f55837..afa65d5 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendAnalyzer.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendAnalyzer.cs
@@ -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([
+ new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
+ new(AnalyzerHelpers.IsNullableProperty, "true"),
+ ])
+ : ImmutableDictionary.CreateRange([
+ new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType)
+ ]);
+
var diagnostic = Diagnostic.Create(
descriptor: Rule,
location: argument.GetLocation(),
messageArgs: argumentType.Name,
- properties: ImmutableDictionary.CreateRange([
- new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
- ]));
+ properties: properties);
context.ReportDiagnostic(diagnostic);
}
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendCodeFixProvider.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendCodeFixProvider.cs
index 993dde8..de34149 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendCodeFixProvider.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/StringBuilderAppendCodeFixProvider.cs
@@ -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;
@@ -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);
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringAnalyzer.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringAnalyzer.cs
index 1d06ece..06c8cc9 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringAnalyzer.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringAnalyzer.cs
@@ -54,20 +54,37 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
{
var invocation = (InvocationExpressionSyntax)context.Node;
- // Check if this is a member access expression (e.g., value.ToString())
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ // Check if there are too many arguments)
+ if (invocation.ArgumentList.Arguments.Count > 1)
{
return;
}
- // Check if there are too many arguments)
- if (invocation.ArgumentList.Arguments.Count > 1)
+ // Determine the method name and receiver expression
+ // Handle both regular member access (value.ToString()) and conditional access (value?.ToString())
+ SimpleNameSyntax methodName;
+ ExpressionSyntax receiverExpression;
+ bool isConditionalAccess = false;
+
+ 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;
+ isConditionalAccess = true;
+ }
+ else
{
return;
}
// Check if the method name is "ToString"
- if (memberAccess.Name.Identifier.Text != "ToString")
+ if (methodName.Identifier.Text != "ToString")
{
return;
}
@@ -79,12 +96,13 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
return;
}
- // Verify this is the ToString() method from System.Object or System.Enum
+ // Verify this is the ToString() method from System.Object, System.Enum, or Nullable
// We handle format specifiers, so accept ToString() with 0 or 1 parameters
if (methodSymbol.Name != "ToString" ||
methodSymbol.Parameters.Length > 1 ||
(methodSymbol.ContainingType.SpecialType != SpecialType.System_Object &&
- methodSymbol.ContainingType.SpecialType != SpecialType.System_Enum))
+ methodSymbol.ContainingType.SpecialType != SpecialType.System_Enum &&
+ methodSymbol.ContainingType.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T))
{
return;
}
@@ -95,7 +113,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
{
var argument = invocation.ArgumentList.Arguments[0];
var constantValue = context.SemanticModel.GetConstantValue(argument.Expression);
-
+
// If we can't determine the value at compile time, don't suggest replacement
// If it's not a string (e.g., it's an IFormatProvider), don't suggest replacement
if (!constantValue.HasValue || constantValue.Value is not string formatString)
@@ -105,7 +123,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
// Check if the format string is compatible with ToStringFast()
// Only "", "G", and "g" are compatible
- if (!string.IsNullOrEmpty(formatString) &&
+ if (!string.IsNullOrEmpty(formatString) &&
!string.Equals(formatString, "G", StringComparison.Ordinal) &&
!string.Equals(formatString, "g", StringComparison.Ordinal))
{
@@ -114,25 +132,44 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedT
}
// Get the type of the receiver (the thing before .ToString())
- 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;
}
+ var isNullable = false;
+ if (receiverType.TypeKind != TypeKind.Enum)
+ {
+ if (!AnalyzerHelpers.TryUnwrapNullableEnum(receiverType, out var unwrapped))
+ {
+ return;
+ }
+
+ receiverType = unwrapped;
+ isNullable = true;
+ }
+
if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes, out var extensionType))
{
return;
}
// Report the diagnostic
+ var properties = isNullable || isConditionalAccess
+ ? ImmutableDictionary.CreateRange([
+ new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
+ new(AnalyzerHelpers.IsNullableProperty, "true"),
+ ])
+ : ImmutableDictionary.CreateRange([
+ new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType)
+ ]);
+
var diagnostic = Diagnostic.Create(
descriptor: Rule,
- location: memberAccess.Name.GetLocation(),
+ location: methodName.GetLocation(),
messageArgs: receiverType.Name,
- properties: ImmutableDictionary.CreateRange([
- new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
- ]));
+ properties: properties);
context.ReportDiagnostic(diagnostic);
}
@@ -157,19 +194,31 @@ private static void AnalyzeInterpolation(SyntaxNodeAnalysisContext context, INam
_ => context.SemanticModel.GetTypeInfo(expression).Type
};
- if (expressionType is null || expressionType.TypeKind != TypeKind.Enum)
+ bool isNullable = false;
+ if (expressionType is null)
{
return;
}
+ if (expressionType.TypeKind != TypeKind.Enum)
+ {
+ if (!AnalyzerHelpers.TryUnwrapNullableEnum(expressionType, out var unwrapped))
+ {
+ return;
+ }
+
+ expressionType = unwrapped;
+ isNullable = true;
+ }
+
// Check if there's a format clause (e.g., :g, :G, :x)
if (interpolation.FormatClause is not null)
{
var formatString = interpolation.FormatClause.FormatStringToken.Text;
-
+
// Check if the format string is compatible with ToStringFast()
// Only "", "g", and "G" are compatible (empty is when there's no format clause)
- if (!string.IsNullOrEmpty(formatString) &&
+ if (!string.IsNullOrEmpty(formatString) &&
!string.Equals(formatString, "G", StringComparison.Ordinal) &&
!string.Equals(formatString, "g", StringComparison.Ordinal))
{
@@ -183,13 +232,18 @@ private static void AnalyzeInterpolation(SyntaxNodeAnalysisContext context, INam
}
// Report the diagnostic on the expression itself
+ var properties = ImmutableDictionary.CreateBuilder();
+ properties.Add(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType);
+ if (isNullable)
+ {
+ properties.Add(AnalyzerHelpers.IsNullableProperty, "true");
+ }
+
var diagnostic = Diagnostic.Create(
descriptor: Rule,
location: expression.GetLocation(),
messageArgs: expressionType.Name,
- properties: ImmutableDictionary.CreateRange([
- new(AnalyzerHelpers.ExtensionTypeNameProperty, extensionType),
- ]));
+ properties: properties.ToImmutable());
context.ReportDiagnostic(diagnostic);
}
diff --git a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringCodeFixProvider.cs b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringCodeFixProvider.cs
index 3b7e6b0..9db2ebb 100644
--- a/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringCodeFixProvider.cs
+++ b/src/NetEscapades.EnumGenerators.Generators/Diagnostics/UsageAnalyzers/ToStringCodeFixProvider.cs
@@ -40,18 +40,34 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
{
// Find the node at the diagnostic location
var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
+ var generator = editor.Generator;
+ var isNullable = diagnostic.Properties.ContainsKey(AnalyzerHelpers.IsNullableProperty);
// Check if this is an interpolation case
- var generator = editor.Generator;
if (node.Parent is InterpolationSyntax interpolation)
{
- var newInvocation = generator.InvocationExpression(
- generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
- interpolation.Expression) // this parameter
- .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
+ ExpressionSyntax newExpression;
+ if (isNullable)
+ {
+ // $"{nullableValue}" → $"{nullableValue?.ToStringFast()}"
+ newExpression = SyntaxFactory.ConditionalAccessExpression(
+ interpolation.Expression,
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberBindingExpression(
+ SyntaxFactory.IdentifierName("ToStringFast"))))
+ .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
+ }
+ else
+ {
+ // $"{value}" → $"{ExtensionType.ToStringFast(value)}"
+ newExpression = (ExpressionSyntax)generator.InvocationExpression(
+ generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
+ interpolation.Expression) // this parameter
+ .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
+ }
// Create a new interpolation with the invocation and no format clause
- var newInterpolation = SyntaxFactory.Interpolation((ExpressionSyntax)newInvocation)
+ var newInterpolation = SyntaxFactory.Interpolation(newExpression)
.WithLeadingTrivia(interpolation.GetLeadingTrivia())
.WithTrailingTrivia(interpolation.GetTrailingTrivia());
@@ -62,18 +78,52 @@ protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnost
editor.ReplaceNode(interpolation, newInterpolation);
}
+ // Check if this is a conditional access case (value?.ToString())
+ else if (node is IdentifierNameSyntax
+ && node.Parent is MemberBindingExpressionSyntax
+ && node.Parent.Parent is InvocationExpressionSyntax bindingInvocation)
+ {
+ // value?.ToString() → value?.ToStringFast() (just rename and strip args)
+ var newInvocation = SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberBindingExpression(
+ SyntaxFactory.IdentifierName("ToStringFast")))
+ .WithTriviaFrom(bindingInvocation);
+
+ editor.ReplaceNode(bindingInvocation, newInvocation);
+ }
// Check if this is a ToString invocation (original case)
- else if (node is IdentifierNameSyntax identifierName
- && identifierName.Parent is MemberAccessExpressionSyntax memberAccess
+ else if (node is IdentifierNameSyntax
+ && node.Parent is MemberAccessExpressionSyntax memberAccess
&& memberAccess.Parent is InvocationExpressionSyntax invocation)
{
- var newInvocation = generator.InvocationExpression(
- generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
- memberAccess.Expression) // this parameter
- .WithTriviaFrom(invocation)
- .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
+ if (isNullable)
+ {
+ // value.ToString() on MyEnum? → value?.ToStringFast() ?? ""
+ var conditionalAccess = SyntaxFactory.ConditionalAccessExpression(
+ memberAccess.Expression,
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberBindingExpression(
+ SyntaxFactory.IdentifierName("ToStringFast"))));
- editor.ReplaceNode(invocation, newInvocation);
+ ExpressionSyntax newExpression = SyntaxFactory.BinaryExpression(
+ SyntaxKind.CoalesceExpression,
+ conditionalAccess,
+ SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("")))
+ .WithTriviaFrom(invocation);
+
+ editor.ReplaceNode(invocation, newExpression);
+ }
+ else
+ {
+ // value.ToString() → ExtensionType.ToStringFast(value)
+ var newInvocation = generator.InvocationExpression(
+ generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "ToStringFast"),
+ memberAccess.Expression) // this parameter
+ .WithTriviaFrom(invocation)
+ .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
+
+ editor.ReplaceNode(invocation, newInvocation);
+ }
}
return Task.CompletedTask;
diff --git a/tests/NetEscapades.EnumGenerators.IntegrationTests/AnalyzerTests.cs b/tests/NetEscapades.EnumGenerators.IntegrationTests/AnalyzerTests.cs
index 8dc9fc9..031f890 100644
--- a/tests/NetEscapades.EnumGenerators.IntegrationTests/AnalyzerTests.cs
+++ b/tests/NetEscapades.EnumGenerators.IntegrationTests/AnalyzerTests.cs
@@ -48,6 +48,12 @@ public void Neeg004Testing()
_ = $"Some value: {test:G} <-";
_ = $"Some value: {EnumInSystem.First} <-";
_ = $"Some value: {EnumInSystem.First:G} <-";
+
+ EnumInSystem? test2 = null;
+ _ = test2.ToString();
+ _ = $"Some value: {test2} <-";
+ _ = $"Some value: {test2.ToString()} <-";
+ _ = $"Some value: {test2:G} <-";
#pragma warning restore NEEG004
}
@@ -58,6 +64,10 @@ public void Neeg005Testing()
var test = FlagsEnum.First;
_ = test.HasFlag(FlagsEnum.Second);
_ = $"Some value: {test.HasFlag(FlagsEnum.Second)} <-";
+
+ FlagsEnum? test2 = null;
+ _ = test2?.HasFlag(FlagsEnum.Second);
+ _ = $"Some value: {test2?.HasFlag(FlagsEnum.Second)} <-";
#pragma warning restore NEEG005
}
@@ -169,6 +179,9 @@ public void Neeg012Testing()
var flagsEnum = FlagsEnum.First;
var sb = new StringBuilder();
sb.Append(flagsEnum);
+
+ FlagsEnum? flagsEnum2 = null;
+ sb.Append(flagsEnum2);
#pragma warning restore NEEG012
}
}
\ No newline at end of file
diff --git a/tests/NetEscapades.EnumGenerators.Tests/HasFlagAnalyzerTests.cs b/tests/NetEscapades.EnumGenerators.Tests/HasFlagAnalyzerTests.cs
index 5e25b3a..be95669 100644
--- a/tests/NetEscapades.EnumGenerators.Tests/HasFlagAnalyzerTests.cs
+++ b/tests/NetEscapades.EnumGenerators.Tests/HasFlagAnalyzerTests.cs
@@ -560,6 +560,129 @@ public void TestMethod()
await VerifyCodeFixAsync(test, fix);
}
+ [Fact]
+ public async Task NullableEnumValuePropertyHasFlagShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ FlagsEnum? value = FlagsEnum.First;
+ var flag = FlagsEnum.Second;
+ var hasFlag = value.Value.{|NEEG005:HasFlag|}(flag);
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ FlagsEnum? value = FlagsEnum.First;
+ var flag = FlagsEnum.Second;
+ var hasFlag = value.Value.HasFlagFast(flag);
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumConditionalAccessHasFlagShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ FlagsEnum? value = FlagsEnum.First;
+ var flag = FlagsEnum.Second;
+ var hasFlag = value?.{|NEEG005:HasFlag|}(flag);
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ FlagsEnum? value = FlagsEnum.First;
+ var flag = FlagsEnum.Second;
+ var hasFlag = value?.HasFlagFast(flag);
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumConditionalAccessHasFlagOnEnumWithoutAttributeShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnumWithoutAttribute? value = TestEnumWithoutAttribute.First;
+ var flag = TestEnumWithoutAttribute.Second;
+ var hasFlag = value?.HasFlag(flag);
+ }
+ }
+ """);
+ await VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task WhenUsageAnalyzersNotEnabled_HasFlagShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var value = FlagsEnum.First;
+ var result = value.HasFlag(FlagsEnum.Second);
+ }
+ }
+ """);
+ // Don't set the config option - analyzer should not run
+ await VerifyAnalyzerAsync(test, EnableState.Missing);
+ }
+
+ [Fact]
+ public async Task WhenUsageAnalyzersDisabled_HasFlagShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var value = FlagsEnum.First;
+ var result = value.HasFlag(FlagsEnum.Second);
+ }
+ }
+ """);
+
+ await VerifyAnalyzerAsync(test, EnableState.Disabled);
+ }
+
private static string GetTestCodeWithExternalEnum(string testCode) => $$"""
using System;
using System.Collections.Generic;
@@ -639,41 +762,4 @@ public static bool HasFlagFast(this FlagsEnum val, FlagsEnum flag)
""";
- [Fact]
- public async Task WhenUsageAnalyzersNotEnabled_HasFlagShouldNotHaveDiagnostic()
- {
- var test = GetTestCode(
- /* lang=c# */
- """
- public class TestClass
- {
- public void TestMethod()
- {
- var value = FlagsEnum.First;
- var result = value.HasFlag(FlagsEnum.Second);
- }
- }
- """);
- // Don't set the config option - analyzer should not run
- await VerifyAnalyzerAsync(test, EnableState.Missing);
- }
-
- [Fact]
- public async Task WhenUsageAnalyzersDisabled_HasFlagShouldNotHaveDiagnostic()
- {
- var test = GetTestCode(
- /* lang=c# */
- """
- public class TestClass
- {
- public void TestMethod()
- {
- var value = FlagsEnum.First;
- var result = value.HasFlag(FlagsEnum.Second);
- }
- }
- """);
-
- await VerifyAnalyzerAsync(test, EnableState.Disabled);
- }
}
\ No newline at end of file
diff --git a/tests/NetEscapades.EnumGenerators.Tests/StringBuilderAppendAnalyzerTests.cs b/tests/NetEscapades.EnumGenerators.Tests/StringBuilderAppendAnalyzerTests.cs
index a10432d..b10c12a 100644
--- a/tests/NetEscapades.EnumGenerators.Tests/StringBuilderAppendAnalyzerTests.cs
+++ b/tests/NetEscapades.EnumGenerators.Tests/StringBuilderAppendAnalyzerTests.cs
@@ -435,6 +435,91 @@ public void TestMethod()
await VerifyCodeFixAsync(test, fix);
}
+ [Fact]
+ public async Task NullableEnumValuePropertyAppendShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var sb = new System.Text.StringBuilder();
+ TestEnum? value = TestEnum.First;
+ sb.Append({|NEEG012:value.Value|});
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var sb = new System.Text.StringBuilder();
+ TestEnum? value = TestEnum.First;
+ sb.Append(value.Value.ToStringFast());
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumAppendShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var sb = new System.Text.StringBuilder();
+ TestEnum? value = TestEnum.First;
+ sb.Append({|NEEG012:value|});
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var sb = new System.Text.StringBuilder();
+ TestEnum? value = TestEnum.First;
+ sb.Append(value?.ToStringFast());
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumAppendOnEnumWithoutAttributeShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ var sb = new System.Text.StringBuilder();
+ TestEnumWithoutAttribute? value = TestEnumWithoutAttribute.First;
+ sb.Append(value);
+ }
+ }
+ """);
+ await VerifyAnalyzerAsync(test);
+ }
+
[Fact]
public async Task WhenUsageAnalyzersNotEnabled_AppendShouldNotHaveDiagnostic()
{
diff --git a/tests/NetEscapades.EnumGenerators.Tests/ToStringAnalyzerTests.cs b/tests/NetEscapades.EnumGenerators.Tests/ToStringAnalyzerTests.cs
index 7226111..c426376 100644
--- a/tests/NetEscapades.EnumGenerators.Tests/ToStringAnalyzerTests.cs
+++ b/tests/NetEscapades.EnumGenerators.Tests/ToStringAnalyzerTests.cs
@@ -770,6 +770,219 @@ public string TestMethod()
await VerifyCodeFixAsync(test, fix);
}
+ [Fact]
+ public async Task NullableEnumValuePropertyToStringShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value.Value.{|NEEG004:ToString|}();
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value.Value.ToStringFast();
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumConditionalAccessToStringShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.{|NEEG004:ToString|}();
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.ToStringFast();
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumConditionalAccessToStringWithCompatibleFormatShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.{|NEEG004:ToString|}("G");
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.ToStringFast();
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Theory]
+ [InlineData("\"D\"")]
+ [InlineData("\"d\"")]
+ [InlineData("\"x\"")]
+ [InlineData("\"X\"")]
+ public async Task NullableEnumConditionalAccessToStringWithIncompatibleFormatShouldNotHaveDiagnostic(string param)
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ $$"""
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.ToString({{param}});
+ }
+ }
+ """);
+ await VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task NullableEnumConditionalAccessToStringOnEnumWithoutAttributeShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnumWithoutAttribute? value = TestEnumWithoutAttribute.First;
+ var str = value?.ToString();
+ }
+ }
+ """);
+ await VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task NullableNonEnumConditionalAccessToStringShouldNotHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ int? value = 42;
+ var str = value?.ToString();
+ }
+ }
+ """);
+ await VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task NullableEnumDirectToStringShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value.{|NEEG004:ToString|}();
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = value?.ToStringFast() ?? "";
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
+ [Fact]
+ public async Task NullableEnumInStringInterpolationShouldHaveDiagnostic()
+ {
+ var test = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = $"{{|NEEG004:value|}}";
+ }
+ }
+ """);
+
+ var fix = GetTestCode(
+ /* lang=c# */
+ """
+ public class TestClass
+ {
+ public void TestMethod()
+ {
+ TestEnum? value = TestEnum.First;
+ var str = $"{value?.ToStringFast()}";
+ }
+ }
+ """);
+ await VerifyCodeFixAsync(test, fix);
+ }
+
[Fact]
public async Task WhenUsageAnalyzersNotEnabled_ToStringShouldNotHaveDiagnostic()
{