Skip to content

Commit d647e27

Browse files
committed
[Analyzer] Value objects and smart enums must not be inside a generic class
1 parent e438edf commit d647e27

File tree

12 files changed

+286
-35
lines changed

12 files changed

+286
-35
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ public sealed class ContainingTypeState : IEquatable<ContainingTypeState>, IHash
55
public string Name { get; }
66
public bool IsReferenceType { get; }
77
public bool IsRecord { get; }
8+
public IReadOnlyList<GenericTypeParameterState> GenericParameters { get; }
89

910
public ContainingTypeState(
1011
string name,
1112
bool isReferenceType,
12-
bool isRecord)
13+
bool isRecord,
14+
IReadOnlyList<GenericTypeParameterState> genericParameters)
1315
{
1416
Name = name;
1517
IsReferenceType = isReferenceType;
1618
IsRecord = isRecord;
19+
GenericParameters = genericParameters;
1720
}
1821

1922
public bool Equals(ContainingTypeState? other)
@@ -26,7 +29,8 @@ public bool Equals(ContainingTypeState? other)
2629

2730
return Name == other.Name
2831
&& IsReferenceType == other.IsReferenceType
29-
&& IsRecord == other.IsRecord;
32+
&& IsRecord == other.IsRecord
33+
&& GenericParameters.SequenceEqual(other.GenericParameters);
3034
}
3135

3236
public override bool Equals(object? obj)
@@ -41,6 +45,7 @@ public override int GetHashCode()
4145
var hashCode = Name.GetHashCode();
4246
hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode();
4347
hashCode = (hashCode * 397) ^ IsRecord.GetHashCode();
48+
hashCode = (hashCode * 397) ^ GenericParameters.ComputeHashCode();
4449

4550
return hashCode;
4651
}

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
4949
DiagnosticsDescriptors.ExplicitComparerWithoutEqualityComparer,
5050
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer,
5151
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial,
52-
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustNotHaveGenerics
52+
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustNotHaveGenerics,
53+
DiagnosticsDescriptors.TypeMustNotBeInsideGenericType
5354
];
5455

5556
/// <inheritdoc />
@@ -418,7 +419,7 @@ private static void ValidateAdHocUnion(
418419

419420
CheckConstructors(context, type, mustBePrivate: false, canHavePrimaryConstructor: false);
420421
TypeMustBePartial(context, type);
421-
TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration, "Union");
422+
TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration);
422423
}
423424

424425
private static void ValidateUnion(
@@ -581,7 +582,8 @@ private static void ValidateCustomKeyMemberImplementation(
581582

582583
CheckConstructors(context, type, mustBePrivate: false, canHavePrimaryConstructor: false);
583584
TypeMustBePartial(context, type);
584-
TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration, "Value Object");
585+
TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration);
586+
TypeMustNotBeInsideGenericType(context, type, locationOfFirstDeclaration);
585587

586588
var assignableMembers = type.GetAssignableFieldsAndPropertiesAndCheckForReadOnly(factory, false, true, context.CancellationToken, context)
587589
.Where(m => !m.IsStatic)
@@ -678,7 +680,8 @@ private static void ValidateEnum(
678680

679681
CheckConstructors(context, enumType, mustBePrivate: true, canHavePrimaryConstructor: false);
680682
TypeMustBePartial(context, enumType);
681-
TypeMustNotBeGeneric(context, enumType, locationOfFirstDeclaration, "Enumeration");
683+
TypeMustNotBeGeneric(context, enumType, locationOfFirstDeclaration);
684+
TypeMustNotBeInsideGenericType(context, enumType, locationOfFirstDeclaration);
682685

683686
var items = enumType.GetEnumItems();
684687

@@ -741,10 +744,16 @@ private static void ValidateKeyedSmartEnum(
741744
ValidateKeyMemberComparers(context, enumType, keyType, attribute, locationOfFirstDeclaration, factory, false);
742745
}
743746

744-
private static void TypeMustNotBeGeneric(OperationAnalysisContext context, INamedTypeSymbol type, Location locationOfFirstDeclaration, string typeKind)
747+
private static void TypeMustNotBeGeneric(OperationAnalysisContext context, INamedTypeSymbol type, Location locationOfFirstDeclaration)
745748
{
746749
if (!type.TypeParameters.IsDefaultOrEmpty)
747-
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsValueObjectsAndAdHocUnionsMustNotBeGeneric, locationOfFirstDeclaration, typeKind, BuildTypeName(type));
750+
ReportDiagnostic(context, DiagnosticsDescriptors.EnumsValueObjectsAndAdHocUnionsMustNotBeGeneric, locationOfFirstDeclaration, BuildTypeName(type));
751+
}
752+
753+
private static void TypeMustNotBeInsideGenericType(OperationAnalysisContext context, INamedTypeSymbol type, Location locationOfFirstDeclaration)
754+
{
755+
if (type.IsNestedInGenericClass())
756+
ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustNotBeInsideGenericType, locationOfFirstDeclaration, BuildTypeName(type));
748757
}
749758

