Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace NetEscapades.EnumGenerators.Diagnostics.UsageAnalyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TryParseAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NEEG011";
public static readonly DiagnosticDescriptor Rule = new(
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
id: DiagnosticId,
#pragma warning restore RS2008
title: "Use generated TryParse() instead of Enum.TryParse()",
messageFormat: "Use generated TryParse() instead of Enum.TryParse() 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, ExternalEnumDictionary externalEnumTypes)
{
var invocation = (InvocationExpressionSyntax)context.Node;

if (invocation.ArgumentList.Arguments.Count is 0 or > 4
|| invocation.Expression is not MemberAccessExpressionSyntax memberAccess
|| memberAccess.Name.Identifier.Text != nameof(Enum.TryParse)
|| !invocation.ArgumentList.Arguments[^1].RefKindKeyword.IsKind(SyntaxKind.OutKeyword))
{
// can't be the one we want
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 TryParse() method from System.Enum
if (methodSymbol.Name != nameof(Enum.TryParse) ||
methodSymbol.ContainingType.SpecialType != SpecialType.System_Enum)
{
return;
}

ITypeSymbol? enumType = null;

// Handle six basic patterns, value may be string or ReadOnlySpan<char>
// 1. Enum.TryParse(typeof(TEnum), value, out result) - has 3 parameters
// 2. Enum.TryParse(typeof(TEnum), value, ignoreCase, out result) - has 4 parameters
// 3. Enum.TryParse<TEnum>(value, out result) - has 2 parameters, is generic
// 4. Enum.TryParse<TEnum>(value, ignoreCase, out result) - has 3 parameters, is generic
// 5. Enum.TryParse<TEnum>(ReadOnlySpan<char> value, out result) - has 2 parameters, is generic (NET5+)
// 6. Enum.TryParse<TEnum>(ReadOnlySpan<char> value, ignoreCase, out result) - has 3 parameters, is generic (NET5+)
if (methodSymbol is { IsGenericMethod: true, TypeArguments.Length: 1 })
{
// Pattern: Enum.TryParse<TEnum>(value, out result) or Enum.TryParse<TEnum>(value, ignoreCase, out result)
if (invocation.ArgumentList.Arguments.Count is not (2 or 3))
{
return;
}

enumType = methodSymbol.TypeArguments[0];
}
else if (methodSymbol.Parameters.Length is 3 or 4
&& invocation.ArgumentList.Arguments is [{ Expression: TypeOfExpressionSyntax typeOfExpression }, ..])
{
// Pattern: Enum.TryParse(typeof(TEnum), value, out result) or Enum.TryParse(typeof(TEnum), value, ignoreCase, out result)
enumType = context.SemanticModel.GetTypeInfo(typeOfExpression.Type).Type;
}

if (enumType is null || enumType.TypeKind != TypeKind.Enum)
{
return;
}

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

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

context.ReportDiagnostic(diagnostic);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification;

namespace NetEscapades.EnumGenerators.Diagnostics.UsageAnalyzers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TryParseCodeFixProvider)), Shared]
public class TryParseCodeFixProvider : CodeFixProviderBase
{
private const string Title = "Replace with generated TryParse()";

public sealed override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(TryParseAnalyzer.DiagnosticId);

public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (!context.Diagnostics.IsDefaultOrEmpty)
{
// Register a code action for TryParse() replacement
context.RegisterCodeFix(
CodeAction.Create(
title: Title,
createChangedDocument: c => FixAllAsync(context.Document, context.Diagnostics, c),
equivalenceKey: Title),
context.Diagnostics);
}

return Task.CompletedTask;
}

