diff --git a/FastCloner.Tests/SpecialCaseTests.cs b/FastCloner.Tests/SpecialCaseTests.cs index 98ed41c..c4c5df1 100644 --- a/FastCloner.Tests/SpecialCaseTests.cs +++ b/FastCloner.Tests/SpecialCaseTests.cs @@ -11,6 +11,8 @@ using System.Globalization; using System.Net; using System.Net.Http.Headers; +using System.Numerics; +using System.Reflection; using System.Runtime.InteropServices; using System.Text; using FastCloner.Code; @@ -233,6 +235,310 @@ public class C1 : CBase public C2 C2 { get; set; } = new C2(); } + [Test] + public void Uri_DeepClone_Test() + { + // Arrange + Uri original = new Uri("https://example.com/path?query=value#fragment"); + + // Act + Uri clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.AbsoluteUri, Is.EqualTo(original.AbsoluteUri)); + Assert.That(clone.Host, Is.EqualTo(original.Host)); + Assert.That(clone.PathAndQuery, Is.EqualTo(original.PathAndQuery)); + Assert.That(clone.Fragment, Is.EqualTo(original.Fragment)); + Assert.That(clone, Is.Not.SameAs(original)); + }); + } + + [Test] + public void Complex_DeepClone_Test() + { + // Arrange + Complex original = new Complex(3.14, 2.718); + + // Act + Complex clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Real, Is.EqualTo(original.Real)); + Assert.That(clone.Imaginary, Is.EqualTo(original.Imaginary)); + Assert.That(clone.Magnitude, Is.EqualTo(original.Magnitude)); + Assert.That(clone.Phase, Is.EqualTo(original.Phase)); + }); + } + + [Test] + public void BigInteger_DeepClone_Test() + { + // Arrange + BigInteger? original = BigInteger.Parse("123456789012345678901234567890"); + + // Act + BigInteger? clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone, Is.EqualTo(original)); + Assert.That(clone.ToString(), Is.EqualTo("123456789012345678901234567890")); + Assert.That((-clone).ToString(), Is.EqualTo("-123456789012345678901234567890")); + }); + } + + [Test] + public void BigInteger_DeepClone_EdgeCases_Test() + { + // Arrange + BigInteger[] originals = + { + BigInteger.Zero, + BigInteger.One, + BigInteger.MinusOne, + BigInteger.Parse("-340282366920938463463374607431768211456"), + BigInteger.Parse("340282366920938463463374607431768211455") + }; + + // Act & Assert + foreach (var original in originals) + { + var clone = original.DeepClone(); + Assert.That(clone, Is.EqualTo(original), $"Failed for value: {original}"); + } + } + + [Test] + public void Version_DeepClone_Test() + { + // Arrange + Version original = new Version(1, 2, 3, 4); + + // Act + Version clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Major, Is.EqualTo(original.Major)); + Assert.That(clone.Minor, Is.EqualTo(original.Minor)); + Assert.That(clone.Build, Is.EqualTo(original.Build)); + Assert.That(clone.Revision, Is.EqualTo(original.Revision)); + Assert.That(clone, Is.Not.SameAs(original)); + }); + } + + class ValTupleTest + { + public int Val { get; set; } + } + + [Test] + public void ValueTuple_Simple_DeepClone_Test() + { + // Arrange + (int X, string Y) original = (X: 42, Y: "test"); + + // Act + (int X, string Y) clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.X, Is.EqualTo(original.X)); + Assert.That(clone.Y, Is.EqualTo(original.Y)); + }); + } + + [Test] + public void ValueTuple_Simple_DeepClone_Test2() + { + ValTupleTest valX = new ValTupleTest { Val = 42 }; + + // Arrange + (ValTupleTest X, ValTupleTest Y) original = (X: valX, Y: new ValTupleTest { Val = 43 }); + + // Act + (ValTupleTest X, ValTupleTest Y) clone = original.DeepClone(); + (ValTupleTest X, ValTupleTest Y) shallow = original.ShallowClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.X.Val, Is.EqualTo(original.X.Val)); + Assert.That(clone.Y.Val, Is.EqualTo(original.Y.Val)); + }); + + valX.Val = 80; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(original.X, clone.X), Is.False); + Assert.That(original.X.Val, Is.EqualTo(80)); + Assert.That(clone.X.Val, Is.EqualTo(42)); + Assert.That(shallow.X.Val, Is.EqualTo(80)); + }); + } + + [Test] + public void ValueTuple_WithReferenceType_DeepClone_Test() + { + // Arrange + List list = new List { 1, 2, 3 }; + (int X, List List) original = (X: 42, List: list); + + // Act + (int X, List List) clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.X, Is.EqualTo(original.X)); + Assert.That(clone.List, Is.EqualTo(original.List)); + Assert.That(clone.List, Is.Not.SameAs(original.List)); + }); + } + + [Test] + public void ValueTuple_Nested_DeepClone_Test() + { + // Arrange + (int A, string B) nested = (A: 1, B: "inner"); + ((int A, string B) X, string Y) original = (X: nested, Y: "outer"); + + // Act + ((int A, string B) X, string Y) clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.X.A, Is.EqualTo(original.X.A)); + Assert.That(clone.X.B, Is.EqualTo(original.X.B)); + Assert.That(clone.Y, Is.EqualTo(original.Y)); + }); + } + + [Test] + public void ValueTuple_WithComplexType_DeepClone_Test() + { + // Arrange + Uri uri = new Uri("https://example.com"); + (int Id, Uri Uri) original = (Id: 1, Uri: uri); + + // Act + (int Id, Uri Uri) clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Id, Is.EqualTo(original.Id)); + Assert.That(clone.Uri.AbsoluteUri, Is.EqualTo(original.Uri.AbsoluteUri)); + Assert.That(clone.Uri, Is.Not.SameAs(original.Uri)); + }); + } + + [Test] + public void ValueTuple_Mutability_Test() + { + // Arrange + List list = [1, 2, 3]; + (int X, List List) original = (X: 42, List: list); + (int X, List List) clone = original.DeepClone(); + + // Act + clone.X = 100; + clone.List.Add(4); + + // Assert + Assert.Multiple(() => + { + Assert.That(original.X, Is.EqualTo(42)); + Assert.That(original.List, Has.Count.EqualTo(3)); + + Assert.That(clone.X, Is.EqualTo(100)); + Assert.That(clone.List, Has.Count.EqualTo(4)); + }); + } + + [Test] + public void Range_DeepClone_Test() + { + // Arrange + Range original = new Range(Index.FromStart(1), Index.FromEnd(5)); + + // Act + Range clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Start.Value, Is.EqualTo(original.Start.Value)); + Assert.That(clone.Start.IsFromEnd, Is.EqualTo(original.Start.IsFromEnd)); + Assert.That(clone.End.Value, Is.EqualTo(original.End.Value)); + Assert.That(clone.End.IsFromEnd, Is.EqualTo(original.End.IsFromEnd)); + Assert.That(clone, Is.EqualTo(original)); + }); + } + + [Test] + public void Index_DeepClone_Test() + { + // Arrange + Index original = new Index(42, fromEnd: true); + + // Act + Index clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Value, Is.EqualTo(original.Value)); + Assert.That(clone.IsFromEnd, Is.EqualTo(original.IsFromEnd)); + Assert.That(clone, Is.EqualTo(original)); + }); + } + + [Test] + public void Index_DeepClone_FromStart_Test() + { + // Arrange + Index original = Index.FromStart(10); + + // Act + Index clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Value, Is.EqualTo(original.Value)); + Assert.That(clone.IsFromEnd, Is.False); + Assert.That(clone, Is.EqualTo(original)); + }); + } + + [Test] + public void Index_DeepClone_FromEnd_Test() + { + // Arrange + Index original = Index.FromEnd(10); + + // Act + Index clone = original.DeepClone(); + + // Assert + Assert.Multiple(() => + { + Assert.That(clone.Value, Is.EqualTo(original.Value)); + Assert.That(clone.IsFromEnd, Is.True); + Assert.That(clone, Is.EqualTo(original)); + }); + } + [Test] public void Test_DeepClone_ClassHierarchy() { diff --git a/FastCloner/Code/FastClonerExprGenerator.cs b/FastCloner/Code/FastClonerExprGenerator.cs index b897d58..cfeaa38 100644 --- a/FastCloner/Code/FastClonerExprGenerator.cs +++ b/FastCloner/Code/FastClonerExprGenerator.cs @@ -118,7 +118,7 @@ private static List GetAllMembers(Type type) return GenerateProcessArrayMethod(type); } - if (type.FullName != null && type.FullName.StartsWith("System.Tuple`")) + if (type.FullName is not null && type.FullName.StartsWith("System.Tuple`")) { // if not safe type it is no guarantee that some type will contain reference to // this tuple. In usual way, we're creating new object, setting reference for it diff --git a/FastCloner/Code/FastClonerSafeTypes.cs b/FastCloner/Code/FastClonerSafeTypes.cs index bbe7e8a..2f96189 100644 --- a/FastCloner/Code/FastClonerSafeTypes.cs +++ b/FastCloner/Code/FastClonerSafeTypes.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Globalization; +using System.Numerics; using System.Reflection; using System.Text; @@ -43,13 +45,16 @@ internal static class FastClonerSafeTypes [typeof(Half)] = true, [typeof(Int128)] = true, [typeof(UInt128)] = true, + [typeof(Complex)] = true, // Others [typeof(DBNull)] = true, [StringComparer.Ordinal.GetType()] = true, [StringComparer.OrdinalIgnoreCase.GetType()] = true, [StringComparer.InvariantCulture.GetType()] = true, - [StringComparer.InvariantCultureIgnoreCase.GetType()] = true + [StringComparer.InvariantCultureIgnoreCase.GetType()] = true, + [typeof(Range)] = true, + [typeof(Index)] = true }; static FastClonerSafeTypes() @@ -65,100 +70,96 @@ static FastClonerSafeTypes() knownTypes.TryAdd(x, true); } } - - private static bool CanReturnSameType(Type type, HashSet? processingTypes) + + private static bool IsSpecialEqualityComparer(string fullName) => fullName switch { - if (knownTypes.TryGetValue(type, out bool isSafe)) - { - return isSafe; - } + _ when fullName.StartsWith("System.Collections.Generic.GenericEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.ObjectEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.EnumEqualityComparer`") => true, + _ when fullName.StartsWith("System.Collections.Generic.NullableEqualityComparer`") => true, + "System.Collections.Generic.ByteEqualityComparer" => true, + _ => false + }; + + private static class TypePrefixes + { + public const string SystemReflection = "System.Reflection."; + public const string SystemRuntimeType = "System.RuntimeType"; + public const string MicrosoftExtensions = "Microsoft.Extensions.DependencyInjection."; + } - if (typeof(Delegate).IsAssignableFrom(type)) + private static readonly Assembly propertyInfoAssembly = typeof(PropertyInfo).Assembly; + private static bool IsReflectionType(Type type) => type.FullName?.StartsWith(TypePrefixes.SystemReflection) is true && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly); + + private static IEnumerable GetAllTypeFields(Type type) + { + Type? currentType = type; + + while (currentType is not null) { - knownTypes.TryAdd(type, false); - return false; + foreach (FieldInfo field in currentType.GetAllFields()) + { + yield return field; + } + + currentType = currentType.BaseType(); } - - // enums are safe - // pointers (e.g. int*) are unsafe, but we cannot do anything with it except blind copy + } + + private static bool IsSafeSystemType(Type type) + { if (type.IsEnum() || type.IsPointer) - { - knownTypes.TryAdd(type, true); return true; - } - + + if (type.IsCOMObject) + return true; + if (type.FullName is null) - { - knownTypes.TryAdd(type, true); return true; - } - if (type.FullName.StartsWith("System.Reflection.") && type.Assembly == typeof(PropertyInfo).Assembly) - { - knownTypes.TryAdd(type, true); + if (IsReflectionType(type)) return true; - } - // these types are serious native resources, it is better not to clone it if (type.IsSubclassOf(typeof(System.Runtime.ConstrainedExecution.CriticalFinalizerObject))) - { - knownTypes.TryAdd(type, true); return true; - } - // Better not to do anything with COM - if (type.IsCOMObject) - { - knownTypes.TryAdd(type, true); + if (type.FullName.StartsWith(TypePrefixes.SystemRuntimeType)) return true; - } - if (type.FullName.StartsWith("System.RuntimeType")) - { - knownTypes.TryAdd(type, true); + if (type.FullName.StartsWith(TypePrefixes.MicrosoftExtensions)) return true; - } - if (type.FullName.StartsWith("System.Reflection.") && Equals(type.GetTypeInfo().Assembly, typeof(PropertyInfo).GetTypeInfo().Assembly)) - { - knownTypes.TryAdd(type, true); + if (type.FullName is "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector") return true; - } - if (type.IsSubclassOfTypeByName("CriticalFinalizerObject")) + return false; + } + + private static bool CanReturnSameType(Type type, HashSet? processingTypes = null) + { + if (knownTypes.TryGetValue(type, out bool isSafe)) { - knownTypes.TryAdd(type, true); - return true; + return isSafe; } - // better not to touch ms dependency injection - if (type.FullName.StartsWith("Microsoft.Extensions.DependencyInjection.")) + if (typeof(Delegate).IsAssignableFrom(type)) { - knownTypes.TryAdd(type, true); - return true; + knownTypes.TryAdd(type, false); + return false; } - if (type.FullName == "Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector") + if (IsSafeSystemType(type)) { knownTypes.TryAdd(type, true); return true; } - // default comparers should not be cloned due possible comparison EqualityComparer.Default == comparer - if (type.FullName.Contains("EqualityComparer")) + if (type.FullName?.Contains("EqualityComparer") == true && IsSpecialEqualityComparer(type.FullName)) { - if (type.FullName.StartsWith("System.Collections.Generic.GenericEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.ObjectEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.EnumEqualityComparer`") - || type.FullName.StartsWith("System.Collections.Generic.NullableEqualityComparer`") - || type.FullName == "System.Collections.Generic.ByteEqualityComparer") - { - knownTypes.TryAdd(type, true); - return true; - } + knownTypes.TryAdd(type, true); + return true; } - // classes are always unsafe (we should copy it fully to count references) if (!type.IsValueType()) { knownTypes.TryAdd(type, false); @@ -167,36 +168,32 @@ private static bool CanReturnSameType(Type type, HashSet? processingTypes) processingTypes ??= []; - // structs cannot have a loops, but check it anyway - processingTypes.Add(type); - - List fi = []; - Type? tp = type; - do + if (!processingTypes.Add(type)) { - fi.AddRange(tp.GetAllFields()); - tp = tp.BaseType(); + return true; } - while (tp != null); - foreach (FieldInfo fieldInfo in fi) + foreach (FieldInfo fieldInfo in GetAllTypeFields(type)) { - // type loop Type fieldType = fieldInfo.FieldType; + if (processingTypes.Contains(fieldType)) + { continue; + } - // not safe and not not safe. we need to go deeper - if (!CanReturnSameType(fieldType, processingTypes)) + if (CanReturnSameType(fieldType, processingTypes)) { - knownTypes.TryAdd(type, false); - return false; + continue; } + + knownTypes.TryAdd(type, false); + return false; } knownTypes.TryAdd(type, true); return true; } - public static bool CanReturnSameObject(Type type) => CanReturnSameType(type, null); + public static bool CanReturnSameObject(Type type) => CanReturnSameType(type); } \ No newline at end of file