Skip to content

Commit cf6a1a3

Browse files
committed
Replaced DefinedEnums by enum validation.
1 parent 3412ecc commit cf6a1a3

22 files changed

+688
-2048
lines changed

DomainModeling.Analyzer/Analyzers/DefinedEnumMemberWithoutPrimitiveAnalyzer.cs

Lines changed: 0 additions & 86 deletions
This file was deleted.

DomainModeling.Analyzer/Analyzers/ImplicitDefinedEnumConversionFromNonConstantAnalyzer.cs

Lines changed: 0 additions & 109 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using Microsoft.CodeAnalysis.Operations;
5+
6+
namespace Architect.DomainModeling.Analyzer.Analyzers;
7+
8+
/// <summary>
9+
/// Prevents assignment of unvalidated enum values to members of an IDomainObject.
10+
/// </summary>
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public sealed class UnvalidatedEnumMemberAssignmentAnalyzer : DiagnosticAnalyzer
13+
{
14+
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "False positive.")]
15+
[System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "Not yet implemented.")]
16+
private static readonly DiagnosticDescriptor DiagnosticDescriptor = new DiagnosticDescriptor(
17+
id: "UnvalidatedEnumAssignmentToDomainobject",
18+
title: "Unvalidated enum assignment to domain object member",
19+
messageFormat: "The assigned value was not validated. Use the AsDefined(), AsDefinedFlags(), or AsUnvalidated() extension methods to specify the intent.",
20+
category: "Usage",
21+
defaultSeverity: DiagnosticSeverity.Warning,
22+
isEnabledByDefault: true);
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [DiagnosticDescriptor];
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics);
29+
context.EnableConcurrentExecution();
30+
31+
context.RegisterOperationAction(AnalyzeAssignment,
32+
OperationKind.SimpleAssignment,
33+
OperationKind.CoalesceAssignment);
34+
}
35+
36+
private static void AnalyzeAssignment(OperationAnalysisContext context)
37+
{
38+
var assignment = (IAssignmentOperation)context.Operation;
39+
40+
if (assignment.Target is not IMemberReferenceOperation memberRef)
41+
return;
42+
43+
if (assignment.Value.Type is not { } assignedValueType)
44+
return;
45+
46+
// Dig through nullable
47+
if (assignedValueType.IsNullable(out var nullableUnderlyingType))
48+
assignedValueType = nullableUnderlyingType;
49+
50+
var memberType = memberRef.Type.IsNullable(out var memberNullableUnderlyingType) ? memberNullableUnderlyingType : memberRef.Type;
51+
if (memberType is not { TypeKind: TypeKind.Enum } enumType)
52+
return;
53+
54+
// Only if target member is a member of some IDomainObject
55+
if (!memberRef.Member.ContainingType.AllInterfaces.Any(interf =>
56+
interf is { Name: "IDomainObject", ContainingNamespace: { Name: "DomainModeling", ContainingNamespace: { Name: "Architect", ContainingNamespace.IsGlobalNamespace: true } } }))
57+
return;
58+
59+
// Ignore if valid by AsDefined() or the like
60+
if (IsValidatedWithExtensionMethod(assignment.Value))
61+
return;
62+
63+
var constantValue = assignment.Value.ConstantValue;
64+
65+
// Dig through up to two conversions
66+
if (assignment.Value is IConversionOperation conversionOperation)
67+
{
68+
// Ignore if valid by AsDefined() or the like
69+
if (IsValidatedWithExtensionMethod(conversionOperation.Operand))
70+
return;
71+
72+
if (!constantValue.HasValue && conversionOperation.Operand.ConstantValue.HasValue)
73+
constantValue = conversionOperation.Operand.ConstantValue.Value;
74+
75+
if (conversionOperation.Operand is IConversionOperation nestedConversionOperation)
76+
{
77+
// Ignore if valid by AsDefined() or the like
78+
if (IsValidatedWithExtensionMethod(nestedConversionOperation.Operand))
79+
return;
80+
81+
if (!constantValue.HasValue && nestedConversionOperation.Operand.ConstantValue.HasValue)
82+
constantValue = nestedConversionOperation.Operand.ConstantValue.Value;
83+
}
84+
}
85+
86+
// Ignore if assigning null or a defined constant
87+
if (constantValue.HasValue && (constantValue.Value is null || IsDefinedEnumConstantOrNullableThereof(enumType, constantValue.Value)))
88+
return;
89+
90+
// Ignore if assigning a nullable member with default(T?) (i.e. null) or default (i.e. null)
91+
if (assignment.Value is IDefaultValueOperation or IConversionOperation { Operand: IDefaultValueOperation { ConstantValue.HasValue: false } } && assignment.Target.Type.IsNullable(out _))
92+
return;
93+
94+
var diagnostic = Diagnostic.Create(
95+
DiagnosticDescriptor,
96+
assignment.Syntax.GetLocation());
97+
98+
context.ReportDiagnostic(diagnostic);
99+
}
100+
101+
private static bool IsValidatedWithExtensionMethod(IOperation operation)
102+
{
103+
if (operation is not IInvocationOperation invocation)
104+
return false;
105+
106+
var method = invocation.TargetMethod;
107+
method = method.ReducedFrom ?? method; // value.AsDefined() vs. EnumExtensions.AsDefined()
108+
109+
if (method.Name is not "AsDefined" and not "AsDefinedFlags" and not "AsUnvalidated")
110+
return false;
111+
112+
if (method.ContainingType is not { Name: "EnumExtensions", ContainingNamespace: { Name: "DomainModeling", ContainingNamespace: { Name: "Architect", ContainingNamespace.IsGlobalNamespace: true } } })
113+
return false;
114+
115+
return true;
116+
}
117+
118+
private static bool IsDefinedEnumConstantOrNullableThereof(ITypeSymbol enumType, object constantValue)
119+
{
120+
if (enumType is not INamedTypeSymbol { EnumUnderlyingType: { } } namedEnumType)
121+
return false;
122+
123+
var binaryValue = GetBinaryValue(namedEnumType.EnumUnderlyingType, constantValue);
124+
125+
var valueIsDefined = namedEnumType.GetMembers().Any(member =>
126+
member is IFieldSymbol { ConstantValue: { } value } && GetBinaryValue(namedEnumType.EnumUnderlyingType, value) == binaryValue);
127+
128+
return valueIsDefined;
129+
}
130+
131+
private static ulong? GetBinaryValue(ITypeSymbol enumUnderlyingType, object value)
132+
{
133+
return (enumUnderlyingType.SpecialType, Type.GetTypeCode(value.GetType())) switch
134+
{
135+
(SpecialType.System_Byte, TypeCode.Byte) => Convert.ToByte(value),
136+
(SpecialType.System_SByte, TypeCode.SByte) => (ulong)Convert.ToSByte(value),
137+
(SpecialType.System_UInt16, TypeCode.UInt16) => Convert.ToUInt16(value),
138+
(SpecialType.System_Int16, TypeCode.Int16) => (ulong)Convert.ToInt16(value),
139+
(SpecialType.System_UInt32, TypeCode.UInt32) => Convert.ToUInt32(value),
140+
(SpecialType.System_Int32, TypeCode.Int32) => (ulong)Convert.ToInt32(value),
141+
(SpecialType.System_UInt64, TypeCode.UInt64) => Convert.ToUInt64(value),
142+
(SpecialType.System_Int64, TypeCode.Int64) => (ulong)Convert.ToInt64(value),
143+
_ => null,
144+
};
145+
}
146+
}

0 commit comments

Comments
 (0)