750759
private static void Check_ItemLike_StaticProperties(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class DiagnosticsDescriptors
1717
public static readonly DiagnosticDescriptor InnerEnumOnFirstLevelMustBePrivate = new("TTRESG014", "First-level inner enumerations must be private", "Derived inner enumeration '{0}' on first-level must be private", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
1818
public static readonly DiagnosticDescriptor InnerEnumOnNonFirstLevelMustBePublic = new("TTRESG015", "Non-first-level inner enumerations must be public", "Derived inner enumeration '{0}' on non-first-level must be public", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
1919
public static readonly DiagnosticDescriptor KeyMemberShouldNotBeNullable = new("TTRESG017", "The key member must not be nullable", "A key member must not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
20-
public static readonly DiagnosticDescriptor EnumsValueObjectsAndAdHocUnionsMustNotBeGeneric = new("TTRESG033", "Enumerations, value objects and ad hoc unions must not be generic", "{0} '{1}' must not be generic", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
20+
public static readonly DiagnosticDescriptor EnumsValueObjectsAndAdHocUnionsMustNotBeGeneric = new("TTRESG033", "Enumerations, value objects and ad hoc unions must not be generic", "Type '{1}' must not be generic", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2121
public static readonly DiagnosticDescriptor BaseClassFieldMustBeReadOnly = new("TTRESG034", "Field of the base class must be read-only", "The field '{0}' of the base class '{1}' must be read-only", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2222
public static readonly DiagnosticDescriptor BaseClassPropertyMustBeReadOnly = new("TTRESG035", "Property of the base class must be read-only", "The property '{0}' of the base class '{1}' must be read-only", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
2323
public static readonly DiagnosticDescriptor EnumKeyShouldNotBeNullable = new("TTRESG036", "The key must not be nullable", "The generic type T of SmartEnumAttribute<T> must not be nullable", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
@@ -33,6 +33,7 @@ internal static class DiagnosticsDescriptors
3333
public static readonly DiagnosticDescriptor ComplexValueObjectWithStringMembersNeedsDefaultEqualityComparer = new("TTRESG049", "Complex Value Object with string members needs equality comparer", "Complex Value Object with string members needs equality comparer. Use ValueObjectKeyMemberEqualityComparerAttribute<TAccessor, TKey> to defined the equality comparer. Example: [ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>].", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3434
public static readonly DiagnosticDescriptor MethodWithUseDelegateFromConstructorMustBePartial = new("TTRESG050", $"Method with '{Constants.Attributes.UseDelegateFromConstructorAttribute.NAME}' must be partial", $"The method '{{0}}' with '{Constants.Attributes.UseDelegateFromConstructorAttribute.NAME}' must be marked as partial", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3535
public static readonly DiagnosticDescriptor MethodWithUseDelegateFromConstructorMustNotHaveGenerics = new("TTRESG051", $"Method with '{Constants.Attributes.UseDelegateFromConstructorAttribute.NAME}' must not have generics", $"The method '{{0}}' with '{Constants.Attributes.UseDelegateFromConstructorAttribute.NAME}' must not have generic type parameters. Use inheritance approach instead.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
36+
public static readonly DiagnosticDescriptor TypeMustNotBeInsideGenericType = new("TTRESG052", "The type must not be inside generic type", "Type '{0}' must not be inside a generic type", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3637

3738
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3839
public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation", "Error during code generation for '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ private bool IsEnumCandidate(TypeDeclarationSyntax typeDeclaration)
344344
return null;
345345
}
346346

347+
if (type.IsNestedInGenericClass())
348+
{
349+
Logger.LogDebug("Type must not be inside a generic class", tds);
350+
return null;
351+
}
352+
347353
ITypeSymbol? keyMemberType = null;
348354

349355
if (isKeyed)

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Unions/UnionSourceGenerator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationTo
9999
.Select(i => new UnionTypeMemberState(i.Type))
100100
.ToList();
101101

102+
if (derivedTypes.Count == 0)
103+
{
104+
Logger.LogDebug("Union has no derived types", tds);
105+
return null;
106+
}
107+
102108
var settings = new UnionSettings(context.Attributes[0]);
103109

104110
var unionState = new UnionSourceGenState(type,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,12 @@ private bool IsValueObjectCandidate(TypeDeclarationSyntax typeDeclaration)
458458
return null;
459459
}
460460

461+
if (type.IsNestedInGenericClass())
462+
{
463+
Logger.LogDebug("Type must not be inside a generic class", tds);
464+
return null;
465+
}
466+
461467
var factory = TypedMemberStateFactoryProvider.GetFactoryOrNull(context.SemanticModel.Compilation, Logger);
462468

463469
if (factory is null)
@@ -503,6 +509,12 @@ private bool IsValueObjectCandidate(TypeDeclarationSyntax typeDeclaration)
503509
if (!TryGetType(context, tds, out var type))
504510
return null;
505511

512+
if (type.IsNestedInGenericClass())
513+
{
514+
Logger.LogDebug("Type must not be inside a generic class", tds);
515+
return null;
516+
}
517+
506518
var factory = TypedMemberStateFactoryProvider.GetFactoryOrNull(context.SemanticModel.Compilation, Logger);
507519

508520
if (factory is null)

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/MethodSymbolExtensions.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,6 @@ public static bool IsArithmeticOperator(this IMethodSymbol method, ITypeSymbol t
2929

3030
public static IReadOnlyList<GenericTypeParameterState> GetGenericTypeParameters(this IMethodSymbol method)
3131
{
32-
if (method.TypeParameters.Length <= 0)
33-
return [];
34-
35-
var genericTypeParameters = new List<GenericTypeParameterState>(method.TypeParameters.Length);
36-
37-
for (var i = 0; i < method.TypeParameters.Length; i++)
38-
{
39-
var typeParam = method.TypeParameters[i];
40-
var constraints = typeParam.GetConstraints();
41-
42-
genericTypeParameters.Add(new GenericTypeParameterState(
43-
typeParam.Name,
44-
constraints));
45-
}
46-
47-
return genericTypeParameters ?? [];
32+
return method.TypeParameters.GetGenericTypeParameters();
4833
}
4934
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/NamedTypeSymbolExtensions.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ private static IReadOnlyList<ConstructorState> GetConstructors(
5353
return ctorStates ?? (IReadOnlyList<ConstructorState>)Array.Empty<ConstructorState>();
5454
}
5555

56+
public static IReadOnlyList<GenericTypeParameterState> GetGenericTypeParameters(this INamedTypeSymbol type)
57+
{
58+
return type.TypeParameters.GetGenericTypeParameters();
59+
}
60+
5661
public static IReadOnlyList<ContainingTypeState> GetContainingTypes(
5762
this INamedTypeSymbol type)
5863
{
@@ -64,12 +69,32 @@ public static IReadOnlyList<ContainingTypeState> GetContainingTypes(
6469

6570
while (containingType != null)
6671
{
67-
types.Add(new ContainingTypeState(containingType.Name, containingType.IsReferenceType, containingType.IsRecord));
72+
var typeState = new ContainingTypeState(
73+
containingType.Name,
74+
containingType.IsReferenceType,
75+
containingType.IsRecord,
76+
containingType.GetGenericTypeParameters());
77+
types.Add(typeState);
6878
containingType = containingType.ContainingType;
6979
}
7080

7181
types.Reverse();
7282

7383
return types;
7484
}
85+
86+
public static bool IsNestedInGenericClass(this INamedTypeSymbol type)
87+
{
88+
var containingType = type.ContainingType;
89+
90+
while (containingType != null)
91+
{
92+
if (!containingType.TypeParameters.IsDefaultOrEmpty)
93+
return true;
94+
95+
containingType = containingType.ContainingType;
96+
}
97+
98+
return false;
99+
}
75100
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ public static StringBuilder RenderContainingTypesStart(
305305
: containingType.IsReferenceType ? "class " : "struct ";
306306

307307
sb.Append(@"
308-
partial ").Append(typeKind).Append(containingType.Name).Append(@"
308+
partial ").Append(typeKind).Append(containingType.Name).AppendGenericTypeParameters(containingType.GenericParameters).Append(@"
309309
{");
310310
}
311311

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Thinktecture.CodeAnalysis;
2+
3+
namespace Thinktecture;
4+
5+
public static class TypeParameterSymbolExtensions
6+
{
7+
public static IReadOnlyList<GenericTypeParameterState> GetGenericTypeParameters(this ImmutableArray<ITypeParameterSymbol> generics)
8+
{
9+
if (generics.IsDefaultOrEmpty)
10+
return [];
11+
12+
var genericTypeParameters = new List<GenericTypeParameterState>(generics.Length);
13+
14+
for (var i = 0; i < generics.Length; i++)
15+
{
16+
var typeParam = generics[i];
17+
var constraints = typeParam.GetConstraints();
18+
19+
genericTypeParameters.Add(new GenericTypeParameterState(
20+
typeParam.Name,
21+
constraints));
22+
}
23+
24+
return genericTypeParameters;
25+
}
26+
}

0 commit comments

Comments
 (0)