Skip to content

Commit efa6566

Browse files
committed
sg: nullable warnings
1 parent ffd9f3c commit efa6566

4 files changed

Lines changed: 264 additions & 8 deletions

File tree

src/FastCloner.SourceGenerator/CloneCodeGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ private void WriteAbstractTypeDispatcher(string typeName)
312312

313313
if (_context.IsFastClonerAvailable)
314314
{
315-
sb.AppendLine($" return ({typeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source")};");
315+
sb.AppendLine($" return ({typeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source")}!;");
316316
}
317317
else
318318
{
@@ -660,7 +660,7 @@ private void WriteClonerClass()
660660

661661
if (_context.IsFastClonerAvailable)
662662
{
663-
sb.AppendLine($" return ({fallbackCastTypeParam}){CloneGeneratorContext.FastClonerDeepCloneCall("source")};");
663+
sb.AppendLine($" return ({fallbackCastTypeParam}){CloneGeneratorContext.FastClonerDeepCloneCall("source")}!;");
664664
}
665665
else
666666
{

src/FastCloner.SourceGenerator/CollectionHelperGenerator.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ private static string GetItemCloneExpression(CloneGeneratorContext context, Memb
443443
}
444444

445445
return context.IsFastClonerAvailable ?
446-
$"({member.ElementTypeName}?){CloneGeneratorContext.FastClonerDeepCloneCall(itemVar)}" :
446+
$"({member.ElementTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall(itemVar)}!" :
447447
itemVar;
448448
}
449449

@@ -677,7 +677,7 @@ private static (string keyExpr, string valExpr) GetKeyValueExpressions(CloneGene
677677
}
678678
else if (context.IsFastClonerAvailable)
679679
{
680-
keyExpr = $"({member.KeyTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("kvp.Key")}";
680+
keyExpr = $"({member.KeyTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("kvp.Key")}!";
681681
}
682682
}
683683

@@ -712,7 +712,7 @@ private static (string keyExpr, string valExpr) GetKeyValueExpressions(CloneGene
712712
}
713713
else if (context.IsFastClonerAvailable)
714714
{
715-
valExpr = $"({member.ValueTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("kvp.Value")}";
715+
valExpr = $"({member.ValueTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("kvp.Value")}!";
716716
}
717717
}
718718

