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

Add analyzer "Use enum field explicitly" (RCS1257) #889

Merged
merged 21 commits into from
Aug 19, 2023
Merged
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
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