Skip to content

Commit 2ab4519

Browse files
committed
Add diagnostic and code fix for equality/comparison mismatch
1 parent 3097e05 commit 2ab4519

33 files changed

Lines changed: 696 additions & 220 deletions

File tree

docs

Submodule docs updated from 345ecf0 to 765d788

samples/Basic.Samples/SmartEnums/ProductGroup.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
namespace Thinktecture.SmartEnums;
22

3-
[SmartEnum<int>(ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
4-
SkipToString = true)]
3+
[SmartEnum<int>(
4+
ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
5+
EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
6+
SkipToString = true)]
57
public partial class ProductGroup
68
{
79
public static readonly ProductGroup Apple = new(1, "Apple", ProductCategory.Fruits);

samples/Basic.Samples/ValueObjects/Amount.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ namespace Thinktecture.ValueObjects;
33
[ValueObject<decimal>(DefaultInstancePropertyName = "Zero", // renames Amount.Empty to Amount.Zero
44
AllowDefaultStructs = true,
55
ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for comparison of amount with a decimal without implicit conversion: amount > 42m
6-
AdditionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for arithmetic operations of amount with a decimal without implicit conversion: amount + 42m
6+
EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
7+
AdditionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for arithmetic operations of amount with a decimal without implicit conversion: amount + 42m
78
SubtractionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
89
MultiplyOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
910
DivisionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)]

samples/Basic.Samples/ValueObjects/AmountClass.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Thinktecture.ValueObjects;
22

33
[ValueObject<int>(ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
4+
EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
45
AdditionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
56
SubtractionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
67
MultiplyOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads,
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
### New Rules
22

3-
Rule ID | Category | Severity | Notes
4-
-----------|---------------------------------------|----------|------------------------
5-
TTRESG061 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
6-
TTRESG062 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
7-
TTRESG063 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
8-
TTRESG064 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
9-
TTRESG065 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
3+
Rule ID | Category | Severity | Notes
4+
--------|----------|----------|-------
5+
TTRESG061 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
6+
TTRESG062 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
7+
TTRESG063 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
8+
TTRESG064 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
9+
TTRESG065 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
10+
TTRESG105 | ThinktectureRuntimeExtensionsAnalyzer | Warning | DiagnosticsDescriptors

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvid
2020
private const string _SEAL_CLASS = "Seal class";
2121
private const string _DEFINE_VALUE_OBJECT_EQUALITY_COMPARER = "Define Value Object equality comparer";
2222
private const string _DEFINE_VALUE_OBJECT_COMPARER = "Define Value Object comparer";
23+
private const string _ALIGN_COMPARISON_EQUALITY_OPERATORS = "Align comparison/equality operators";
2324

2425
/// <inheritdoc />
2526
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
@@ -39,6 +40,7 @@ public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvid
3940
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial.Id,
4041
DiagnosticsDescriptors.UnionRecordMustBeSealed.Id,
4142
DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired.Id,
43+
DiagnosticsDescriptors.ComparisonAndEqualityOperatorsMismatch.Id,
4244
];
4345

4446
/// <inheritdoc />
@@ -60,7 +62,11 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
6062
CodeFixesContext? ctx = null;
6163
CodeFixesContext GetCodeFixesContext() => ctx ??= new CodeFixesContext(diagnostic, root);
6264

63-
if (diagnostic.Id == DiagnosticsDescriptors.TypeMustBePartial.Id)
65+
if (diagnostic.Id == DiagnosticsDescriptors.ComparisonAndEqualityOperatorsMismatch.Id)
66+
{
67+
context.RegisterCodeFix(CodeAction.Create(_ALIGN_COMPARISON_EQUALITY_OPERATORS, t => AlignComparisonEqualityOperatorsAsync(context.Document, root, GetCodeFixesContext().TypeDeclaration, t), _ALIGN_COMPARISON_EQUALITY_OPERATORS), diagnostic);
68+
}
69+
else if (diagnostic.Id == DiagnosticsDescriptors.TypeMustBePartial.Id)
6470
{
6571
context.RegisterCodeFix(CodeAction.Create(_MAKE_PARTIAL, _ => AddTypeModifierAsync(context.Document, root, GetCodeFixesContext().TypeDeclaration, SyntaxKind.PartialKeyword), _MAKE_PARTIAL), diagnostic);
6672
}
@@ -402,22 +408,148 @@ private static ThrowStatementSyntax BuildThrowNotImplementedException()
402408
return throwStatement;
403409
}
404410

405-
private sealed class CodeFixesContext(Diagnostic diagnostic, SyntaxNode root)
411+
private static async Task<Document> AlignComparisonEqualityOperatorsAsync(
412+
Document document,
413+
SyntaxNode root,
414+
TypeDeclarationSyntax? declaration,
415+
CancellationToken cancellationToken)
406416
{
407-
private TypeDeclarationSyntax? _typeDeclaration;
408-
public TypeDeclarationSyntax? TypeDeclaration => _typeDeclaration ??= GetDeclaration<TypeDeclarationSyntax>();
417+
if (declaration is null)
418+
return document;
419+
420+
var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
421+
422+
if (model is null)
423+
return document;
424+
425+
var type = model.GetDeclaredSymbol(declaration, cancellationToken);
426+
427+
// Find SmartEnum or ValueObject attribute
428+
AttributeData? targetAttributeData = null;
429+
430+
if (type.IsSmartEnum(out var smartEnumAttribute))
431+
{
432+
targetAttributeData = smartEnumAttribute;
433+
}
434+
else if (type.IsKeyedValueObjectType(out var valueObjectAttribute))
435+
{
436+
targetAttributeData = valueObjectAttribute;
437+
}
409438

410-
private FieldDeclarationSyntax? _fieldDeclaration;
411-
public FieldDeclarationSyntax? FieldDeclaration => _fieldDeclaration ??= GetDeclaration<FieldDeclarationSyntax>();
439+
if (targetAttributeData?.ApplicationSyntaxReference is null)
440+
return document;
441+
442+
var attributeSyntax = await targetAttributeData.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken) as AttributeSyntax;
443+
444+
if (attributeSyntax is null)
445+
return document;
412446

413-
private MemberDeclarationSyntax? _memberDeclaration;
414-
public MemberDeclarationSyntax? MemberDeclaration => _memberDeclaration ??= GetDeclaration<MemberDeclarationSyntax>();
447+
var argumentList = attributeSyntax.ArgumentList;
448+
AttributeArgumentSyntax? comparisonArg = null;
449+
AttributeArgumentSyntax? equalityArg = null;
450+
451+
if (argumentList is not null)
452+
{
453+
for (var i = 0; i < argumentList.Arguments.Count; i++)
454+
{
455+
var arg = argumentList.Arguments[i];
456+
var name = arg.NameEquals?.Name.Identifier.Text;
457+
458+
switch (name)
459+
{
460+
case Constants.Attributes.Properties.COMPARISON_OPERATORS:
461+
comparisonArg = arg;
462+
break;
463+
case Constants.Attributes.Properties.EQUALITY_COMPARISON_OPERATORS:
464+
equalityArg = arg;
465+
break;
466+
}
467+
}
468+
}
415469

416-
private PropertyDeclarationSyntax? _propertyDeclaration;
417-
public PropertyDeclarationSyntax? PropertyDeclaration => _propertyDeclaration ??= GetDeclaration<PropertyDeclarationSyntax>();
470+
var comparisonValue = GetOperatorsGenerationValue(model, comparisonArg?.Expression, cancellationToken);
471+
var equalityValue = GetOperatorsGenerationValue(model, equalityArg?.Expression, cancellationToken);
418472

419-
private MethodDeclarationSyntax? _methodDeclaration;
420-
public MethodDeclarationSyntax? MethodDeclaration => _methodDeclaration ??= GetDeclaration<MethodDeclarationSyntax>();
473+
if (comparisonValue == equalityValue)
474+
return document;
475+
476+
var newOperatorsGeneration = (comparisonValue, equalityValue) switch
477+
{
478+
(null, not null) => equalityValue,
479+
(not null, null) => comparisonValue,
480+
(not null, not null) => comparisonValue is OperatorsGeneration.None || equalityValue is OperatorsGeneration.None
481+
? OperatorsGeneration.None
482+
: comparisonValue.Value > equalityValue.Value ? comparisonValue : equalityValue,
483+
(_, _) => OperatorsGeneration.None
484+
};
485+
486+
var newArgumentList = argumentList ?? SyntaxFactory.AttributeArgumentList();
487+
488+
if (comparisonValue != newOperatorsGeneration)
489+
{
490+
var newArg = SyntaxFactory.AttributeArgument(SyntaxFactory.ParseExpression($"OperatorsGeneration.{newOperatorsGeneration.ToString()}"))
491+
.WithNameEquals(SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName(Constants.Attributes.Properties.COMPARISON_OPERATORS)));
492+
493+
newArgumentList = comparisonArg is null
494+
? newArgumentList.WithArguments(newArgumentList.Arguments.Add(newArg))
495+
: newArgumentList.ReplaceNode(comparisonArg, newArg);
496+
}
497+
498+
if (equalityValue != newOperatorsGeneration)
499+
{
500+
var newArg = SyntaxFactory.AttributeArgument(SyntaxFactory.ParseExpression($"OperatorsGeneration.{newOperatorsGeneration.ToString()}"))
501+
.WithNameEquals(SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName(Constants.Attributes.Properties.EQUALITY_COMPARISON_OPERATORS)));
502+
503+
newArgumentList = equalityArg is null
504+
? newArgumentList.WithArguments(newArgumentList.Arguments.Add(newArg))
505+
: newArgumentList.ReplaceNode(equalityArg, newArg);
506+
}
507+
508+
var newAttribute = attributeSyntax.WithArgumentList(newArgumentList);
509+
var newDeclaration = declaration.ReplaceNode(attributeSyntax, newAttribute);
510+
var newRoot = root.ReplaceNode(declaration, newDeclaration);
511+
return document.WithSyntaxRoot(newRoot);
512+
}
513+
514+
// Helper that tries to resolve the enum value of an expression referencing OperatorsGeneration.
515+
private static OperatorsGeneration? GetOperatorsGenerationValue(SemanticModel model, ExpressionSyntax? expr, CancellationToken cancellationToken)
516+
{
517+
if (expr is null)
518+
return null;
519+
520+
// Try constant value first.
521+
var constant = model.GetConstantValue(expr, cancellationToken);
522+
523+
if (constant.HasValue)
524+
{
525+
switch (constant.Value)
526+
{
527+
case int i when Enum.IsDefined(typeof(OperatorsGeneration), i):
528+
return (OperatorsGeneration)i;
529+
case OperatorsGeneration og:
530+
return og;
531+
}
532+
}
533+
534+
// Try symbol info (enum field access or qualified name)
535+
var symbol = model.GetSymbolInfo(expr, cancellationToken).Symbol;
536+
537+
if (symbol is IFieldSymbol { ContainingType: { TypeKind: TypeKind.Enum, Name: nameof(OperatorsGeneration) } } field)
538+
{
539+
if (Enum.TryParse(field.Name, out OperatorsGeneration parsed))
540+
return parsed;
541+
}
542+
543+
return null;
544+
}
545+
546+
private sealed class CodeFixesContext(Diagnostic diagnostic, SyntaxNode root)
547+
{
548+
public TypeDeclarationSyntax? TypeDeclaration => field ??= GetDeclaration<TypeDeclarationSyntax>();
549+
public FieldDeclarationSyntax? FieldDeclaration => field ??= GetDeclaration<FieldDeclarationSyntax>();
550+
public MemberDeclarationSyntax? MemberDeclaration => field ??= GetDeclaration<MemberDeclarationSyntax>();
551+
public PropertyDeclarationSyntax? PropertyDeclaration => field ??= GetDeclaration<PropertyDeclarationSyntax>();
552+
public MethodDeclarationSyntax? MethodDeclaration => field ??= GetDeclaration<MethodDeclarationSyntax>();
421553

422554
private T? GetDeclaration<T>()
423555
where T : MemberDeclarationSyntax

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
6060
DiagnosticsDescriptors.TypeMustNotHaveMoveThanOneValueObjectAttribute,
6161
DiagnosticsDescriptors.TypeMustNotHaveMoveThanOneDiscriminatedUnionAttribute,
6262
DiagnosticsDescriptors.AdHocUnionMustHaveAtLeastTwoMemberTypes,
63+
DiagnosticsDescriptors.ComparisonAndEqualityOperatorsMismatch,
6364
];
6465

