Skip to content

Commit

Permalink
Add analyzer "Unnecessary enum flag" (RCS1258) (#886)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt authored Aug 19, 2023
1 parent 4fc79a0 commit f057b97
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 0 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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.
- Add analyzer "Unnecessary enum flag" [RCS1258](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1258.md) ([#886](https://github.com/JosefPihrt/Roslynator/pull/886)).
- Enabled by default.

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 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;

namespace Roslynator.CSharp.CodeFixes;

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

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

if (!TryFindFirstAncestorOrSelf(root, context.Span, out MemberAccessExpressionSyntax memberAccessExpression))
return;

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

switch (diagnostic.Id)
{
case DiagnosticIdentifiers.UnnecessaryEnumFlag:
{
CodeAction codeAction = CodeAction.Create(
"Remove unnecessary flag",
ct =>
{
var bitwiseOr = (BinaryExpressionSyntax)memberAccessExpression.Parent;
ExpressionSyntax newExpression = (bitwiseOr.Left == memberAccessExpression)
? bitwiseOr.Right
: bitwiseOr.Left;
newExpression = newExpression.WithTriviaFrom(bitwiseOr);
return document.ReplaceNodeAsync(bitwiseOr, newExpression, ct);
},
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
break;
}
}
}
}
14 changes: 14 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7368,6 +7368,20 @@ void M()
</Sample>
</Samples>
</Analyzer>
<Analyzer Identifier="UnnecessaryEnumFlag" IsDevelopment="true">
<Id>RCS1258</Id>
<Title>Unnecessary enum flag.</Title>
<MessageFormat>Enum flag '{0}' is already contained in flag '{1}'.</MessageFormat>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<SupportsFadeOut>true</SupportsFadeOut>
<Samples>
<Sample>
<Before><![CDATA[var styles = NumberStyles.Integer | NumberStyles.AllowLeadingWhite]]></Before>
<After><![CDATA[var styles = NumberStyles.Integer]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
95 changes: 95 additions & 0 deletions src/Analyzers/CSharp/Analysis/UnnecessaryEnumFlagAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 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 UnnecessaryEnumFlagAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

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

return _supportedDiagnostics;
}
}

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

context.RegisterSyntaxNodeAction(c => AnalyzeBitwiseOrExpression(c), SyntaxKind.BitwiseOrExpression);
}

private static void AnalyzeBitwiseOrExpression(SyntaxNodeAnalysisContext context)
{
var bitwiseAnd = (BinaryExpressionSyntax)context.Node;

foreach (ExpressionSyntax expression in bitwiseAnd.AsChain())
{
if (!expression.IsKind(SyntaxKind.SimpleMemberAccessExpression))
return;
}

ITypeSymbol symbol = context.SemanticModel.GetTypeSymbol(bitwiseAnd, context.CancellationToken);

if (!symbol.HasAttribute(MetadataNames.System_FlagsAttribute))
return;

var enumSymbol = (INamedTypeSymbol)symbol;
var values = new List<(ExpressionSyntax, ulong)>();

foreach (ExpressionSyntax expression in bitwiseAnd.AsChain())
{
Optional<object> constantValueOpt = context.SemanticModel.GetConstantValue(expression, context.CancellationToken);

if (constantValueOpt.HasValue)
{
ulong value = SymbolUtility.GetEnumValueAsUInt64(constantValueOpt.Value, enumSymbol);
var addToValues = true;

for (int i = values.Count - 1; i >= 0; i--)
{
(ExpressionSyntax expression2, ulong value2) = values[i];

if ((value & value2) != 0)
{
if (value <= value2)
{
ReportDiagnostic(expression, expression2);
addToValues = false;
}
else
{
ReportDiagnostic(expression2, expression);
values.RemoveAt(i);
}
}
}

if (addToValues)
values.Add((expression, value));
}
}

void ReportDiagnostic(ExpressionSyntax expression, ExpressionSyntax expression2)
{
context.ReportDiagnostic(
DiagnosticRules.UnnecessaryEnumFlag,
expression,
context.SemanticModel.GetSymbol(expression, context.CancellationToken).Name,
context.SemanticModel.GetSymbol(expression2, context.CancellationToken).Name);
}
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,6 @@ public static partial class DiagnosticIdentifiers
public const string SimplifyArgumentNullCheck = "RCS1255";
public const string InvalidArgumentNullCheck = "RCS1256";
public const string UseEnumFieldExplicitly = "RCS1257";
public const string UnnecessaryEnumFlag = "RCS1258";
}
}
12 changes: 12 additions & 0 deletions src/Analyzers/CSharp/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2533,5 +2533,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.UseEnumFieldExplicitly,
customTags: Array.Empty<string>());

/// <summary>RCS1258</summary>
public static readonly DiagnosticDescriptor UnnecessaryEnumFlag = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.UnnecessaryEnumFlag,
title: "Unnecessary enum flag.",
messageFormat: "Enum flag '{0}' is already contained in flag '{1}'.",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.UnnecessaryEnumFlag,
customTags: WellKnownDiagnosticTags.Unnecessary);

}
}
82 changes: 82 additions & 0 deletions src/Tests/Analyzers.Tests/RCS1258UnnecessaryEnumFlagTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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 RCS1258UnnecessaryEnumFlagTests : AbstractCSharpDiagnosticVerifier<UnnecessaryEnumFlagAnalyzer, UnnecessaryEnumFlagCodeFixProvider>
{
public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.UnnecessaryEnumFlag;

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryEnumFlag)]
public async Task Test()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Globalization;
class C
{
void M()
{
var styles = NumberStyles.Integer | [|NumberStyles.AllowLeadingWhite|];
}
}
", @"
using System.Globalization;
class C
{
void M()
{
var styles = NumberStyles.Integer;
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryEnumFlag)]
public async Task Test2()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Globalization;
class C
{
void M()
{
var styles = [|NumberStyles.AllowLeadingWhite|] | [|NumberStyles.AllowTrailingWhite|] | NumberStyles.Integer;
}
}
", @"
using System.Globalization;
class C
{
void M()
{
var styles = NumberStyles.Integer;
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryEnumFlag)]
public async Task TestNoDiagnostic()
{
await VerifyNoDiagnosticAsync(@"
using System.Globalization;
class C
{
void M()
{
var styles = NumberStyles.AllowCurrencySymbol | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowHexSpecifier | NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite | NumberStyles.AllowParentheses | NumberStyles.AllowThousands | NumberStyles.AllowTrailingSign | NumberStyles.AllowTrailingWhite;
}
}
");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,9 @@ roslynator_analyzers.enabled_by_default = true|false
# Use enum field explicitly
#dotnet_diagnostic.rcs1257.severity = suggestion
# Unnecessary enum flag
#dotnet_diagnostic.rcs1258.severity = suggestion
# Use pattern matching
#dotnet_diagnostic.rcs9001.severity = silent
Expand Down

0 comments on commit f057b97

Please sign in to comment.