Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyzers and fix for casting numeric literal values to enums #505

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
32 changes: 32 additions & 0 deletions docs/analyzers/RCS1235.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# RCS1235: Use enumeration member instead of casting numeric literal value

| Property | Value |
| -------- | ----------- |
| Id | RCS1235 |
| Category | Design |
| Severity | Info |

## Example

### Code with Diagnostic

```csharp
enum E {
F = 123,
};

var f = (E)123; RCS1235
```

### Code with Fix

```csharp
var f = E.F;
```

## See Also

* [How to Suppress a Diagnostic](../HowToConfigureAnalyzers.md#how-to-suppress-a-diagnostic)


*\(Generated with [DotMarkdown](http://github.com/JosefPihrt/DotMarkdown)\)*
26 changes: 26 additions & 0 deletions docs/analyzers/RCS1236.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# RCS1236: Value is not defined in enumeration

| Property | Value |
| -------- | ----------- |
| Id | RCS1236 |
| Category | Design |
| Severity | Warning |

## Example

### Code with Diagnostic

```csharp
enum E {
F = 123,
};

var f = (E)987; RCS1236
```

## See Also

* [How to Suppress a Diagnostic](../HowToConfigureAnalyzers.md#how-to-suppress-a-diagnostic)


*\(Generated with [DotMarkdown](http://github.com/JosefPihrt/DotMarkdown)\)*
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CodeFixes;
using Roslynator.CSharp.Refactorings;

namespace Roslynator.CSharp.CodeFixes
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider))]
[Shared]
public class UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider : BaseCodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral);

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.GetSyntaxRootAsync().ConfigureAwait(false);
if (!TryFindFirstAncestorOrSelf(root, context.Span, out CastExpressionSyntax castExpression))
{
return;
}

var codeAction = CodeAction.Create(
"Use enumeration field",
cancellationToken => UseEnumFieldInsteadOfCastNumericLiteralRefactoring.RefactorAsync(context.Document, castExpression, cancellationToken),
GetEquivalenceKey(DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral));

context.RegisterCodeFix(codeAction, context.Diagnostics);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Roslynator.CSharp.Refactorings
{
internal static class UseEnumFieldInsteadOfCastNumericLiteralRefactoring
{
public static async Task<Document> RefactorAsync(
Document document,
CastExpressionSyntax castExpression,
CancellationToken cancellationToken)
{
var semanticModel = await document.GetSemanticModelAsync().ConfigureAwait(false);
var namedTypeSymbol = (INamedTypeSymbol)semanticModel.GetTypeSymbol(castExpression.Type, cancellationToken);

var expression = (LiteralExpressionSyntax)castExpression.Expression;
var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol);

var enumField = GetEnumFieldWithSpecifiedValue(namedTypeSymbol, numericLiteralValue);

var newNode = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, castExpression.Type, SyntaxFactory.IdentifierName(enumField.Name))
.WithOperatorToken(SyntaxFactory.Token(SyntaxKind.DotToken));

return await document.ReplaceNodeAsync(castExpression, newNode, cancellationToken).ConfigureAwait(false);
}

private static EnumFieldSymbolInfo GetEnumFieldWithSpecifiedValue(INamedTypeSymbol enumSymbol, ulong value)
{
foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast<IFieldSymbol>())
{
var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol);
if (fieldInfo.Value == value)
{
return fieldInfo;
}
}

Debug.Fail("Enum must have field of this value as the analyser flagged this");

return default;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Roslynator.CSharp.Analysis
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NumericLiteralOutOfEnumRangeAnalyzer : BaseDiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.NumericLiteralOutOfEnumRange);

public override void Initialize(AnalysisContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

base.Initialize(context);

context.RegisterSyntaxNodeAction(AnalyzeCastExpression, SyntaxKind.CastExpression);
}

public static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context)
{
var castExpression = (CastExpressionSyntax)context.Node;
if (!(castExpression.Expression is LiteralExpressionSyntax expression))
{
return;
}

if (!(context.SemanticModel.GetTypeSymbol(castExpression.Type, context.CancellationToken) is INamedTypeSymbol namedTypeSymbol)
|| namedTypeSymbol.TypeKind != TypeKind.Enum)
{
return;
}

var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol);
if (!EnumHasDefinedFieldWithNumericLiteralValue(namedTypeSymbol, numericLiteralValue))
{
DiagnosticHelpers.ReportDiagnostic(context, DiagnosticDescriptors.NumericLiteralOutOfEnumRange, context.Node);
}
}

private static bool EnumHasDefinedFieldWithNumericLiteralValue(INamedTypeSymbol enumSymbol, ulong value)
{
foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast<IFieldSymbol>())
{
var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol);
if (fieldInfo.Value == value)
{
return true;
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Roslynator.CSharp.Analysis
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UseEnumFieldInsteadOfCastNumericLiteralAnalyzer : BaseDiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseEnumFieldInsteadOfCastNumericLiteral);

public override void Initialize(AnalysisContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

base.Initialize(context);

context.RegisterSyntaxNodeAction(AnalyzeCastExpression, SyntaxKind.CastExpression);
}

public static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context)
{
var castExpression = (CastExpressionSyntax)context.Node;
if (!(castExpression.Expression is LiteralExpressionSyntax expression))
{
return;
}

if (!(context.SemanticModel.GetTypeSymbol(castExpression.Type, context.CancellationToken) is INamedTypeSymbol namedTypeSymbol)
|| namedTypeSymbol.TypeKind != TypeKind.Enum)
{
return;
}

var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol);
if (EnumHasDefinedFieldWithNumericLiteralValue(namedTypeSymbol, numericLiteralValue))
{
DiagnosticHelpers.ReportDiagnostic(context, DiagnosticDescriptors.UseEnumFieldInsteadOfCastNumericLiteral, context.Node);
}
}

private static bool EnumHasDefinedFieldWithNumericLiteralValue(INamedTypeSymbol enumSymbol, ulong value)
{
foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast<IFieldSymbol>())
{
var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol);
if (fieldInfo.Value == value)
{
return true;
}
}

return false;
}
}
}
23 changes: 23 additions & 0 deletions src/Analyzers/CSharp/DiagnosticDescriptors.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2569,5 +2569,28 @@ public static partial class DiagnosticDescriptors
helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.DuplicateEnumValue}",
customTags: Array.Empty<string>());

/// <summary>RCS1235</summary>
public static readonly DiagnosticDescriptor UseEnumFieldInsteadOfCastNumericLiteral = new DiagnosticDescriptor(
id: DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral,
title: "Use enumeration member instead of casting numeric literal value.",
messageFormat: "Use enumeration member instead of casting numeric literal value.",
category: DiagnosticCategories.Design,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral}",
customTags: Array.Empty<string>());

/// <summary>RCS1236</summary>
public static readonly DiagnosticDescriptor NumericLiteralOutOfEnumRange = new DiagnosticDescriptor(
id: DiagnosticIdentifiers.NumericLiteralOutOfEnumRange,
title: "Value is not defined in enumeration.",
messageFormat: "Value is not defined in enumeration.",
category: DiagnosticCategories.Design,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: null,
helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.NumericLiteralOutOfEnumRange}",
customTags: Array.Empty<string>());
}
}
2 changes: 2 additions & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,7 @@ public static partial class DiagnosticIdentifiers
public const string OrderElementsInDocumentationComment = "RCS1232";
public const string UseShortCircuitingOperator = "RCS1233";
public const string DuplicateEnumValue = "RCS1234";
public const string UseEnumFieldInsteadOfCastNumericLiteral = "RCS1235";
public const string NumericLiteralOutOfEnumRange = "RCS1236";
}
}