diff --git a/ChangeLog.md b/ChangeLog.md index 3885def36e..bd7cb88746 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/CastExpressionCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/CastExpressionCodeFixProvider.cs new file mode 100644 index 0000000000..5b27af3bf0 --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/CastExpressionCodeFixProvider.cs @@ -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 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 UseEnumFieldExplicitlyAsync( + CastExpressionSyntax castExpression, + Document document, + CancellationToken cancellationToken) + { + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + Optional 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 flags = FlagsUtility.Instance.GetFlags(value).ToList(); + + List 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() + .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)); + } + } +} diff --git a/src/Analyzers.xml b/src/Analyzers.xml index 9ccef0e95c..2c334157e6 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -7356,6 +7356,18 @@ void M() - optional and its default value is `null`. + + RCS1257 + Use enum field explicitly. + Info + true + + + + + + + RCS9001 UsePatternMatching diff --git a/src/Analyzers/CSharp/Analysis/UseEnumFieldExplicitlyAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UseEnumFieldExplicitlyAnalyzer.cs new file mode 100644 index 0000000000..74b5d16f1b --- /dev/null +++ b/src/Analyzers/CSharp/Analysis/UseEnumFieldExplicitlyAnalyzer.cs @@ -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 _supportedDiagnostics; + + public override ImmutableArray 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 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.Instance.IsComposite(value)) + { + EnumSymbolInfo enumInfo = EnumSymbolInfo.Create(enumSymbol); + + foreach (ulong flag in FlagsUtility.Instance.GetFlags(value)) + { + if (!enumInfo.Contains(flag)) + return; + } + + context.ReportDiagnostic(DiagnosticRules.UseEnumFieldExplicitly, castExpression); + } + } +} diff --git a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index f851844545..721968b654 100644 --- a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -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"; } } \ No newline at end of file diff --git a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs index 184d31ff9a..725c04660d 100644 --- a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs @@ -2521,5 +2521,17 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.InvalidArgumentNullCheck, customTags: Array.Empty()); + /// RCS1257 + 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()); + } } \ No newline at end of file diff --git a/src/Tests/Analyzers.Tests/RCS1257UseEnumFieldExplicitlyTests.cs b/src/Tests/Analyzers.Tests/RCS1257UseEnumFieldExplicitlyTests.cs new file mode 100644 index 0000000000..c814d98e13 --- /dev/null +++ b/src/Tests/Analyzers.Tests/RCS1257UseEnumFieldExplicitlyTests.cs @@ -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 +{ + 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; + } +} +"); + } +} diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index 46030c2698..ead0f0f981 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -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