Skip to content

Commit 054e9e4

Browse files
committed
add analyzer for regular unions
1 parent ade77f7 commit 054e9e4

5 files changed

Lines changed: 192 additions & 8 deletions

File tree

src/Thinktecture.Runtime.Extensions.SourceGenerator/AnalyzerReleases.Unshipped.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
TTRESG063 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
88
TTRESG064 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
99
TTRESG065 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
10-
TTRESG066 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
11-
TTRESG067 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
12-
TTRESG068 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
13-
TTRESG069 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
14-
TTRESG070 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
10+
TTRESG066 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
11+
TTRESG067 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
12+
TTRESG068 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
13+
TTRESG069 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
14+
TTRESG070 | ThinktectureRuntimeExtensionsAnalyzer | Error | DiagnosticsDescriptors
1515
TTRESG105 | ThinktectureRuntimeExtensionsAnalyzer | Warning | DiagnosticsDescriptors
16+
TTRESG106 | ThinktectureRuntimeExtensionsAnalyzer | Warning | DiagnosticsDescriptors

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
5050
DiagnosticsDescriptors.UnionMustBeSealedOrHavePrivateConstructorsOnly,
5151
DiagnosticsDescriptors.UnionRecordMustBeSealed,
5252
DiagnosticsDescriptors.NonAbstractDerivedUnionIsLessAccessibleThanBaseUnion,
53+
DiagnosticsDescriptors.InnerTypeDoesNotDeriveFromUnion,
5354
DiagnosticsDescriptors.AllowDefaultStructsCannotBeTrueIfValueObjectIsStructButKeyTypeIsClass,
5455
DiagnosticsDescriptors.AllowDefaultStructsCannotBeTrueIfSomeMembersDisallowDefaultValues,
5556
DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired,
@@ -523,6 +524,7 @@ private static void ValidateRegularUnion(
523524
{
524525
CheckConstructors(context, type, mustBePrivate: true, canHavePrimaryConstructor: false);
525526
TypeMustBePartial(context, type);
527+
CheckForNonDerivedUnionTypes(context, type);
526528
ValidateUnionDerivedTypes(context, type);
527529
}
528530

@@ -1163,6 +1165,31 @@ private static void ValidateUnionDerivedTypes(SymbolAnalysisContext context, INa
11631165
}
11641166
}
11651167

1168+
private static void CheckForNonDerivedUnionTypes(SymbolAnalysisContext context, INamedTypeSymbol type)
1169+
{
1170+
var allInnerTypes = type.GetTypeMembers();
1171+
var baseTypeTuple = (type, type.GetGenericTypeDefinition());
1172+
1173+
for (var i = 0; i < allInnerTypes.Length; i++)
1174+
{
1175+
var innerType = allInnerTypes[i];
1176+
1177+
// Skip non-class types (enums, delegates, etc.)
1178+
if (innerType.TypeKind != TypeKind.Class)
1179+
continue;
1180+
1181+
if (!innerType.IsDerivedFrom(baseTypeTuple))
1182+
{
1183+
ReportDiagnostic(
1184+
context,
1185+
DiagnosticsDescriptors.InnerTypeDoesNotDeriveFromUnion,
1186+
innerType.GetTypeIdentifierLocation(context.CancellationToken),
1187+
innerType,
1188+
type);
1189+
}
1190+
}
1191+
}
1192+
11661193
private static void EnumKeyMemberNameMustNotBeItems(
11671194
SymbolAnalysisContext context,
11681195
AttributeData smartEnumAttribute,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ internal static class DiagnosticsDescriptors
6060
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);
6161
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);
6262
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);
63+
public static readonly DiagnosticDescriptor InnerTypeDoesNotDeriveFromUnion = new("TTRESG106", "Inner type should derive from union type", "The inner type '{0}' should derive from union type '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
6364

6465
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);
6566
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -655,17 +655,17 @@ private static void FindDerivedInnerTypes(
655655
}
656656
}
657657