@@ -797,7 +797,7 @@ private static void WriteArrayCloneMethod(CloneGeneratorContext context, MemberM
797797
}
798798
else if (context.IsFastClonerAvailable)
799799
{
800-
itemExpr = $"({member.ElementTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source[i]")}";
800+
itemExpr = $"({member.ElementTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source[i]")}!";
801801
}
802802
else
803803
{
@@ -903,7 +903,7 @@ private static void WriteMultiDimArrayCloneMethod(CloneGeneratorContext context,
903903
}
904904
else if (context.IsFastClonerAvailable)
905905
{
906-
itemExpr = $"({member.ElementTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall($"source[{indexList}]")}";
906+
itemExpr = $"({member.ElementTypeName}){CloneGeneratorContext.FastClonerDeepCloneCall($"source[{indexList}]")}!";
907907
}
908908
else
909909
{

src/FastCloner.Tests/DiagnosticTests.cs

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,108 @@ public void GenericListClass_Should_Use_FastCloner_For_Items()
186186
Assert.That(clone.Items[0], Is.Not.SameAs(original.Items[0])); // Should be deep cloned
187187
}
188188

189-
// Helper method to run the generator
189+
[Test]
190+
public void GeneratedCode_ForCollectionWithNonClonableElements_ShouldNotProduceNullableWarnings()
191+
{
192+
string source = @"
193+
#nullable enable
194+
using FastCloner.SourceGenerator.Shared;
195+
using System.Collections.Generic;
196+
197+
namespace TestNamespace;
198+
199+
public class NonClonableItem
200+
{
201+
public NonClonableItem(int x) { Value = x; }
202+
public int Value { get; set; }
203+
}
204+
205+
[FastClonerClonable]
206+
public class ClassWithNonClonableCollection
207+
{
208+
public List<NonClonableItem> Items { get; set; } = new();
209+
}
210+
";
211+
(ImmutableArray<Diagnostic> _, ImmutableArray<Diagnostic> compilationDiags) = RunGeneratorAndCompile(source);
212+
213+
List<Diagnostic> nullableWarnings = compilationDiags
214+
.Where(d => d.Id is "CS8604" or "CS8600")
215+
.Where(d => d.Severity == DiagnosticSeverity.Warning)
216+
.ToList();
217+
218+
Assert.That(nullableWarnings, Is.Empty,
219+
"Generated code should not produce nullable warnings (CS8604/CS8600). " +
220+
$"Found: {string.Join("; ", nullableWarnings.Select(d => $"{d.Id}: {d.GetMessage()}"))}");
221+
}
222+
223+
[Test]
224+
public void GeneratedCode_ForArrayWithNonClonableElements_ShouldNotProduceNullableWarnings()
225+
{
226+
string source = @"
227+
#nullable enable
228+
using FastCloner.SourceGenerator.Shared;
229+
230+
namespace TestNamespace;
231+
232+
public class NonClonableItem
233+
{
234+
public NonClonableItem(int x) { Value = x; }
235+
public int Value { get; set; }
236+
}
237+
238+
[FastClonerClonable]
239+
public class ClassWithNonClonableArray
240+
{
241+
public NonClonableItem[] Items { get; set; } = [];
242+
}
243+
";
244+
(ImmutableArray<Diagnostic> _, ImmutableArray<Diagnostic> compilationDiags) = RunGeneratorAndCompile(source);
245+
246+
List<Diagnostic> nullableWarnings = compilationDiags
247+
.Where(d => d.Id is "CS8604" or "CS8600")
248+
.Where(d => d.Severity == DiagnosticSeverity.Warning)
249+
.ToList();
250+
251+
Assert.That(nullableWarnings, Is.Empty,
252+
"Generated code should not produce nullable warnings for arrays. " +
253+
$"Found: {string.Join("; ", nullableWarnings.Select(d => $"{d.Id}: {d.GetMessage()}"))}");
254+
}
255+
256+
[Test]
257+
public void GeneratedCode_ForDictionaryWithNonClonableValues_ShouldNotProduceNullableWarnings()
258+
{
259+
string source = @"
260+
#nullable enable
261+
using FastCloner.SourceGenerator.Shared;
262+
using System.Collections.Generic;
263+
264+
namespace TestNamespace;
265+
266+
public class NonClonableItem
267+
{
268+
public NonClonableItem(int x) { Value = x; }
269+
public int Value { get; set; }
270+
}
271+
272+
[FastClonerClonable]
273+
public class ClassWithNonClonableDictionary
274+
{
275+
public Dictionary<string, NonClonableItem> Items { get; set; } = new();
276+
}
277+
";
278+
(ImmutableArray<Diagnostic> _, ImmutableArray<Diagnostic> compilationDiags) = RunGeneratorAndCompile(source);
279+
280+
List<Diagnostic> nullableWarnings = compilationDiags
281+
.Where(d => d.Id is "CS8604" or "CS8600")
282+
.Where(d => d.Severity == DiagnosticSeverity.Warning)
283+
.ToList();
284+
285+
Assert.That(nullableWarnings, Is.Empty,
286+
"Generated code should not produce nullable warnings for dictionaries. " +
287+
$"Found: {string.Join("; ", nullableWarnings.Select(d => $"{d.Id}: {d.GetMessage()}"))}");
288+
}
289+
290+
// Helper method to run the generator (returns only generator diagnostics)
190291
private static ImmutableArray<Diagnostic> RunGenerator(string source)
191292
{
192293
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);
@@ -214,6 +315,40 @@ private static ImmutableArray<Diagnostic> RunGenerator(string source)
214315
return result.Diagnostics;
215316
}
216317

318+
/// <summary>
319+
/// Runs the generator and compiles the result, returning both generator and compilation diagnostics.
320+
/// Includes FastCloner runtime reference so DeepClone fallback code is generated.
321+
/// </summary>
322+
private static (ImmutableArray<Diagnostic> GeneratorDiags, ImmutableArray<Diagnostic> CompilationDiags) RunGeneratorAndCompile(string source)
323+
{
324+
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);
325+
326+
List<MetadataReference> references =
327+
[
328+
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
329+
MetadataReference.CreateFromFile(typeof(FastClonerClonableAttribute).Assembly.Location),
330+
MetadataReference.CreateFromFile(typeof(FastCloner).Assembly.Location),
331+
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
332+
MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location),
333+
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location)
334+
];
335+
336+
CSharpCompilation compilation = CSharpCompilation.Create(
337+
"TestAssembly",
338+
[syntaxTree],
339+
references,
340+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
341+
nullableContextOptions: NullableContextOptions.Enable));
342+
343+
FastClonerIncrementalGenerator generator = new FastClonerIncrementalGenerator();
344+
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
345+
346+
driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> generatorDiags);
347+
348+
ImmutableArray<Diagnostic> compilationDiags = outputCompilation.GetDiagnostics();
349+
return (generatorDiags, compilationDiags);
350+
}
351+
217352
public class UnclonableClass
218353
{
219354
public int Value { get; set; }

src/FastCloner.Tests/SourceGeneratorEdgeCaseTests.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,4 +1025,125 @@ public void ObservableCollection_WithObjects_GetterOnly_Should_Be_Deep_Cloned()
10251025
}
10261026

