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() {