658-
private static bool IsDerivedFrom(
658+
public static bool IsDerivedFrom(
659659
this ITypeSymbol? type,
660660
(INamedTypeSymbol Type, INamedTypeSymbol TypeDef) baseType)
661661
{
662662
while (!type.IsNullOrDotnetBaseType())
663663
{
664664
if (baseType.Type.TypeKind == TypeKind.Interface)
665665
{
666-
foreach (var @interface in type.Interfaces)
666+
for (var i = 0; i < type.Interfaces.Length; i++)
667667
{
668-
if (SymbolEqualityComparer.Default.Equals(@interface, baseType.Type))
668+
if (SymbolEqualityComparer.Default.Equals(type.Interfaces[i], baseType.Type))
669669
return true;
670670
}
671671
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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 TTRESG106_InnerTypeDoesNotDeriveFromUnion
8+
{
9+
private const string _DIAGNOSTIC_ID = "TTRESG106";
10+
11+
public class Inner_type_does_not_derive_from_union
12+
{
13+
[Theory]
14+
[InlineData("class")]
15+
[InlineData("record")]
16+
public async Task Should_trigger_on_non_derived_inner_class(string type)
17+
{
18+
var code = $$"""
19+
20+
using System;
21+
using Thinktecture;
22+
23+
namespace TestNamespace
24+
{
25+
[Union]
26+
public partial {{type}} TestUnion
27+
{
28+
public sealed {{type}} First : TestUnion;
29+
public sealed {{type}} {|#0:Second|};
30+
}
31+
}
32+
""";
33+
34+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Second", "TestUnion");
35+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly], expected);
36+
}
37+
38+
[Fact]
39+
public async Task Should_trigger_on_multiple_non_derived_inner_types()
40+
{
41+
var code = """
42+
43+
using System;
44+
using Thinktecture;
45+
46+
namespace TestNamespace
47+
{
48+
[Union]
49+
public partial class TestUnion
50+
{
51+
public sealed class First : TestUnion;
52+
public sealed class {|#0:Second|};
53+
public sealed class {|#1:Third|};
54+
}
55+
}
56+
""";
57+
58+
var expected0 = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Second", "TestUnion");
59+
var expected1 = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(1).WithArguments("Third", "TestUnion");
60+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly], expected0, expected1);
61+
}
62+
63+
[Fact]
64+
public async Task Should_not_trigger_when_all_inner_types_derive_from_union()
65+
{
66+
var code = """
67+
68+
using System;
69+
using Thinktecture;
70+
71+
namespace TestNamespace
72+
{
73+
[Union]
74+
public partial class TestUnion
75+
{
76+
public sealed class First : TestUnion;
77+
public sealed class Second : TestUnion;
78+
public sealed class Third : TestUnion;
79+
}
80+
}
81+
""";
82+
83+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly]);
84+
}
85+
86+
[Fact]
87+
public async Task Should_not_trigger_when_union_has_no_inner_types()
88+
{
89+
var code = """
90+
91+
using System;
92+
using Thinktecture;
93+
94+
namespace TestNamespace
95+
{
96+
[Union]
97+
public partial class TestUnion
98+
{
99+
}
100+
}
101+
""";
102+
103+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly]);
104+
}
105+
106+
[Fact]
107+
public async Task Should_not_trigger_on_nested_type_that_derives_transitively()
108+
{
109+
var code = """
110+
111+
using System;
112+
using Thinktecture;
113+
114+
namespace TestNamespace
115+
{
116+
[Union]
117+
public partial class TestUnion
118+
{
119+
public class First : TestUnion
120+
{
121+
private First() { }
122+
123+
public sealed class Nested : First;
124+
}
125+
}
126+
}
127+
""";
128+
129+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly]);
130+
}
131+
132+
[Fact]
133+
public async Task Should_not_trigger_on_enum_or_delegate_inner_types()
134+
{
135+
var code = """
136+
137+
using System;
138+
using Thinktecture;
139+
140+
namespace TestNamespace
141+
{
142+
[Union]
143+
public partial class TestUnion
144+
{
145+
public sealed class First : TestUnion;
146+
public enum MyEnum { A, B }
147+
public delegate void MyDelegate();
148+
}
149+
}
150+
""";
151+
152+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly]);
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)