protected override Task FixWithEditor(DocumentEditor editor, Diagnostic diagnostic,
INamedTypeSymbol extensionTypeSymbol,
CancellationToken cancellationToken)
{
// Find the invocation node at the diagnostic location
var node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
if (node is not InvocationExpressionSyntax invocation)
{
return Task.CompletedTask;
}

// Get the symbol to determine which pattern we're dealing with
var symbolInfo = editor.SemanticModel.GetSymbolInfo(invocation, cancellationToken);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
{
return Task.CompletedTask;
}

// This is a bit awkward, but we have a different order for TryParse in our extensions compared to the framework
// That's something we might want to fix sooner rather than later...
ArgumentSyntax? valueArgument = null;
ArgumentSyntax? outArgument = null;
ArgumentSyntax? ignoreCaseArgument = null;

// Determine which arguments to use
if (methodSymbol is { IsGenericMethod: true, TypeArguments.Length: 1 })
{
// Pattern: Enum.TryParse<TEnum>(value, out result) or Enum.TryParse<TEnum>(value, ignoreCase, out result)
if (invocation.ArgumentList.Arguments.Count >= 2)
{
valueArgument = invocation.ArgumentList.Arguments[0];

if (invocation.ArgumentList.Arguments.Count == 2)
{
// TryParse<TEnum>(value, out result)
outArgument = invocation.ArgumentList.Arguments[1];
}
else if (invocation.ArgumentList.Arguments.Count == 3)
{
// TryParse<TEnum>(value, ignoreCase, out result)
ignoreCaseArgument = invocation.ArgumentList.Arguments[1];
outArgument = invocation.ArgumentList.Arguments[2];
}
}
}
else if (methodSymbol.Parameters.Length is 3 or 4
&& invocation.ArgumentList.Arguments.Count >= 3)
{
// Pattern: Enum.TryParse(typeof(TEnum), value, out result) or Enum.TryParse(typeof(TEnum), value, ignoreCase, out result)
valueArgument = invocation.ArgumentList.Arguments[1];

if (invocation.ArgumentList.Arguments.Count == 3)
{
// TryParse(typeof(TEnum), value, out result)
outArgument = invocation.ArgumentList.Arguments[2];
}
else if (invocation.ArgumentList.Arguments.Count == 4)
{
// TryParse(typeof(TEnum), value, ignoreCase, out result)
ignoreCaseArgument = invocation.ArgumentList.Arguments[2];
outArgument = invocation.ArgumentList.Arguments[3];
}
}

if (valueArgument is null || outArgument is null)
{
return Task.CompletedTask;
}

// Create new invocation: ExtensionsClass.Parse(value) or ExtensionsClass.Parse(value, ignoreCase)
var generator = editor.Generator;
SyntaxNode newInvocation;
if (ignoreCaseArgument is not null)
{
// Call with ignoreCase parameter
newInvocation = generator.InvocationExpression(
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "TryParse"),
valueArgument,
outArgument,
ignoreCaseArgument)
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
}
else
{
// Call without ignoreCase parameter
newInvocation = generator.InvocationExpression(
generator.MemberAccessExpression(generator.TypeExpression(extensionTypeSymbol), "TryParse"),
valueArgument,
outArgument)
.WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Simplifier.Annotation);
}

editor.ReplaceNode(invocation, newInvocation);
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ dotnet_diagnostic.NEEG006.severity = error
dotnet_diagnostic.NEEG007.severity = error
dotnet_diagnostic.NEEG008.severity = error
dotnet_diagnostic.NEEG009.severity = error
dotnet_diagnostic.NEEG010.severity = error
dotnet_diagnostic.NEEG010.severity = error
dotnet_diagnostic.NEEG011.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,27 @@ public void Neeg010Testing()
#pragma warning restore NEEG010
}
#endif

[Fact]
public void Neeg011Testing()
{
#pragma warning disable NEEG011
_ = Enum.TryParse<FlagsEnum>("Second", out _);
Enum.TryParse<FlagsEnum>("Second", true, out var e);
_ = $"Some value: {Enum.TryParse<FlagsEnum>("First", out _)} <-";
#if NETCOREAPP
_ = Enum.TryParse(typeof(FlagsEnum), "Second", out var t);
_ = Enum.TryParse(typeof(FlagsEnum), "Second", true, out object? t2);
_ = Enum.TryParse(typeof(FlagsEnum), "Second", ignoreCase: false, out _);
#if NET5_0_OR_GREATER
var toParse = "Second".AsSpan();
_ = Enum.TryParse(typeof(FlagsEnum), toParse, out _);
_ = Enum.TryParse(typeof(FlagsEnum), toParse, ignoreCase: true, out _);
_ = Enum.TryParse<FlagsEnum>(toParse, out _);
_ = Enum.TryParse<FlagsEnum>(toParse, true, out _);
_ = $"Some value: {Enum.TryParse(typeof(FlagsEnum), "First".AsSpan(), out _)} <-";
#endif
#endif
#pragma warning restore NEEG011
}
}
Loading
Loading