10271027
#endregion
1028+
1029+
#region Issue 12: Nullable Warnings in Generated Collection Clone Code (GitHub Issue #30)
1030+
1031+
/// <summary>
1032+
/// Non-clonable type (no parameterless ctor, no [FastClonerClonable]).
1033+
/// The source generator falls back to FastCloner.DeepClone() for this type,
1034+
/// which previously generated a nullable cast (Type?) causing CS8604 warnings.
1035+
/// </summary>
1036+
public class ExternalNonClonableItem
1037+
{
1038+
public ExternalNonClonableItem(int id) { Id = id; }
1039+
public int Id { get; set; }
1040+
public string? Label { get; set; }
1041+
}
1042+
1043+
[FastClonerClonable]
1044+
public class ContainerWithNonClonableList
1045+
{
1046+
public List<ExternalNonClonableItem> Items { get; set; } = new();
1047+
}
1048+
1049+
[FastClonerClonable]
1050+
public class ContainerWithNonClonableArray
1051+
{
1052+
public ExternalNonClonableItem[] Items { get; set; } = [];
1053+
}
1054+
1055+
[FastClonerClonable]
1056+
public class ContainerWithNonClonableDictionary
1057+
{
1058+
public Dictionary<string, ExternalNonClonableItem> Items { get; set; } = new();
1059+
}
1060+
1061+
[Test]
1062+
[SourceGeneratorCompatible]
1063+
public void List_WithNonClonableElements_ShouldDeepCloneViaFastCloner()
1064+
{
1065+
ContainerWithNonClonableList original = new()
1066+
{
1067+
Items =
1068+
[
1069+
new ExternalNonClonableItem(1) { Label = "First" },
1070+
new ExternalNonClonableItem(2) { Label = "Second" },
1071+
new ExternalNonClonableItem(3) { Label = "Third" }
1072+
]
1073+
};
1074+
1075+
ContainerWithNonClonableList clone = original.FastDeepClone();
1076+
1077+
Assert.That(clone, Is.Not.Null);
1078+
Assert.That(clone.Items, Is.Not.SameAs(original.Items));
1079+
Assert.That(clone.Items.Count, Is.EqualTo(3));
1080+
1081+
for (int i = 0; i < original.Items.Count; i++)
1082+
{
1083+
Assert.That(clone.Items[i], Is.Not.SameAs(original.Items[i]));
1084+
Assert.That(clone.Items[i].Id, Is.EqualTo(original.Items[i].Id));
1085+
Assert.That(clone.Items[i].Label, Is.EqualTo(original.Items[i].Label));
1086+
}
1087+
1088+
original.Items[0].Label = "Modified";
1089+
original.Items.Add(new ExternalNonClonableItem(4) { Label = "Fourth" });
1090+
Assert.That(clone.Items[0].Label, Is.EqualTo("First"));
1091+
Assert.That(clone.Items.Count, Is.EqualTo(3));
1092+
}
1093+
1094+
[Test]
1095+
[SourceGeneratorCompatible]
1096+
public void Array_WithNonClonableElements_ShouldDeepCloneViaFastCloner()
1097+
{
1098+
ContainerWithNonClonableArray original = new()
1099+
{
1100+
Items =
1101+
[
1102+
new ExternalNonClonableItem(1) { Label = "A" },
1103+
new ExternalNonClonableItem(2) { Label = "B" }
1104+
]
1105+
};
1106+
1107+
ContainerWithNonClonableArray clone = original.FastDeepClone();
1108+
1109+
Assert.That(clone, Is.Not.Null);
1110+
Assert.That(clone.Items, Is.Not.SameAs(original.Items));
1111+
Assert.That(clone.Items.Length, Is.EqualTo(2));
1112+
Assert.That(clone.Items[0], Is.Not.SameAs(original.Items[0]));
1113+
Assert.That(clone.Items[0].Id, Is.EqualTo(1));
1114+
Assert.That(clone.Items[0].Label, Is.EqualTo("A"));
1115+
Assert.That(clone.Items[1].Id, Is.EqualTo(2));
1116+
1117+
original.Items[0].Label = "Modified";
1118+
Assert.That(clone.Items[0].Label, Is.EqualTo("A"));
1119+
}
1120+
1121+
[Test]
1122+
[SourceGeneratorCompatible]
1123+
public void Dictionary_WithNonClonableValues_ShouldDeepCloneViaFastCloner()
1124+
{
1125+
ContainerWithNonClonableDictionary original = new()
1126+
{
1127+
Items = new Dictionary<string, ExternalNonClonableItem>
1128+
{
1129+
["x"] = new ExternalNonClonableItem(1) { Label = "X" },
1130+
["y"] = new ExternalNonClonableItem(2) { Label = "Y" }
1131+
}
1132+
};
1133+
1134+
ContainerWithNonClonableDictionary clone = original.FastDeepClone();
1135+
1136+
Assert.That(clone, Is.Not.Null);
1137+
Assert.That(clone.Items, Is.Not.SameAs(original.Items));
1138+
Assert.That(clone.Items.Count, Is.EqualTo(2));
1139+
Assert.That(clone.Items["x"], Is.Not.SameAs(original.Items["x"]));
1140+
Assert.That(clone.Items["x"].Id, Is.EqualTo(1));
1141+
Assert.That(clone.Items["x"].Label, Is.EqualTo("X"));
1142+
Assert.That(clone.Items["y"].Id, Is.EqualTo(2));
1143+
1144+
original.Items["x"].Label = "Modified";
1145+
Assert.That(clone.Items["x"].Label, Is.EqualTo("X"));
1146+
}
1147+
1148+
#endregion
10281149
}

0 commit comments

Comments
 (0)