diff --git a/ChangeLog.md b/ChangeLog.md index bd7cb88746..9febd45f62 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryEnumFlagCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryEnumFlagCodeFixProvider.cs new file mode 100644 index 0000000000..e289836def --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UnnecessaryEnumFlagCodeFixProvider.cs @@ -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 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; + } + } + } +} diff --git a/src/Analyzers.xml b/src/Analyzers.xml index 2c334157e6..99eaf79f86 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -7368,6 +7368,20 @@ void M() + + RCS1258 + Unnecessary enum flag. + Enum flag '{0}' is already contained in flag '{1}'. + Info + true + true + + + + + + + RCS9001 UsePatternMatching diff --git a/src/Analyzers/CSharp/Analysis/UnnecessaryEnumFlagAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UnnecessaryEnumFlagAnalyzer.cs new file mode 100644 index 0000000000..2ba984900e --- /dev/null +++ b/src/Analyzers/CSharp/Analysis/UnnecessaryEnumFlagAnalyzer.cs @@ -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 _supportedDiagnostics; + + public override ImmutableArray 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 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); + } + } +} diff --git a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index 721968b654..a98c7280ed 100644 --- a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -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"; } } \ No newline at end of file diff --git a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs index 725c04660d..fa2337e1f9 100644 --- a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs @@ -2533,5 +2533,17 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.UseEnumFieldExplicitly, customTags: Array.Empty()); + /// RCS1258 + 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); + } } \ No newline at end of file diff --git a/src/Tests/Analyzers.Tests/RCS1258UnnecessaryEnumFlagTests.cs b/src/Tests/Analyzers.Tests/RCS1258UnnecessaryEnumFlagTests.cs new file mode 100644 index 0000000000..8fc4b8c85c --- /dev/null +++ b/src/Tests/Analyzers.Tests/RCS1258UnnecessaryEnumFlagTests.cs @@ -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 +{ + 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; + } +} +"); + } +} diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index ead0f0f981..e8978a1f93 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -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