Skip to content

Commit 5998a57

Browse files
committed
Replaced DefinedEnums by enum validation.
1 parent d7a3e5b commit 5998a57

22 files changed

+730
-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: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
// Flag each possible assigned value that is not either validated through one of the extension methods or an acceptable constant
60+
var locations = EnumerateUnvalidatedValues(assignment.Value, memberRef, enumType)
61+
.Select(operation => operation.Syntax.GetLocation());
62+
63+
foreach (var location in locations)
64+
{
65+
var diagnostic = Diagnostic.Create(
66+
DiagnosticDescriptor,
67+
location);
68+
69+
context.ReportDiagnostic(diagnostic);
70+
}
71+
}
72+
73+
private static IEnumerable<IOperation> EnumerateUnvalidatedValues(IOperation operation, IMemberReferenceOperation member, ITypeSymbol enumType)
74+
{
75+
// Dig through up to two conversions
76+
var operationWithoutConversion = operation switch
77+
{
78+
IConversionOperation { Operand: IConversionOperation conversion } => conversion.Operand,
79+
IConversionOperation conversion => conversion.Operand,
80+
_ => operation,
81+
};
82+
83+
// Recurse into the arms of ternaries and switch expressions
84+
if (operationWithoutConversion is IConditionalOperation conditional)
85+
{
86+
foreach (var result in EnumerateUnvalidatedValues(conditional.WhenTrue, member, enumType))
87+
yield return result;
88+
foreach (var result in conditional.WhenFalse is null ? [] : EnumerateUnvalidatedValues(conditional.WhenFalse, member, enumType))
89+
yield return result;
90+
yield break;
91+
}
92+
if (operationWithoutConversion is ISwitchExpressionOperation switchExpression)
93+
{
94+
foreach (var arm in switchExpression.Arms)
95+
foreach (var result in EnumerateUnvalidatedValues(arm.Value, member, enumType))
96+
yield return result;
97+
yield break;
98+
}
99+
100+
// Ignore throw expressions
101+
if (operationWithoutConversion is IThrowOperation)
102+
yield break;
103+
104+
// Ignore if validated by AsDefined() or the like
105+
if (IsValidatedWithExtensionMethod(operationWithoutConversion))
106+
yield break;
107+
108+
var constantValue = operation.ConstantValue;
109+
110+
// Dig through up to two conversions
111+
if (operation is IConversionOperation conversionOperation)
112+
{
113+
if (!constantValue.HasValue && conversionOperation.Operand.ConstantValue.HasValue)
114+
constantValue = conversionOperation.Operand.ConstantValue.Value;
115+
116+
if (conversionOperation.Operand is IConversionOperation nestedConversionOperation)
117+
{
118+
if (!constantValue.HasValue && nestedConversionOperation.Operand.ConstantValue.HasValue)
119+
constantValue = nestedConversionOperation.Operand.ConstantValue.Value;
120+
}
121+
}
122+
123+
// Ignore if assigning null or a defined constant
124+
if (constantValue.HasValue && (constantValue.Value is null || IsDefinedEnumConstantOrNullableThereof(enumType, constantValue.Value)))
125+
yield break;
126+
127+
// Ignore if assigning default(T?) (i.e. null) or default (i.e. null) to a nullable member
128+
// Note: We need to use the "operation" var directly to correctly evaluate the conversions
129+
if (operation is IDefaultValueOperation or IConversionOperation { Operand: IDefaultValueOperation { ConstantValue.HasValue: false } } && member.Type.IsNullable(out _))
130+
yield break;
131+
132+
yield return operation;
133+
}
134+
135+
private static bool IsValidatedWithExtensionMethod(IOperation operation)
136+
{
137+
if (operation is not IInvocationOperation invocation)
138+
return false;
139+
140+
var method = invocation.TargetMethod;
141+
method = method.ReducedFrom ?? method; // value.AsDefined() vs. EnumExtensions.AsDefined()
142+
143+
if (method.Name is not "AsDefined" and not "AsDefinedFlags" and not "AsUnvalidated")
144+
return false;
145+
146+
if (method.ContainingType is not { Name: "EnumExtensions", ContainingNamespace: { Name: "DomainModeling", ContainingNamespace: { Name: "Architect", ContainingNamespace.IsGlobalNamespace: true } } })
147+
return false;
148+
149+
return true;
150+
}
151+
152+
private static bool IsDefinedEnumConstantOrNullableThereof(ITypeSymbol enumType, object constantValue)
153+
{
154+
if (enumType is not INamedTypeSymbol { EnumUnderlyingType: { } } namedEnumType)
155+
return false;
156+
157+
var binaryValue = GetBinaryValue(namedEnumType.EnumUnderlyingType, constantValue);
158+
159+
var valueIsDefined = namedEnumType.GetMembers().Any(member =>
160+
member is IFieldSymbol { ConstantValue: { } value } && GetBinaryValue(namedEnumType.EnumUnderlyingType, value) == binaryValue);
161+
162+
return valueIsDefined;
163+
}
164+
165+
private static ulong? GetBinaryValue(ITypeSymbol enumUnderlyingType, object value)
166+
{
167+
return (enumUnderlyingType.SpecialType, Type.GetTypeCode(value.GetType())) switch
168+
{
169+
(SpecialType.System_Byte, TypeCode.Byte) => Convert.ToByte(value),
170+
(SpecialType.System_SByte, TypeCode.SByte) => (ulong)Convert.ToSByte(value),
171+
(SpecialType.System_UInt16, TypeCode.UInt16) => Convert.ToUInt16(value),
172+
(SpecialType.System_Int16, TypeCode.Int16) => (ulong)Convert.ToInt16(value),
173+
(SpecialType.System_UInt32, TypeCode.UInt32) => Convert.ToUInt32(value),
174+
(SpecialType.System_Int32, TypeCode.Int32) => (ulong)Convert.ToInt32(value),
175+
(SpecialType.System_UInt64, TypeCode.UInt64) => Convert.ToUInt64(value),
176+
(SpecialType.System_Int64, TypeCode.Int64) => (ulong)Convert.ToInt64(value),
177+
_ => null,
178+
};
179+
}
180+
}

0 commit comments

Comments
 (0)