Skip to content

Commit

Permalink
Add analyzer "Use enum field explicitly" (RCS1257) (dotnet#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt authored and JochemHarmes committed Oct 30, 2023
1 parent e5e6559 commit 5f174d9
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 0 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add SECURITY.md ([#1147](https://github.com/josefpihrt/roslynator/pull/1147))
- Add custom FixAllProvider for [RCS1014](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1014.md) ([#1070](https://github.com/JosefPihrt/Roslynator/pull/1070)).
- Add more cases to [RCS1097](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1097.md) ([#1160](https://github.com/JosefPihrt/Roslynator/pull/1160)).
- Add analyzer "Use enum field explicitly" ([RCS1257](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1257)) ([#889](https://github.com/josefpihrt/roslynator/pull/889)).
- Enabled by default.

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CodeFixes;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Roslynator.CSharp.CSharpFactory;

namespace Roslynator.CSharp.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CastExpressionCodeFixProvider))]
[Shared]
public sealed class CastExpressionCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(DiagnosticIdentifiers.UseEnumFieldExplicitly); }
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, context.Span, out CastExpressionSyntax castExpression))
return;

Diagnostic diagnostic = context.Diagnostics[0];
Document document = context.Document;

CodeAction codeAction = CodeAction.Create(
"Use enum field explicitly",
ct => UseEnumFieldExplicitlyAsync(castExpression, document, ct),
GetEquivalenceKey(DiagnosticIdentifiers.UseEnumFieldExplicitly));

context.RegisterCodeFix(codeAction, diagnostic);
}

private static async Task<Document> UseEnumFieldExplicitlyAsync(
CastExpressionSyntax castExpression,
Document document,
CancellationToken cancellationToken)
{
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

Optional<object> constantValueOpt = semanticModel.GetConstantValue(castExpression.Expression, cancellationToken);

var enumSymbol = (INamedTypeSymbol)semanticModel.GetTypeSymbol(castExpression.Type, cancellationToken);

if (enumSymbol.HasAttribute(MetadataNames.System_FlagsAttribute))
{
ulong value = SymbolUtility.GetEnumValueAsUInt64(constantValueOpt.Value, enumSymbol);

List<ulong> flags = FlagsUtility<ulong>.Instance.GetFlags(value).ToList();

List<EnumFieldSymbolInfo> fields = EnumSymbolInfo.Create(enumSymbol).Fields
.Where(f => flags.Contains(f.Value))
.OrderByDescending(f => f.Value)
.ToList();

ExpressionSyntax newExpression = CreateEnumFieldExpression(fields[0].Symbol);

for (int i = 1; i < fields.Count; i++)
{
newExpression = BitwiseOrExpression(
CreateEnumFieldExpression(fields[i].Symbol),
newExpression);
}

newExpression = newExpression.WithTriviaFrom(castExpression);

return await document.ReplaceNodeAsync(castExpression, newExpression, cancellationToken).ConfigureAwait(false);
}
else
{
IFieldSymbol symbol = enumSymbol
.GetMembers()
.OfType<IFieldSymbol>()
.First(fieldSymbol =>
{
return fieldSymbol.HasConstantValue
&& constantValueOpt.Value.Equals(fieldSymbol.ConstantValue);
});

ExpressionSyntax newExpression = CreateEnumFieldExpression(symbol).WithTriviaFrom(castExpression);

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

static MemberAccessExpressionSyntax CreateEnumFieldExpression(IFieldSymbol symbol)
{
return SimpleMemberAccessExpression(
symbol.Type.ToTypeSyntax().WithSimplifierAnnotation(),
IdentifierName(symbol.Name));
}
}
}
12 changes: 12 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7356,6 +7356,18 @@ void M()
- optional and its default value is `null`.
</Summary>
</Analyzer>
<Analyzer Identifier="UseEnumFieldExplicitly">
<Id>RCS1257</Id>
<Title>Use enum field explicitly.</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<Samples>
<Sample>
<Before><![CDATA[var options = (RegexOptions) 1]]></Before>
<After><![CDATA[var options = RegexOptions.IgnoreCase]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
94 changes: 94 additions & 0 deletions src/Analyzers/CSharp/Analysis/UseEnumFieldExplicitlyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

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

namespace Roslynator.CSharp.Analysis;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseEnumFieldExplicitlyAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.UseEnumFieldExplicitly);

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterSyntaxNodeAction(c => AnalyzeCastExpression(c), SyntaxKind.CastExpression);
}

private static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context)
{
var castExpression = (CastExpressionSyntax)context.Node;

ExpressionSyntax expression = castExpression.Expression;

if (expression is not LiteralExpressionSyntax literalExpression)
return;

string s = literalExpression.Token.Text;

if (s.Length == 0)
return;

if (!s.StartsWith("0x")
&& !s.StartsWith("0X")
&& !s.StartsWith("0b")
&& !s.StartsWith("0B")
&& !char.IsDigit(s[0]))
{
return;
}

Optional<object> constantValueOpt = context.SemanticModel.GetConstantValue(literalExpression, context.CancellationToken);

if (!constantValueOpt.HasValue)
return;

var enumSymbol = context.SemanticModel.GetTypeSymbol(castExpression.Type, context.CancellationToken) as INamedTypeSymbol;

if (enumSymbol?.EnumUnderlyingType is null)
return;

ulong value = SymbolUtility.GetEnumValueAsUInt64(constantValueOpt.Value, enumSymbol);

foreach (ISymbol member in enumSymbol.GetMembers())
{
if (member is IFieldSymbol fieldSymbol
&& fieldSymbol.HasConstantValue
&& value == SymbolUtility.GetEnumValueAsUInt64(fieldSymbol.ConstantValue, enumSymbol))
{
context.ReportDiagnostic(DiagnosticRules.UseEnumFieldExplicitly, castExpression);
return;
}
}

if (enumSymbol.HasAttribute(MetadataNames.System_FlagsAttribute)
&& FlagsUtility<ulong>.Instance.IsComposite(value))
{
EnumSymbolInfo enumInfo = EnumSymbolInfo.Create(enumSymbol);

foreach (ulong flag in FlagsUtility<ulong>.Instance.GetFlags(value))
{
if (!enumInfo.Contains(flag))
return;
}

context.ReportDiagnostic(DiagnosticRules.UseEnumFieldExplicitly, castExpression);
}
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,6 @@ public static partial class DiagnosticIdentifiers
public const string NormalizeFormatOfEnumFlagValue = "RCS1254";
public const string SimplifyArgumentNullCheck = "RCS1255";
public const string InvalidArgumentNullCheck = "RCS1256";
public const string UseEnumFieldExplicitly = "RCS1257";
}
}
12 changes: 12 additions & 0 deletions src/Analyzers/CSharp/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2521,5 +2521,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.InvalidArgumentNullCheck,
customTags: Array.Empty<string>());

