Skip to content

Commit 2adb3e4

Browse files
committed
add analyzer for object factory attribute
1 parent dfaed34 commit 2adb3e4

7 files changed

Lines changed: 1047 additions & 6 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
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
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
1215
TTRESG105 | ThinktectureRuntimeExtensionsAnalyzer | Warning | DiagnosticsDescriptors

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
5858
DiagnosticsDescriptors.ObjectFactoryMustImplementStaticValidateMethod,
5959
DiagnosticsDescriptors.ObjectFactoryMustImplementToValueMethod,
6060
DiagnosticsDescriptors.TypeMustNotHaveMoreThanOneAttribute,
61+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithUseWithEntityFramework,
62+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithUseForModelBinding,
63+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithOverlappingSerializationFrameworks,
6164
DiagnosticsDescriptors.TypeMustNotHaveMoveThanOneSmartEnumAttribute,
6265
DiagnosticsDescriptors.TypeMustNotHaveMoveThanOneValueObjectAttribute,
6366
DiagnosticsDescriptors.TypeMustNotHaveMoveThanOneDiscriminatedUnionAttribute,
@@ -529,6 +532,13 @@ private static void ValidateObjectFactories(
529532
ImmutableArray<AttributeData> objectFactoryAttributes,
530533
bool isSmartEnum)
531534
{
535+
if (objectFactoryAttributes.Length > 1)
536+
{
537+
CheckObjectFactoryUseWithEntityFrameworkConflicts(context, type, objectFactoryAttributes);
538+
CheckObjectFactoryUseForModelBindingConflicts(context, type, objectFactoryAttributes);
539+
CheckObjectFactorySerializationFrameworksConflicts(context, type, objectFactoryAttributes);
540+
}
541+
532542
for (var i = 0; i < objectFactoryAttributes.Length; i++)
533543
{
534544
ValidateObjectFactory(context, type, objectFactoryAttributes[i], isSmartEnum);
@@ -885,6 +895,88 @@ private static void ValidateObjectFactory(
885895
}
886896
}
887897

898+
private static void CheckObjectFactoryUseWithEntityFrameworkConflicts(
899+
SymbolAnalysisContext context,
900+
INamedTypeSymbol type,
901+
ImmutableArray<AttributeData> objectFactoryAttributes)
902+
{
903+
var countWithEntityFramework = 0;
904+
905+
for (var i = 0; i < objectFactoryAttributes.Length; i++)
906+
{
907+
if (!objectFactoryAttributes[i].FindUseWithEntityFramework())
908+
continue;
909+
910+
countWithEntityFramework++;
911+
912+
if (countWithEntityFramework <= 1)
913+
continue;
914+
915+
ReportDiagnostic(
916+
context,
917+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithUseWithEntityFramework,
918+
type.GetTypeIdentifierLocation(context.CancellationToken),
919+
type);
920+
921+
return;
922+
}
923+
}
924+
925+
private static void CheckObjectFactoryUseForModelBindingConflicts(
926+
SymbolAnalysisContext context,
927+
INamedTypeSymbol type,
928+
ImmutableArray<AttributeData> objectFactoryAttributes)
929+
{
930+
var countWithModelBinding = 0;
931+
932+
for (var i = 0; i < objectFactoryAttributes.Length; i++)
933+
{
934+
if (!objectFactoryAttributes[i].FindUseForModelBinding())
935+
continue;
936+
937+
countWithModelBinding++;
938+
939+
if (countWithModelBinding <= 1)
940+
continue;
941+
942+
ReportDiagnostic(
943+
context,
944+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithUseForModelBinding,
945+
type.GetTypeIdentifierLocation(context.CancellationToken),
946+
type);
947+
948+
return;
949+
}
950+
}
951+
952+
private static void CheckObjectFactorySerializationFrameworksConflicts(
953+
SymbolAnalysisContext context,
954+
INamedTypeSymbol type,
955+
ImmutableArray<AttributeData> objectFactoryAttributes)
956+
{
957+
var combinedFrameworks = SerializationFrameworks.None;
958+
959+
for (var i = 0; i < objectFactoryAttributes.Length; i++)
960+
{
961+
var frameworks = objectFactoryAttributes[i].FindUseForSerialization();
962+
var overlap = combinedFrameworks & frameworks;
963+
964+
if (overlap != SerializationFrameworks.None)
965+
{
966+
ReportDiagnostic(
967+
context,
968+
DiagnosticsDescriptors.MultipleObjectFactoryAttributesWithOverlappingSerializationFrameworks,
969+
type.GetTypeIdentifierLocation(context.CancellationToken),
970+
BuildTypeName(type),
971+
overlap.ToString());
972+
973+
return;
974+
}
975+
976+
combinedFrameworks |= frameworks;
977+
}
978+
}
979+
888980
private static void ValidateSmartEnum(
889981
SymbolAnalysisContext context,
890982
INamedTypeSymbol enumType,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ internal static class DiagnosticsDescriptors
4646
public static readonly DiagnosticDescriptor TypeMustNotHaveMoveThanOneValueObjectAttribute = new("TTRESG065", $"Type must not have more than one {Constants.Attributes.ValueObject.KEYED_NAME}/{Constants.Attributes.ValueObject.COMPLEX_NAME}", $"The type '{{0}}' must not have more than one {Constants.Attributes.ValueObject.KEYED_NAME}/{Constants.Attributes.ValueObject.COMPLEX_NAME}", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
4747
public static readonly DiagnosticDescriptor TypeMustNotHaveMoveThanOneDiscriminatedUnionAttribute = new("TTRESG066", $"Type must not have more than one {Constants.Attributes.Union.NAME}/{Constants.Attributes.Union.NAME_AD_HOC}", $"The type '{{0}}' must not have more than one {Constants.Attributes.Union.NAME}/{Constants.Attributes.Union.NAME_AD_HOC}", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
4848
public static readonly DiagnosticDescriptor AdHocUnionMustHaveAtLeastTwoMemberTypes = new("TTRESG067", "Ad hoc union must define at least two member types", "Ad hoc union '{0}' must have at least two member types", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
49+
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithUseWithEntityFramework = new("TTRESG068", "Multiple ObjectFactoryAttribute instances cannot have UseWithEntityFramework = true", "Type '{0}' has multiple ObjectFactoryAttribute instances with 'UseWithEntityFramework = true'. Only one ObjectFactoryAttribute can have 'UseWithEntityFramework = true'.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
50+
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithUseForModelBinding = new("TTRESG069", "Multiple ObjectFactoryAttribute instances cannot have UseForModelBinding = true", "Type '{0}' has multiple ObjectFactoryAttribute instances with 'UseForModelBinding = true'. Only one ObjectFactoryAttribute can have 'UseForModelBinding = true'.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
51+
public static readonly DiagnosticDescriptor MultipleObjectFactoryAttributesWithOverlappingSerializationFrameworks = new("TTRESG070", "Multiple ObjectFactoryAttribute instances cannot specify overlapping serialization frameworks", "Type '{0}' has multiple ObjectFactoryAttribute instances with overlapping serialization frameworks '{1}'. Each serialization framework can only be used by one ObjectFactoryAttribute.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
4952

5053
public static readonly DiagnosticDescriptor ErrorDuringModulesAnalysis = new("TTRESG097", "Error during analysis of referenced modules", "Error during analysis of referenced modules: '{0}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true);
5154
public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);

test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG062_ObjectFactoryMustImplementToValueMethod.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -709,8 +709,8 @@ public async Task Should_trigger_for_one_of_two_object_factory_when_both_need_on
709709
710710
namespace TestNamespace
711711
{
712-
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
713-
[ObjectFactory<int>(UseForSerialization = SerializationFrameworks.All)]
712+
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.SystemTextJson)]
713+
[ObjectFactory<int>(UseForSerialization = SerializationFrameworks.NewtonsoftJson)]
714714
public partial class {|#0:TestClass|} : IConvertible<string>
715715
{
716716
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
@@ -740,8 +740,8 @@ string IConvertible<string>.ToValue()
740740
741741
namespace TestNamespace
742742
{
743-
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
744-
[ObjectFactory<int>(UseForSerialization = SerializationFrameworks.All)]
743+
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.SystemTextJson)]
744+
[ObjectFactory<int>(UseForSerialization = SerializationFrameworks.NewtonsoftJson)]
745745
public partial class {|#0:TestClass|} : IConvertible<string>
746746
{
747747
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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 once InconsistentNaming
7+
public class TTRESG068_ObjectFactoryUseWithEntityFrameworkConflict
8+
{
9+
private const string _DIAGNOSTIC_ID = "TTRESG068";
10+
11+
[Fact]
12+
public async Task Should_trigger_when_two_attributes_have_UseWithEntityFramework_true()
13+
{
14+
var code = """
15+
#nullable enable
16+
using System;
17+
using Thinktecture;
18+
19+
namespace TestNamespace
20+
{
21+
[ObjectFactory<int>(UseWithEntityFramework = true)]
22+
[ObjectFactory<string>(UseWithEntityFramework = true)]
23+
public partial class {|#0:TestClass|}
24+
{
25+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
26+
{
27+
item = null;
28+
return null;
29+
}
30+
31+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
32+
{
33+
item = null;
34+
return null;
35+
}
36+
37+
public int ToValue() => 0;
38+
public string ToValue(int _) => "";
39+
}
40+
}
41+
""";
42+
43+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestClass");
44+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly], expected,
45+
Verifier.Diagnostic("TTRESG062").WithSpan(9, 25, 9, 34).WithArguments("TestClass", "string"));
46+
}
47+
48+
[Fact]
49+
public async Task Should_trigger_when_three_attributes_have_UseWithEntityFramework_true()
50+
{
51+
var code = """
52+
#nullable enable
53+
using System;
54+
using Thinktecture;
55+
56+
namespace TestNamespace
57+
{
58+
[ObjectFactory<int>(UseWithEntityFramework = true)]
59+
[ObjectFactory<string>(UseWithEntityFramework = true)]
60+
[ObjectFactory<Guid>(UseWithEntityFramework = true)]
61+
public partial class {|#0:TestClass|}
62+
{
63+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
64+
{
65+
item = null;
66+
return null;
67+
}
68+
69+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
70+
{
71+
item = null;
72+
return null;
73+
}
74+
75+
public static ValidationError? Validate(Guid value, IFormatProvider? provider, out TestClass? item)
76+
{
77+
item = null;
78+
return null;
79+
}
80+
81+
public int ToValue() => 0;
82+
public string ToValue(int _) => "";
83+
public Guid ToValue(string _) => Guid.Empty;
84+
}
85+
}
86+
""";
87+
88+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestClass");
89+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly], expected,
90+
Verifier.Diagnostic("TTRESG062").WithSpan(10, 25, 10, 34).WithArguments("TestClass", "string"),
91+
Verifier.Diagnostic("TTRESG062").WithSpan(10, 25, 10, 34).WithArguments("TestClass", "Guid"));
92+
}
93+
94+
[Fact]
95+
public async Task Should_not_trigger_when_only_one_attribute_has_UseWithEntityFramework_true()
96+
{
97+
var code = """
98+
#nullable enable
99+
using System;
100+
using Thinktecture;
101+
102+
namespace TestNamespace
103+
{
104+
[ObjectFactory<int>(UseWithEntityFramework = true)]
105+
[ObjectFactory<string>(UseWithEntityFramework = false)]
106+
public partial class TestClass
107+
{
108+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
109+
{
110+
item = null;
111+
return null;
112+
}
113+
114+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
115+
{
116+
item = null;
117+
return null;
118+
}
119+
120+
public int ToValue() => 0;
121+
}
122+
}
123+
""";
124+
125+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly]);
126+
}
127+
128+
[Fact]
129+
public async Task Should_not_trigger_when_no_attributes_have_UseWithEntityFramework_true()
130+
{
131+
var code = """
132+
#nullable enable
133+
using System;
134+
using Thinktecture;
135+
136+
namespace TestNamespace
137+
{
138+
[ObjectFactory<int>(UseWithEntityFramework = false)]
139+
[ObjectFactory<string>(UseWithEntityFramework = false)]
140+
public partial class TestClass
141+
{
142+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
143+
{
144+
item = null;
145+
return null;
146+
}
147+
148+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
149+
{
150+
item = null;
151+
return null;
152+
}
153+
}
154+
}
155+
""";
156+
157+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly]);
158+
}
159+
160+
[Fact]
161+
public async Task Should_not_trigger_for_single_attribute_with_UseWithEntityFramework_true()
162+
{
163+
var code = """
164+
#nullable enable
165+
using System;
166+
using Thinktecture;
167+
168+
namespace TestNamespace
169+
{
170+
[ObjectFactory<int>(UseWithEntityFramework = true)]
171+
public partial class TestClass
172+
{
173+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
174+
{
175+
item = null;
176+
return null;
177+
}
178+
179+
public int ToValue() => 0;
180+
}
181+
}
182+
""";
183+
184+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly]);
185+
}
186+
187+
[Fact]
188+
public async Task Should_not_trigger_when_UseWithEntityFramework_not_explicitly_set()
189+
{
190+
var code = """
191+
#nullable enable
192+
using System;
193+
using Thinktecture;
194+
195+
namespace TestNamespace
196+
{
197+
[ObjectFactory<int>]
198+
[ObjectFactory<string>]
199+
public partial class TestClass
200+
{
201+
public static ValidationError? Validate(int value, IFormatProvider? provider, out TestClass? item)
202+
{
203+
item = null;
204+
return null;
205+
}
206+
207+
public static ValidationError? Validate(string? value, IFormatProvider? provider, out TestClass? item)
208+
{
209+
item = null;
210+
return null;
211+
}
212+
}
213+
}
214+
""";
215+
216+
await Verifier.VerifyAnalyzerAsync(code, [typeof(ObjectFactoryAttribute<>).Assembly]);
217+
}
218+
}

0 commit comments

Comments
 (0)