From 6bef4c636bfb21c163947646a12817f9260d8586 Mon Sep 17 00:00:00 2001 From: Ian Kemp Date: Mon, 18 Feb 2019 20:33:19 +0200 Subject: [PATCH] implement analyzers, codefix and refactoring --- docs/analyzers/RCS1235.md | 32 ++++++++++ docs/analyzers/RCS1236.md | 26 ++++++++ ...teadOfCastNumericLiteralCodeFixProvider.cs | 37 +++++++++++ ...dInsteadOfCastNumericLiteralRefactoring.cs | 50 +++++++++++++++ .../NumericLiteralOutOfEnumRangeAnalyzer.cs | 63 +++++++++++++++++++ ...ieldInsteadOfCastNumericLiteralAnalyzer.cs | 63 +++++++++++++++++++ .../CSharp/DiagnosticDescriptors.Generated.cs | 23 +++++++ .../CSharp/DiagnosticIdentifiers.Generated.cs | 2 + 8 files changed, 296 insertions(+) create mode 100644 docs/analyzers/RCS1235.md create mode 100644 docs/analyzers/RCS1236.md create mode 100644 src/Analyzers.CodeFixes/CSharp/CodeFixes/UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider.cs create mode 100644 src/Analyzers.CodeFixes/CSharp/Refactorings/UseEnumFieldInsteadOfCastNumericLiteralRefactoring.cs create mode 100644 src/Analyzers/CSharp/Analysis/NumericLiteralOutOfEnumRangeAnalyzer.cs create mode 100644 src/Analyzers/CSharp/Analysis/UseEnumFieldInsteadOfCastNumericLiteralAnalyzer.cs diff --git a/docs/analyzers/RCS1235.md b/docs/analyzers/RCS1235.md new file mode 100644 index 0000000000..8bffeaa738 --- /dev/null +++ b/docs/analyzers/RCS1235.md @@ -0,0 +1,32 @@ +# RCS1235: Use enumeration member instead of casting numeric literal value + +| Property | Value | +| -------- | ----------- | +| Id | RCS1235 | +| Category | Design | +| Severity | Info | + +## Example + +### Code with Diagnostic + +```csharp +enum E { + F = 123, +}; + +var f = (E)123; RCS1235 +``` + +### Code with Fix + +```csharp +var f = E.F; +``` + +## See Also + +* [How to Suppress a Diagnostic](../HowToConfigureAnalyzers.md#how-to-suppress-a-diagnostic) + + +*\(Generated with [DotMarkdown](http://github.com/JosefPihrt/DotMarkdown)\)* \ No newline at end of file diff --git a/docs/analyzers/RCS1236.md b/docs/analyzers/RCS1236.md new file mode 100644 index 0000000000..7ceaa7bd0f --- /dev/null +++ b/docs/analyzers/RCS1236.md @@ -0,0 +1,26 @@ +# RCS1236: Value is not defined in enumeration + +| Property | Value | +| -------- | ----------- | +| Id | RCS1236 | +| Category | Design | +| Severity | Warning | + +## Example + +### Code with Diagnostic + +```csharp +enum E { + F = 123, +}; + +var f = (E)987; RCS1236 +``` + +## See Also + +* [How to Suppress a Diagnostic](../HowToConfigureAnalyzers.md#how-to-suppress-a-diagnostic) + + +*\(Generated with [DotMarkdown](http://github.com/JosefPihrt/DotMarkdown)\)* \ No newline at end of file diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider.cs new file mode 100644 index 0000000000..7f95e3cc64 --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider.cs @@ -0,0 +1,37 @@ +// Copyright (c) Josef Pihrt. All rights reserved. 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; +using Roslynator.CSharp.Refactorings; + +namespace Roslynator.CSharp.CodeFixes +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider))] + [Shared] + public class UseEnumFieldInsteadOfCastNumericLiteralCodeFixProvider : BaseCodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral); + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.GetSyntaxRootAsync().ConfigureAwait(false); + if (!TryFindFirstAncestorOrSelf(root, context.Span, out CastExpressionSyntax castExpression)) + { + return; + } + + var codeAction = CodeAction.Create( + "Use enumeration field", + cancellationToken => UseEnumFieldInsteadOfCastNumericLiteralRefactoring.RefactorAsync(context.Document, castExpression, cancellationToken), + GetEquivalenceKey(DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral)); + + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + } +} diff --git a/src/Analyzers.CodeFixes/CSharp/Refactorings/UseEnumFieldInsteadOfCastNumericLiteralRefactoring.cs b/src/Analyzers.CodeFixes/CSharp/Refactorings/UseEnumFieldInsteadOfCastNumericLiteralRefactoring.cs new file mode 100644 index 0000000000..da348e1e50 --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/Refactorings/UseEnumFieldInsteadOfCastNumericLiteralRefactoring.cs @@ -0,0 +1,50 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Roslynator.CSharp.Refactorings +{ + internal static class UseEnumFieldInsteadOfCastNumericLiteralRefactoring + { + public static async Task RefactorAsync( + Document document, + CastExpressionSyntax castExpression, + CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync().ConfigureAwait(false); + var namedTypeSymbol = (INamedTypeSymbol)semanticModel.GetTypeSymbol(castExpression.Type, cancellationToken); + + var expression = (LiteralExpressionSyntax)castExpression.Expression; + var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol); + + var enumField = GetEnumFieldWithSpecifiedValue(namedTypeSymbol, numericLiteralValue); + + var newNode = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, castExpression.Type, SyntaxFactory.IdentifierName(enumField.Name)) + .WithOperatorToken(SyntaxFactory.Token(SyntaxKind.DotToken)); + + return await document.ReplaceNodeAsync(castExpression, newNode, cancellationToken).ConfigureAwait(false); + } + + private static EnumFieldSymbolInfo GetEnumFieldWithSpecifiedValue(INamedTypeSymbol enumSymbol, ulong value) + { + foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast()) + { + var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol); + if (fieldInfo.Value == value) + { + return fieldInfo; + } + } + + Debug.Fail("Enum must have field of this value as the analyser flagged this"); + + return default; + } + } +} diff --git a/src/Analyzers/CSharp/Analysis/NumericLiteralOutOfEnumRangeAnalyzer.cs b/src/Analyzers/CSharp/Analysis/NumericLiteralOutOfEnumRangeAnalyzer.cs new file mode 100644 index 0000000000..bfe9266e3f --- /dev/null +++ b/src/Analyzers/CSharp/Analysis/NumericLiteralOutOfEnumRangeAnalyzer.cs @@ -0,0 +1,63 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Roslynator.CSharp.Analysis +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class NumericLiteralOutOfEnumRangeAnalyzer : BaseDiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.NumericLiteralOutOfEnumRange); + + public override void Initialize(AnalysisContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + base.Initialize(context); + + context.RegisterSyntaxNodeAction(AnalyzeCastExpression, SyntaxKind.CastExpression); + } + + public static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context) + { + var castExpression = (CastExpressionSyntax)context.Node; + if (!(castExpression.Expression is LiteralExpressionSyntax expression)) + { + return; + } + + if (!(context.SemanticModel.GetTypeSymbol(castExpression.Type, context.CancellationToken) is INamedTypeSymbol namedTypeSymbol) + || namedTypeSymbol.TypeKind != TypeKind.Enum) + { + return; + } + + var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol); + if (!EnumHasDefinedFieldWithNumericLiteralValue(namedTypeSymbol, numericLiteralValue)) + { + DiagnosticHelpers.ReportDiagnostic(context, DiagnosticDescriptors.NumericLiteralOutOfEnumRange, context.Node); + } + } + + private static bool EnumHasDefinedFieldWithNumericLiteralValue(INamedTypeSymbol enumSymbol, ulong value) + { + foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast()) + { + var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol); + if (fieldInfo.Value == value) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Analyzers/CSharp/Analysis/UseEnumFieldInsteadOfCastNumericLiteralAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UseEnumFieldInsteadOfCastNumericLiteralAnalyzer.cs new file mode 100644 index 0000000000..9a130c2d6f --- /dev/null +++ b/src/Analyzers/CSharp/Analysis/UseEnumFieldInsteadOfCastNumericLiteralAnalyzer.cs @@ -0,0 +1,63 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Roslynator.CSharp.Analysis +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class UseEnumFieldInsteadOfCastNumericLiteralAnalyzer : BaseDiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseEnumFieldInsteadOfCastNumericLiteral); + + public override void Initialize(AnalysisContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + base.Initialize(context); + + context.RegisterSyntaxNodeAction(AnalyzeCastExpression, SyntaxKind.CastExpression); + } + + public static void AnalyzeCastExpression(SyntaxNodeAnalysisContext context) + { + var castExpression = (CastExpressionSyntax)context.Node; + if (!(castExpression.Expression is LiteralExpressionSyntax expression)) + { + return; + } + + if (!(context.SemanticModel.GetTypeSymbol(castExpression.Type, context.CancellationToken) is INamedTypeSymbol namedTypeSymbol) + || namedTypeSymbol.TypeKind != TypeKind.Enum) + { + return; + } + + var numericLiteralValue = SymbolUtility.GetEnumValueAsUInt64(expression.Token.Value, namedTypeSymbol); + if (EnumHasDefinedFieldWithNumericLiteralValue(namedTypeSymbol, numericLiteralValue)) + { + DiagnosticHelpers.ReportDiagnostic(context, DiagnosticDescriptors.UseEnumFieldInsteadOfCastNumericLiteral, context.Node); + } + } + + private static bool EnumHasDefinedFieldWithNumericLiteralValue(INamedTypeSymbol enumSymbol, ulong value) + { + foreach (var fieldSymbol in enumSymbol.GetMembers().Where(f => f.Kind == SymbolKind.Field).Cast()) + { + var fieldInfo = EnumFieldSymbolInfo.Create(fieldSymbol); + if (fieldInfo.Value == value) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Analyzers/CSharp/DiagnosticDescriptors.Generated.cs b/src/Analyzers/CSharp/DiagnosticDescriptors.Generated.cs index 7be45f355e..392869e84a 100644 --- a/src/Analyzers/CSharp/DiagnosticDescriptors.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticDescriptors.Generated.cs @@ -2569,5 +2569,28 @@ public static partial class DiagnosticDescriptors helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.DuplicateEnumValue}", customTags: Array.Empty()); + /// RCS1235 + public static readonly DiagnosticDescriptor UseEnumFieldInsteadOfCastNumericLiteral = new DiagnosticDescriptor( + id: DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral, + title: "Use enumeration member instead of casting numeric literal value.", + messageFormat: "Use enumeration member instead of casting numeric literal value.", + category: DiagnosticCategories.Design, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: null, + helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.UseEnumFieldInsteadOfCastNumericLiteral}", + customTags: Array.Empty()); + + /// RCS1236 + public static readonly DiagnosticDescriptor NumericLiteralOutOfEnumRange = new DiagnosticDescriptor( + id: DiagnosticIdentifiers.NumericLiteralOutOfEnumRange, + title: "Value is not defined in enumeration.", + messageFormat: "Value is not defined in enumeration.", + category: DiagnosticCategories.Design, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: null, + helpLinkUri: $"{HelpLinkUriRoot}{DiagnosticIdentifiers.NumericLiteralOutOfEnumRange}", + customTags: Array.Empty()); } } \ No newline at end of file diff --git a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index 64f7a67933..27c2243b7f 100644 --- a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -217,5 +217,7 @@ public static partial class DiagnosticIdentifiers public const string OrderElementsInDocumentationComment = "RCS1232"; public const string UseShortCircuitingOperator = "RCS1233"; public const string DuplicateEnumValue = "RCS1234"; + public const string UseEnumFieldInsteadOfCastNumericLiteral = "RCS1235"; + public const string NumericLiteralOutOfEnumRange = "RCS1236"; } } \ No newline at end of file