diff --git a/src/FastCloner.SourceGenerator/ContextCodeGenerator.cs b/src/FastCloner.SourceGenerator/ContextCodeGenerator.cs
index f01cd1f..76f582b 100644
--- a/src/FastCloner.SourceGenerator/ContextCodeGenerator.cs
+++ b/src/FastCloner.SourceGenerator/ContextCodeGenerator.cs
@@ -209,7 +209,7 @@ private void GenerateDispatcher()
private void GenerateIsHandled()
{
- _sb.AppendLine(" public override bool IsHandled(Type type)");
+ _sb.AppendLine(" public override bool IsHandled(global::System.Type type)");
_sb.AppendLine(" {");
foreach (TypeModel? type in _model.RegisteredTypes)
{
diff --git a/src/FastCloner.Tests/CollectionTests.cs b/src/FastCloner.Tests/CollectionTests.cs
index e46be1b..f2585fe 100644
--- a/src/FastCloner.Tests/CollectionTests.cs
+++ b/src/FastCloner.Tests/CollectionTests.cs
@@ -1,7 +1,41 @@
+using System.Collections;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace FastCloner.Tests;
+
+///
+/// A non-generic class that implements ISet<string> directly.
+/// Exercises the code path where type.GetGenericArguments() returns an empty array
+/// but IsSetType() matches via the interface.
+///
+public class StringSet : ISet
+{
+ private readonly HashSet _inner = new();
+
+ public int Count => _inner.Count;
+ public bool IsReadOnly => false;
+
+ public bool Add(string item) => _inner.Add(item);
+ public void Clear() => _inner.Clear();
+ public bool Contains(string item) => _inner.Contains(item);
+ public void CopyTo(string[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
+ public void ExceptWith(IEnumerable other) => _inner.ExceptWith(other);
+ public IEnumerator GetEnumerator() => _inner.GetEnumerator();
+ public void IntersectWith(IEnumerable other) => _inner.IntersectWith(other);
+ public bool IsProperSubsetOf(IEnumerable other) => _inner.IsProperSubsetOf(other);
+ public bool IsProperSupersetOf(IEnumerable other) => _inner.IsProperSupersetOf(other);
+ public bool IsSubsetOf(IEnumerable other) => _inner.IsSubsetOf(other);
+ public bool IsSupersetOf(IEnumerable other) => _inner.IsSupersetOf(other);
+ public bool Overlaps(IEnumerable other) => _inner.Overlaps(other);
+ public bool Remove(string item) => _inner.Remove(item);
+ public bool SetEquals(IEnumerable other) => _inner.SetEquals(other);
+ public void SymmetricExceptWith(IEnumerable other) => _inner.SymmetricExceptWith(other);
+ public void UnionWith(IEnumerable other) => _inner.UnionWith(other);
+ void ICollection.Add(string item) => _inner.Add(item);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
+
public class CollectionTests
{
[Test]
@@ -176,4 +210,119 @@ public async Task LinkedList_Should_Be_Deep_Cloned_Correctly()
// Original should remain untouched
await Assert.That(original.Count).IsEqualTo(3);
}
+
+ [Test]
+ public async Task NonGenericSetImplementingISet_Should_Be_Deep_Cloned_Correctly()
+ {
+ StringSet original = new StringSet();
+ original.Add("alpha");
+ original.Add("beta");
+ original.Add("gamma");
+
+ StringSet clone = original.DeepClone();
+
+ await Assert.That(clone).IsNotSameReferenceAs(original);
+ await Assert.That(clone.Count).IsEqualTo(3);
+ await Assert.That(clone.Contains("alpha")).IsTrue();
+ await Assert.That(clone.Contains("beta")).IsTrue();
+ await Assert.That(clone.Contains("gamma")).IsTrue();
+
+ // Mutating clone should not affect original
+ clone.Add("delta");
+ await Assert.That(clone.Count).IsEqualTo(4);
+ await Assert.That(original.Count).IsEqualTo(3);
+ }
+
+ [Test]
+ public async Task NonGenericSetInsideObject_Should_Be_Deep_Cloned_Correctly()
+ {
+ ObjectWithNonGenericSet original = new ObjectWithNonGenericSet
+ {
+ Name = "test",
+ Tags = new StringSet()
+ };
+ original.Tags.Add("a");
+ original.Tags.Add("b");
+
+ ObjectWithNonGenericSet clone = original.DeepClone();
+
+ await Assert.That(clone).IsNotSameReferenceAs(original);
+ await Assert.That(clone.Tags).IsNotSameReferenceAs(original.Tags);
+ await Assert.That(clone.Tags.Count).IsEqualTo(2);
+ await Assert.That(clone.Tags.Contains("a")).IsTrue();
+ await Assert.That(clone.Tags.Contains("b")).IsTrue();
+
+ clone.Tags.Add("c");
+ await Assert.That(clone.Tags.Count).IsEqualTo(3);
+ await Assert.That(original.Tags.Count).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task GenericSetWhereElementIsNotFirstTypeArg_Should_Be_Deep_Cloned_Correctly()
+ {
+ // TTag=List (mutable, no stable hash semantics) forces the iterate-and-clone
+ // path rather than the memberwise fast path, exposing the wrong element type.
+ TaggedSet, string> original = new TaggedSet, string>
+ {
+ Tag = new List { 1, 2, 3 }
+ };
+ original.Add("one");
+ original.Add("two");
+ original.Add("three");
+
+ TaggedSet, string> clone = original.DeepClone();
+
+ await Assert.That(clone).IsNotSameReferenceAs(original);
+ await Assert.That(clone.Tag).IsNotSameReferenceAs(original.Tag);
+ await Assert.That(clone.Tag).IsEquivalentTo(original.Tag);
+ await Assert.That(clone.Count).IsEqualTo(3);
+ await Assert.That(clone.Contains("one")).IsTrue();
+ await Assert.That(clone.Contains("two")).IsTrue();
+ await Assert.That(clone.Contains("three")).IsTrue();
+
+ // Mutating clone should not affect original
+ clone.Add("four");
+ clone.Tag = new List { 99 };
+ await Assert.That(clone.Count).IsEqualTo(4);
+ await Assert.That(original.Count).IsEqualTo(3);
+ await Assert.That(original.Tag).IsEquivalentTo(new List { 1, 2, 3 });
+ }
+}
+
+public class ObjectWithNonGenericSet
+{
+ public string Name { get; set; } = "";
+ public StringSet Tags { get; set; } = new();
+}
+
+///
+/// A generic set where the ISet element type is NOT the first generic parameter.
+/// Exercises the case where type.GetGenericArguments()[0] != the ISet<T> element type.
+///
+public class TaggedSet : ISet
+{
+ private readonly HashSet _inner = new();
+ public TTag? Tag { get; set; }
+
+ public int Count => _inner.Count;
+ public bool IsReadOnly => false;
+
+ public bool Add(TElement item) => _inner.Add(item);
+ public void Clear() => _inner.Clear();
+ public bool Contains(TElement item) => _inner.Contains(item);
+ public void CopyTo(TElement[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
+ public void ExceptWith(IEnumerable other) => _inner.ExceptWith(other);
+ public IEnumerator GetEnumerator() => _inner.GetEnumerator();
+ public void IntersectWith(IEnumerable other) => _inner.IntersectWith(other);
+ public bool IsProperSubsetOf(IEnumerable other) => _inner.IsProperSubsetOf(other);
+ public bool IsProperSupersetOf(IEnumerable other) => _inner.IsProperSupersetOf(other);
+ public bool IsSubsetOf(IEnumerable other) => _inner.IsSubsetOf(other);
+ public bool IsSupersetOf(IEnumerable other) => _inner.IsSupersetOf(other);
+ public bool Overlaps(IEnumerable other) => _inner.Overlaps(other);
+ public bool Remove(TElement item) => _inner.Remove(item);
+ public bool SetEquals(IEnumerable other) => _inner.SetEquals(other);
+ public void SymmetricExceptWith(IEnumerable other) => _inner.SymmetricExceptWith(other);
+ public void UnionWith(IEnumerable other) => _inner.UnionWith(other);
+ void ICollection.Add(TElement item) => _inner.Add(item);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
diff --git a/src/FastCloner.Tests/FastCloner.Tests.csproj b/src/FastCloner.Tests/FastCloner.Tests.csproj
index 74b196c..e5696b0 100644
--- a/src/FastCloner.Tests/FastCloner.Tests.csproj
+++ b/src/FastCloner.Tests/FastCloner.Tests.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/FastCloner.Tests/TypeNameConflictTests.cs b/src/FastCloner.Tests/TypeNameConflictTests.cs
new file mode 100644
index 0000000..e351b4c
--- /dev/null
+++ b/src/FastCloner.Tests/TypeNameConflictTests.cs
@@ -0,0 +1,65 @@
+using FastCloner.SourceGenerator.Shared;
+using System.Threading.Tasks;
+
+namespace FastCloner.Tests.TypeNameConflict;
+
+///
+/// Regression test for https://github.com/AnyMindGroup/FastCloner/issues/37
+/// A user-defined "Type" in the same namespace as a FastClonerContext must not
+/// shadow System.Type in generated code. The source generator must emit
+/// global::System.Type instead of unqualified Type.
+/// If the generator emits unqualified "Type", this file will fail to compile.
+///
+public enum Type
+{
+ A,
+ B,
+ C
+}
+
+public class Widget
+{
+ public string Name { get; set; } = "";
+ public Type Kind { get; set; }
+}
+
+[FastClonerRegister(typeof(Widget))]
+public partial class WidgetCloningContext : FastClonerContext { }
+
+public class TypeNameConflictTests
+{
+ [Test]
+ public async Task Context_IsHandled_Should_Work_When_Type_Name_Is_Shadowed()
+ {
+ FastClonerContext ctx = new WidgetCloningContext();
+
+ await Assert.That(ctx.IsHandled(typeof(Widget))).IsTrue();
+ await Assert.That(ctx.IsHandled(typeof(string))).IsFalse();
+ }
+
+ [Test]
+ public async Task Context_TryClone_Should_Work_When_Type_Name_Is_Shadowed()
+ {
+ FastClonerContext ctx = new WidgetCloningContext();
+ Widget original = new Widget { Name = "gear", Kind = Type.B };
+
+ await Assert.That(ctx.TryClone(original, out object? clone)).IsTrue();
+ Widget cloned = (Widget)clone!;
+ await Assert.That(cloned).IsNotSameReferenceAs(original);
+ await Assert.That(cloned.Name).IsEqualTo("gear");
+ await Assert.That(cloned.Kind).IsEqualTo(Type.B);
+ }
+
+ [Test]
+ public async Task Context_Clone_Should_Work_When_Type_Name_Is_Shadowed()
+ {
+ WidgetCloningContext ctx = new WidgetCloningContext();
+ Widget original = new Widget { Name = "spring", Kind = Type.C };
+
+ Widget? cloned = ctx.Clone(original);
+ await Assert.That(cloned).IsNotNull();
+ await Assert.That(cloned).IsNotSameReferenceAs(original);
+ await Assert.That(cloned!.Name).IsEqualTo("spring");
+ await Assert.That(cloned.Kind).IsEqualTo(Type.C);
+ }
+}
diff --git a/src/FastCloner/Code/FastClonerExprGenerator.cs b/src/FastCloner/Code/FastClonerExprGenerator.cs
index 58f68e6..de8beba 100644
--- a/src/FastCloner/Code/FastClonerExprGenerator.cs
+++ b/src/FastCloner/Code/FastClonerExprGenerator.cs
@@ -347,7 +347,10 @@ private static bool ShouldDeepCloneStructReadonlyFields(Type type)
internal static object? GenerateProcessMethod(Type realType, bool asObject) => GenerateProcessMethod(realType, asObject && realType.IsValueType(), new ExpressionPosition(0, 0));
public static bool IsListType(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
- public static bool IsSetType(Type type) => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>));
+ public static bool IsSetType(Type type) => GetSetInterface(type) is not null;
+
+ private static Type? GetSetInterface(Type type) =>
+ type.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>));
public static bool IsConcurrentBagOrQueue(Type type)
{
@@ -921,9 +924,10 @@ private static FastClonerCache.TypeShape BuildTypeShape(Type type)
return GenerateProcessConcurrentBagOrQueueMethod(type, position);
}
- if (IsSetType(type))
+ Type? setInterface = GetSetInterface(type);
+ if (setInterface is not null)
{
- return GenerateProcessSetMethod(type, position);
+ return GenerateProcessSetMethod(type, setInterface, position);
}
if (type.IsArray)
@@ -1853,7 +1857,7 @@ private static object GenerateProcessConcurrentBagOrQueueMethod(Type type, Expre
).Compile();
}
- private static object GenerateProcessSetMethod(Type type, ExpressionPosition position)
+ private static object GenerateProcessSetMethod(Type type, Type setInterface, ExpressionPosition position)
{
if (FastClonerCache.IsTypeIgnored(type))
{
@@ -1862,7 +1866,7 @@ private static object GenerateProcessSetMethod(Type type, ExpressionPosition pos
return Expression.Lambda>(pFrom, pFrom, pState).Compile();
}
- Type elementType = type.GenericArguments()[0];
+ Type elementType = setInterface.GetGenericArguments()[0];
// Fast path check first - avoid creating expressions if we don't need them
bool isImmutable = IsImmutableCollection(type);