/// <summary>RCS1257</summary>
public static readonly DiagnosticDescriptor UseEnumFieldExplicitly = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.UseEnumFieldExplicitly,
title: "Use enum field explicitly.",
messageFormat: "Use enum field explicitly.",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.UseEnumFieldExplicitly,
customTags: Array.Empty<string>());

}
}
106 changes: 106 additions & 0 deletions src/Tests/Analyzers.Tests/RCS1257UseEnumFieldExplicitlyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Roslynator.CSharp.CodeFixes;
using Roslynator.Testing.CSharp;
using Xunit;

namespace Roslynator.CSharp.Analysis.Tests;

public class RCS1257UseEnumFieldExplicitlyTests : AbstractCSharpDiagnosticVerifier<UseEnumFieldExplicitlyAnalyzer, CastExpressionCodeFixProvider>
{
public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.UseEnumFieldExplicitly;

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseEnumFieldExplicitly)]
public async Task Test()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Text.RegularExpressions;
class C
{
void M()
{
var x = [|(RegexOptions)1|];
}
}
", @"
using System.Text.RegularExpressions;
class C
{
void M()
{
var x = RegexOptions.IgnoreCase;
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseEnumFieldExplicitly)]
public async Task Test_Flags()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Text.RegularExpressions;
class C
{
void M()
{
var x = [|(RegexOptions)7|];
}
}
", @"
using System.Text.RegularExpressions;
class C
{
void M()
{
var x = RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture;
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseEnumFieldExplicitly)]
public async Task TestNoDiagnostic_UndefinedValue()
{
await VerifyNoDiagnosticAsync(@"
using System.Text.RegularExpressions;
class C
{
void M()
{
var x = (Foo)17;
}
}
[System.Flags]
enum Foo
{
None = 0,
A = 1,
B = 2,
C = 4,
D = 8,
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseEnumFieldExplicitly)]
public async Task TestNoDiagnostic_FileAttributes()
{
await VerifyNoDiagnosticAsync(@"
class C
{
void M()
{
var x = (System.IO.FileAttributes)0;
}
}
");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,9 @@ roslynator_analyzers.enabled_by_default = true|false
# Invalid argument null check
#dotnet_diagnostic.rcs1256.severity = suggestion
# Use enum field explicitly
#dotnet_diagnostic.rcs1257.severity = suggestion
# Use pattern matching
#dotnet_diagnostic.rcs9001.severity = silent
Expand Down

0 comments on commit 5f174d9

Please sign in to comment.