6566
/// <inheritdoc />
@@ -588,6 +589,29 @@ private static void ValidateKeyedValueObject(
588589
keyType.Name);
589590
}
590591
}
592+
593+
if (attribute.FindSkipEqualityComparison() != true)
594+
CheckForComparisonMismatch(context, type, attribute);
595+
}
596+
597+
private static void CheckForComparisonMismatch(
598+
OperationAnalysisContext context,
599+
INamedTypeSymbol type,
600+
IObjectCreationOperation attribute)
601+
{
602+
var comparison = attribute.FindComparisonOperators();
603+
var equality = attribute.FindEqualityComparisonOperators();
604+
605+
if (comparison != equality)
606+
{
607+
ReportDiagnostic(
608+
context,
609+
DiagnosticsDescriptors.ComparisonAndEqualityOperatorsMismatch,
610+
attribute.Syntax.GetLocation(),
611+
type.Name,
612+
comparison.ToString(),
613+
equality.ToString());
614+
}
591615
}
592616

593617
private static void ValidateKeyMemberComparers(
@@ -872,6 +896,7 @@ private static void ValidateSmartEnum(
872896
ValidateEnumDerivedTypes(context, enumType);
873897
EnumKeyMemberNameMustNotBeItem(context, attribute, tdsLocation);
874898
ValidateKeyedSmartEnum(context, enumType, attribute, tdsLocation, factory);
899+
CheckForComparisonMismatch(context, enumType, attribute);
875900
}
876901

877902
private static void ValidateKeyedSmartEnum(
@@ -1163,9 +1188,27 @@ private static void ReportDiagnostic(OperationAnalysisContext context, Diagnosti
11631188
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, arg0, arg1));
11641189
}
11651190

