Skip to content
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
134 changes: 134 additions & 0 deletions src/DataverseAnalyzer/EnumAssignmentAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace DataverseAnalyzer;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class EnumAssignmentAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor Rule = new(
"CT0002",
Resources.CT0002_Title,
Resources.CT0002_MessageFormat,
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Resources.CT0002_Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
ArgumentNullException.ThrowIfNull(context);
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeAssignmentExpression, SyntaxKind.SimpleAssignmentExpression);
context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration);
}

private static void AnalyzeAssignmentExpression(SyntaxNodeAnalysisContext context)
{
var assignment = (AssignmentExpressionSyntax)context.Node;
AnalyzeEnumAssignment(context, assignment.Left, assignment.Right);
}

private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context)
{
var property = (PropertyDeclarationSyntax)context.Node;
if (property.Initializer is not null)
{
AnalyzeEnumAssignmentForProperty(context, property, property.Initializer.Value);
}
}

private static void AnalyzeEnumAssignmentForProperty(SyntaxNodeAnalysisContext context, PropertyDeclarationSyntax property, ExpressionSyntax right)
{
// Check if the right side is a numeric literal
if (right is not LiteralExpressionSyntax literal || !IsNumericLiteral(literal))
{
return;
}

// Get the type of the property from its declaration
var propertyTypeInfo = context.SemanticModel.GetTypeInfo(property.Type);
var targetType = propertyTypeInfo.Type;

// Handle nullable enum types
if (targetType is null)
{
return;
}

if (targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
targetType = ((INamedTypeSymbol)targetType).TypeArguments[0];
}

// Check if the target type is an enum
if (targetType.TypeKind != TypeKind.Enum)
{
return;
}

var targetName = property.Identifier.ValueText;
var literalValue = literal.Token.ValueText;

var diagnostic = Diagnostic.Create(Rule, right.GetLocation(), targetName, literalValue);
context.ReportDiagnostic(diagnostic);
}

private static void AnalyzeEnumAssignment(SyntaxNodeAnalysisContext context, SyntaxNode left, ExpressionSyntax right)
{
// Check if the right side is a numeric literal
if (right is not LiteralExpressionSyntax literal || !IsNumericLiteral(literal))
{
return;
}

// Get the type of the left side
var leftTypeInfo = context.SemanticModel.GetTypeInfo(left);
var targetType = leftTypeInfo.Type;

// Handle nullable enum types
if (targetType is null)
{
return;
}

if (targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
targetType = ((INamedTypeSymbol)targetType).TypeArguments[0];
}

// Check if the target type is an enum
if (targetType.TypeKind != TypeKind.Enum)
{
return;
}

var targetName = GetTargetName(left);
var literalValue = literal.Token.ValueText;

var diagnostic = Diagnostic.Create(Rule, right.GetLocation(), targetName, literalValue);
context.ReportDiagnostic(diagnostic);
}

private static bool IsNumericLiteral(LiteralExpressionSyntax literal)
{
return literal.Token.IsKind(SyntaxKind.NumericLiteralToken);
}

private static string GetTargetName(SyntaxNode left)
{
return left switch
{
PropertyDeclarationSyntax property => property.Identifier.ValueText,
IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText,
_ => "property",
};
}
}
6 changes: 6 additions & 0 deletions src/DataverseAnalyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/DataverseAnalyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@
<data name="CT0001_CodeFix_Title" xml:space="preserve">
<value>Add braces</value>
</data>
<data name="CT0002_Title" xml:space="preserve">
<value>Enum properties should not be assigned literal values</value>
</data>
<data name="CT0002_MessageFormat" xml:space="preserve">
<value>Enum property '{0}' should not be assigned literal value '{1}'. Use the appropriate enum value instead</value>
</data>
<data name="CT0002_Description" xml:space="preserve">
<value>Enum properties should be assigned enum values rather than literal numeric values to improve code readability and maintainability.</value>
</data>
</root>
Loading