Skip to content

Commit 2098d5d

Browse files
committed
Added analyzer for non-abstract generic types inside a union.
1 parent 95590da commit 2098d5d

File tree

13 files changed

+311
-54
lines changed

13 files changed

+311
-54
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>8.5.4</VersionPrefix>
5+
<VersionPrefix>8.5.5</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
5050
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer,
5151
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial,
5252
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustNotHaveGenerics,
53-
DiagnosticsDescriptors.TypeMustNotBeInsideGenericType
53+
DiagnosticsDescriptors.TypeMustNotBeInsideGenericType,
54+
DiagnosticsDescriptors.NonAbstractUnionDerivedTypesMustNotBeGeneric
5455
];
5556

5657
/// <inheritdoc />
@@ -428,6 +429,20 @@ private static void ValidateUnion(
428429
{
429430
CheckConstructors(context, type, mustBePrivate: true, canHavePrimaryConstructor: false);
430431
TypeMustBePartial(context, type);
432+
NonAbstractDerivedTypesMustNotBeGeneric(context, type);
433+
}
434+
435+
private static void NonAbstractDerivedTypesMustNotBeGeneric(OperationAnalysisContext context, INamedTypeSymbol unionType)
436+
{
437+
var derivedTypes = unionType.FindDerivedInnerTypes();
438+
439+
for (var i = 0; i < derivedTypes.Count; i++)
440+
{
441+
var (type, _, _) = derivedTypes[i];
442+
443+
if (!type.IsAbstract && type.Arity != 0)
444+
ReportDiagnostic(context, DiagnosticsDescriptors.NonAbstractUnionDerivedTypesMustNotBeGeneric, GetDerivedTypeLocation(context, type), type);
445+
}
431446
}
432447

433448
private static void ValidateKeyedValueObject(
@@ -778,7 +793,7 @@ private static void ValidateDerivedTypes(OperationAnalysisContext context, IName
778793

779794
for (var i = 0; i < derivedTypes.Count; i++)
780795
{
781-
var (type, level) = derivedTypes[i];
796+
var (type, _, level) = derivedTypes[i];
782797

783798
if (level == 1)
784799
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal static class DiagnosticsDescriptors
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);
3636
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);
37+
public static readonly DiagnosticDescriptor NonAbstractUnionDerivedTypesMustNotBeGeneric = new("TTRESG053", "Non-abstract derived type of a union must not be generic", "Non-abstract derived type '{0}' of a union must not be generic", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
3738

3839
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
3940
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,21 +413,21 @@ private bool IsEnumCandidate(TypeDeclarationSyntax typeDeclaration)
413413

414414
var hasCreateInvalidItemImplementation = keyMemberType is not null && settings.IsValidatable && type.HasCreateInvalidItemImplementation(keyMemberType, cancellationToken);
415415

416-
var derivedTypeNames = FindDerivedTypes(type);
416+
var derivedTypeDefinitionNames = FindDerivedTypes(type);
417417
var enumState = new EnumSourceGeneratorState(factory,
418418
type,
419419
keyMember,
420420
attributeInfo.ValidationError,
421421
new EnumSettings(settings, attributeInfo),
422422
hasCreateInvalidItemImplementation,
423-
derivedTypeNames.Count > 0,
423+
derivedTypeDefinitionNames.Count > 0,
424424
cancellationToken);
425425
var derivedTypes = new SmartEnumDerivedTypes(enumState.Namespace,
426426
enumState.Name,
427427
enumState.TypeFullyQualified,
428428
enumState.IsReferenceType,
429429
enumState.ContainingTypes,
430-
derivedTypeNames);
430+
derivedTypeDefinitionNames);
431431

432432
Logger.LogDebug("The type declaration is a valid smart enum", null, enumState);
433433

@@ -453,7 +453,7 @@ private static IReadOnlyList<string> FindDerivedTypes(INamedTypeSymbol type)
453453
return Array.Empty<string>();
454454

455455
return derivedTypes
456-
.Select(t => t.Type.ToFullyQualifiedDisplayString())
456+
.Select(t => t.TypeDef.ToFullyQualifiedDisplayString())
457457
.Distinct()
458458
.ToList();
459459
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private List<TypeMember> OrderTypeMembers(
4040
{
4141
var typeMember = _state.TypeMembers[i];
4242

43-
if (typeMember.BaseTypeFullyQualified == state.TypeFullyQualified)
43+
if (typeMember.BaseTypeDefinitionFullyQualified == state.TypeDefinitionFullyQualified)
4444
{
4545
typeMembers.Add(MakeTypeMember(typeMember, sb));
4646
}
@@ -60,7 +60,7 @@ private List<TypeMember> OrderTypeMembers(
6060

6161
for (var j = typeMembers.Count - 1; j >= 0; j--)
6262
{
63-
if (typeMembers[j].State.TypeFullyQualified != unsortedTypeMember.BaseTypeFullyQualified)
63+
if (typeMembers[j].State.TypeDefinitionFullyQualified != unsortedTypeMember.BaseTypeDefinitionFullyQualified)
6464
continue;
6565

6666
typeMembers.Add(MakeTypeMember(unsortedTypeMember, sb));

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public class UnionSourceGenState : IEquatable<UnionSourceGenState>, ITypeFullyQu
55
public string? Namespace { get; }
66
public string Name { get; }
77
public string TypeFullyQualified { get; }
8+
public string TypeDefinitionFullyQualified { get; }
89
public bool IsRecord { get; }
910
public bool HasNonDefaultConstructor { get; }
1011

@@ -20,21 +21,14 @@ public UnionSourceGenState(
2021
{
2122
Name = type.Name;
2223
Namespace = type.ContainingNamespace?.IsGlobalNamespace == true ? null : type.ContainingNamespace?.ToString();
24+
TypeFullyQualified = type.ToFullyQualifiedDisplayString();
25+
TypeDefinitionFullyQualified = type.GetGenericTypeDefinition().ToFullyQualifiedDisplayString();
2326
HasNonDefaultConstructor = !type.Constructors.IsDefaultOrEmpty && type.Constructors.Any(c => !c.IsImplicitlyDeclared);
2427
ContainingTypes = type.GetContainingTypes();
2528
GenericsFullyQualified = type.Arity == 0
2629
? []
2730
: type.TypeArguments.Select(t => t.ToFullyQualifiedDisplayString()).ToList();
2831

29-
if (type is { IsGenericType: true, IsUnboundGenericType: true })
30-
{
31-
TypeFullyQualified = type.OriginalDefinition.ToFullyQualifiedDisplayString();
32-
}
33-
else
34-
{
35-
TypeFullyQualified = type.ToFullyQualifiedDisplayString();
36-
}
37-
3832
IsRecord = type.IsRecord;
3933
TypeMembers = typeMembers;
4034
Settings = settings;

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,27 @@ private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationTo
9595
return null;
9696
}
9797

98-
var derivedTypes = type.FindDerivedInnerTypes()
99-
.Select(i => new UnionTypeMemberState(i.Type))
100-
.ToList();
98+
var derivedTypeInfos = type.FindDerivedInnerTypes();
10199

102-
if (derivedTypes.Count == 0)
100+
if (derivedTypeInfos.Count == 0)
103101
{
104102
Logger.LogDebug("Union has no derived types", tds);
105103
return null;
106104
}
107105

106+
var derivedTypes = new List<UnionTypeMemberState>(derivedTypeInfos.Count);
107+
108+
foreach (var derivedTypeInfo in derivedTypeInfos)
109+
{
110+
if (!derivedTypeInfo.Type.IsAbstract && derivedTypeInfo.Type.Arity != 0)
111+
{
112+
Logger.LogDebug("Derived type of a union must not have generic parameters, unless it is abstract", tds);
113+
return null;
114+
}
115+
116+
derivedTypes.Add(new UnionTypeMemberState(derivedTypeInfo.Type, derivedTypeInfo.TypeDef));
117+
}
118+
108119
var settings = new UnionSettings(context.Attributes[0]);
109120

110121
var unionState = new UnionSourceGenState(type,

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,27 @@ namespace Thinktecture.CodeAnalysis.Unions;
33
public class UnionTypeMemberState : IEquatable<UnionTypeMemberState>, ITypeFullyQualified, IHashCodeComputable
44
{
55
public string TypeFullyQualified { get; }
6+
public string TypeDefinitionFullyQualified { get; }
67
public string Name { get; }
78
public bool IsAbstract { get; }
89
public string BaseTypeFullyQualified { get; }
10+
public string BaseTypeDefinitionFullyQualified { get; }
911
public IReadOnlyList<ContainingTypeState> ContainingTypes { get; }
1012

1113
public UnionTypeMemberState(
12-
INamedTypeSymbol type)
14+
INamedTypeSymbol type,
15+
INamedTypeSymbol typeDefinition)
1316
{
17+
if (type.BaseType is null)
18+
throw new InvalidOperationException($"Inner union type ''{TypeFullyQualified} must have a base type.");
19+
1420
Name = type.Name;
1521
TypeFullyQualified = type.ToFullyQualifiedDisplayString();
22+
TypeDefinitionFullyQualified = typeDefinition.ToFullyQualifiedDisplayString();
23+
BaseTypeFullyQualified = type.BaseType.ToFullyQualifiedDisplayString();
24+
BaseTypeDefinitionFullyQualified = type.BaseType.GetGenericTypeDefinition().ToFullyQualifiedDisplayString();
1625
IsAbstract = type.IsAbstract;
1726

18-
if (type is { IsGenericType: true, IsUnboundGenericType: true })
19-
{
20-
TypeFullyQualified = type.OriginalDefinition.ToFullyQualifiedDisplayString();
21-
BaseTypeFullyQualified = type.OriginalDefinition.BaseType?.ToFullyQualifiedDisplayString()
22-
?? throw new InvalidOperationException($"Inner union type ''{TypeFullyQualified} must have a base type.");
23-
}
24-
else
25-
{
26-
TypeFullyQualified = type.ToFullyQualifiedDisplayString();
27-
BaseTypeFullyQualified = type.BaseType?.ToFullyQualifiedDisplayString()
28-
?? throw new InvalidOperationException($"Inner union type ''{TypeFullyQualified} must have a base type.");
29-
}
30-
3127
ContainingTypes = type.GetContainingTypes();
3228
}
3329

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

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -379,21 +379,33 @@ public static bool IsIComparisonOperators(this INamedTypeSymbol @interface, ITyp
379379
&& @interface.TypeArguments[2].SpecialType == SpecialType.System_Boolean;
380380
}
381381

382-
public static IReadOnlyList<(INamedTypeSymbol Type, int Level)> FindDerivedInnerTypes(
383-
this ITypeSymbol baseType)
382+
public static INamedTypeSymbol GetGenericTypeDefinition(
383+
this INamedTypeSymbol type)
384384
{
385-
List<(INamedTypeSymbol, int Level)>? derivedTypes = null;
385+
return type is { IsGenericType: true, IsUnboundGenericType: false }
386+
? type.ConstructUnboundGenericType()
387+
: type;
388+
}
389+
390+
public static IReadOnlyList<(INamedTypeSymbol Type, INamedTypeSymbol TypeDef, int Level)> FindDerivedInnerTypes(
391+
this INamedTypeSymbol baseType)
392+
{
393+
List<(INamedTypeSymbol, INamedTypeSymbol, int Level)>? derivedTypes = null;
386394

387-
FindDerivedInnerTypes(baseType, 0, baseType, ref derivedTypes);
395+
FindDerivedInnerTypes(
396+
baseType,
397+
0,
398+
(baseType, baseType.GetGenericTypeDefinition()),
399+
ref derivedTypes);
388400

389-
return derivedTypes ?? (IReadOnlyList<(INamedTypeSymbol Type, int Level)>)Array.Empty<(INamedTypeSymbol Type, int Level)>();
401+
return derivedTypes ?? (IReadOnlyList<(INamedTypeSymbol, INamedTypeSymbol, int)>)Array.Empty<(INamedTypeSymbol, INamedTypeSymbol, int)>();
390402
}
391403

392404
private static void FindDerivedInnerTypes(
393-
ITypeSymbol typeToCheck,
405+
INamedTypeSymbol typeToCheck,
394406
int currentLevel,
395-
ITypeSymbol baseType,
396-
ref List<(INamedTypeSymbol, int Level)>? derivedTypes)
407+
(INamedTypeSymbol Type, INamedTypeSymbol TypeDef) baseType,
408+
ref List<(INamedTypeSymbol Type, INamedTypeSymbol TypeDef, int Level)>? derivedTypes)
397409
{
398410
currentLevel++;
399411

@@ -412,31 +424,28 @@ private static void FindDerivedInnerTypes(
412424

413425
if (IsDerivedFrom(innerType, baseType))
414426
{
415-
var derivedType = innerType;
416-
417-
if (derivedType is { IsGenericType: true, IsUnboundGenericType: false })
418-
derivedType = derivedType.ConstructUnboundGenericType();
419-
420-
(derivedTypes ??= []).Add((derivedType, currentLevel));
427+
(derivedTypes ??= []).Add((innerType, innerType.GetGenericTypeDefinition(), currentLevel));
421428
}
422429

423430
FindDerivedInnerTypes(innerType, currentLevel, baseType, ref derivedTypes);
424431
}
425432
}
426433

427-
private static bool IsDerivedFrom(this ITypeSymbol? type, ITypeSymbol baseType)
434+
private static bool IsDerivedFrom(
435+
this ITypeSymbol? type,
436+
(INamedTypeSymbol Type, INamedTypeSymbol TypeDef) baseType)
428437
{
429438
while (!type.IsNullOrObject())
430439
{
431-
if (baseType.TypeKind == TypeKind.Interface)
440+
if (baseType.Type.TypeKind == TypeKind.Interface)
432441
{
433442
foreach (var @interface in type.Interfaces)
434443
{
435-
if (SymbolEqualityComparer.Default.Equals(@interface, baseType))
444+
if (SymbolEqualityComparer.Default.Equals(@interface, baseType.Type))
436445
return true;
437446
}
438447
}
439-
else if (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType))
448+
else if (SymbolEqualityComparer.Default.Equals(type.BaseType?.GetGenericTypeDefinition(), baseType.TypeDef))
440449
{
441450
return true;
442451
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Threading.Tasks;
2+
using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier<Thinktecture.CodeAnalysis.Diagnostics.ThinktectureRuntimeExtensionsAnalyzer, Thinktecture.CodeAnalysis.CodeFixes.ThinktectureRuntimeExtensionsCodeFixProvider>;
3+
4+
namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests;
5+
6+
// ReSharper disable InconsistentNaming
7+
public class TTRESG053_NonAbstractUnionDerivedTypesMustNotBeGeneric
8+
{
9+
private const string _DIAGNOSTIC_ID = "TTRESG053";
10+
11+
public class Non_abstract_unions_must_not_be_generic
12+
{
13+
[Fact]
14+
public async Task Should_trigger_on_generic_class()
15+
{
16+
var code = """
17+
18+
using System;
19+
using Thinktecture;
20+
21+
namespace TestNamespace
22+
{
23+
[Union]
24+
public partial class TestUnion<T>
25+
{
26+
public class {|#0:First|}<T>(T Value) : TestUnion<T>;
27+
}
28+
}
29+
""";
30+
31+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("First<T>");
32+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly], expected);
33+
}
34+
35+
[Fact]
36+
public async Task Should_not_trigger_on_non_generic_class()
37+
{
38+
var code = """
39+
40+
using System;
41+
using Thinktecture;
42+
43+
namespace TestNamespace
44+
{
45+
[Union]
46+
public partial class TestUnion<T>
47+
{
48+
public class {|#0:First|}(T Value) : TestUnion<T>;
49+
}
50+
}
51+
""";
52+
53+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly]);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)