1166-
private static void ReportDiagnostic(OperationAnalysisContext context, DiagnosticDescriptor descriptor, Location location, string arg0, string arg1, string arg2)
1191+
private static void ReportDiagnostic(
1192+
OperationAnalysisContext context,
1193+
DiagnosticDescriptor descriptor,
1194+
Location location,
1195+
string arg0,
1196+
string arg1,
1197+
string arg2)
1198+
{
1199+
ReportDiagnostic(context, descriptor, location, null, arg0, arg1, arg2);
1200+
}
1201+
1202+
private static void ReportDiagnostic(
1203+
OperationAnalysisContext context,
1204+
DiagnosticDescriptor descriptor,
1205+
Location location,
1206+
IEnumerable<Location>? additionalLocations,
1207+
string arg0,
1208+
string arg1,
1209+
string arg2)
11671210
{
1168-
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, arg0, arg1, arg2));
1211+
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs: [arg0, arg1, arg2], additionalLocations: additionalLocations));
11691212
}
11701213

11711214
private static void ReportDiagnostic(OperationAnalysisContext context, DiagnosticDescriptor descriptor, Location location, ITypeSymbol arg0, string arg1)

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ internal static class DiagnosticsDescriptors
5454
public static readonly DiagnosticDescriptor ExplicitComparerWithoutEqualityComparer = new("TTRESG102", "The type has a comparer defined but no equality comparer", "The type '{0}' has a comparer defined but no equality comparer", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
5555
public static readonly DiagnosticDescriptor ExplicitEqualityComparerWithoutComparer = new("TTRESG103", "The type has an equality comparer defined but no comparer", "The type '{0}' has an equality comparer defined but no comparer", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
5656
public static readonly DiagnosticDescriptor MembersDisallowingDefaultValuesMustBeRequired = new("TTRESG104", "The member must be marked as 'required' to ensure proper initialization", "The {0} '{1}' of type '{2}' must be marked as 'required' to ensure proper initialization", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
57+
public static readonly DiagnosticDescriptor ComparisonAndEqualityOperatorsMismatch = new("TTRESG105", "Comparison and equality operators settings mismatch", "The type '{0}' has 'ComparisonOperators = {1}' and 'EqualityComparisonOperators = {2}' which differ. Set them to the same value.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
5758

5859
public static readonly DiagnosticDescriptor InternalApiUsage = new("TTRESG1000", "Internal Thinktecture.Runtime.Extensions API usage", "'{0}' is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to the same compatibility standards as public APIs. It may be changed or removed without notice in any release.", nameof(ThinktectureRuntimeExtensionsInternalUsageAnalyzer), DiagnosticSeverity.Warning, true);
5960
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/AllEnumSettings.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ public AllEnumSettings(AttributeData attribute)
3737
SwitchMapStateParameterName = attribute.FindSwitchMapStateParameterName();
3838

3939
// Comparison operators depend on the equality comparison operators
40-
if (ComparisonOperators > EqualityComparisonOperators)
40+
if (EqualityComparisonOperators == OperatorsGeneration.None)
41+
{
42+
ComparisonOperators = OperatorsGeneration.None;
43+
}
44+
else if (ComparisonOperators > EqualityComparisonOperators)
45+
{
4146
EqualityComparisonOperators = ComparisonOperators;
47+
}
4248
}
4349

4450
public override bool Equals(object? obj)

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/AllValueObjectSettings.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,14 @@ public AllValueObjectSettings(
6565
SerializationFrameworks = valueObjectAttribute.FindSerializationFrameworks();
6666

6767
// Comparison operators depend on the equality comparison operators
68-
if (ComparisonOperators > EqualityComparisonOperators)
69-
EqualityComparisonOperators = ComparisonOperators;
70-
71-
if (SkipEqualityComparison)
68+
if (SkipEqualityComparison || EqualityComparisonOperators == OperatorsGeneration.None)
7269
{
73-
ComparisonOperators = OperatorsGeneration.None;
74-
EqualityComparisonOperators = OperatorsGeneration.None;
70+
ComparisonOperators = OperatorsGeneration.None;
71+
EqualityComparisonOperators = OperatorsGeneration.None;
72+
}
73+
else if (ComparisonOperators > EqualityComparisonOperators)
74+
{
75+
EqualityComparisonOperators = ComparisonOperators;
7576
}
7677
}
7778

0 commit comments

Comments
 (0)