diff --git a/docs/guids.md b/docs/guids.md index 526743de7..16a66b1e8 100644 --- a/docs/guids.md +++ b/docs/guids.md @@ -23,7 +23,7 @@ var target = new GuidTarget await Verify(target); ``` -snippet source | anchor +snippet source | anchor Results in the following: diff --git a/docs/members-throw.md b/docs/members-throw.md index 1324480da..847c1f8d6 100644 --- a/docs/members-throw.md +++ b/docs/members-throw.md @@ -35,7 +35,7 @@ public Task CustomExceptionPropFluent() .IgnoreMembersThatThrow(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -45,7 +45,7 @@ Or globally: ```cs VerifierSettings.IgnoreMembersThatThrow(); ``` -snippet source | anchor +snippet source | anchor Result: @@ -82,7 +82,7 @@ public Task ExceptionMessagePropFluent() .IgnoreMembersThatThrow(_ => _.Message == "Ignore"); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -92,7 +92,7 @@ Or globally: ```cs VerifierSettings.IgnoreMembersThatThrow(_ => _.Message == "Ignore"); ``` -snippet source | anchor +snippet source | anchor Result: diff --git a/docs/obsolete-members.md b/docs/obsolete-members.md index c42a33f81..bd3f9d2bc 100644 --- a/docs/obsolete-members.md +++ b/docs/obsolete-members.md @@ -31,7 +31,7 @@ public Task WithObsoleteProp() return Verify(target); } ``` -snippet source | anchor +snippet source | anchor Result: @@ -79,7 +79,7 @@ public Task WithObsoletePropIncludedFluent() .IncludeObsoletes(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -89,7 +89,7 @@ Or globally: ```cs VerifierSettings.IncludeObsoletes(); ``` -snippet source | anchor +snippet source | anchor Result: diff --git a/docs/serializer-settings.md b/docs/serializer-settings.md index ecf1e3a37..b9134a101 100644 --- a/docs/serializer-settings.md +++ b/docs/serializer-settings.md @@ -132,7 +132,7 @@ var settings = new JsonSerializerSettings DefaultValueHandling = DefaultValueHandling.Ignore }; ``` -snippet source | anchor +snippet source | anchor @@ -495,7 +495,7 @@ public Task ScopedSerializerFluent() .AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All); } ``` -snippet source | anchor +snippet source | anchor Result: @@ -623,7 +623,7 @@ public Task IgnoreTypeFluent() .IgnoreMembersWithType(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -633,7 +633,7 @@ Or globally: ```cs VerifierSettings.IgnoreMembersWithType(); ``` -snippet source | anchor +snippet source | anchor Result: @@ -770,7 +770,7 @@ public Task ScrubTypeFluent() .ScrubMembersWithType(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -780,7 +780,7 @@ Or globally: ```cs VerifierSettings.ScrubMembersWithType(); ``` -snippet source | anchor +snippet source | anchor Result: @@ -859,7 +859,7 @@ public Task AddIgnoreInstanceFluent() .IgnoreInstance(_ => _.Property == "Ignore"); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -869,7 +869,7 @@ Or globally: ```cs VerifierSettings.IgnoreInstance(_ => _.Property == "Ignore"); ``` -snippet source | anchor +snippet source | anchor Result: @@ -931,7 +931,7 @@ public Task AddScrubInstanceFluent() .ScrubInstance(_ => _.Property == "Ignore"); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -941,7 +941,7 @@ Or globally: ```cs VerifierSettings.ScrubInstance(_ => _.Property == "Ignore"); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1004,7 +1004,7 @@ public Task IgnoreMemberByExpressionFluent() _ => _.PropertyThatThrows); } ``` -snippet source | anchor +snippet source | anchor Or globally @@ -1019,7 +1019,7 @@ VerifierSettings.IgnoreMembers( _ => _.GetOnlyProperty, _ => _.PropertyThatThrows); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1079,7 +1079,7 @@ public Task ScrubMemberByExpressionFluent() _ => _.PropertyThatThrows); } ``` -snippet source | anchor +snippet source | anchor Or globally @@ -1094,7 +1094,7 @@ VerifierSettings.ScrubMembers( _ => _.GetOnlyProperty, _ => _.PropertyThatThrows); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1173,7 +1173,7 @@ public Task IgnoreMemberByNameFluent() .IgnoreMember(_ => _.PropertyThatThrows); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -1193,7 +1193,7 @@ VerifierSettings.IgnoreMember("Field"); // For a specific type with expression VerifierSettings.IgnoreMember(_ => _.PropertyThatThrows); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1268,7 +1268,7 @@ public Task ScrubMemberByNameFluent() .ScrubMember(_ => _.PropertyThatThrows); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -1288,7 +1288,7 @@ VerifierSettings.ScrubMember("Field"); // For a specific type with expression VerifierSettings.ScrubMember(_ => _.PropertyThatThrows); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1381,7 +1381,7 @@ public Task IgnoreDictionaryByPredicate() return Verify(target, settings); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -1393,7 +1393,7 @@ VerifierSettings.IgnoreMembers( _=>_.DeclaringType == typeof(TargetClass) && _.Name == "Proprty"); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1482,7 +1482,7 @@ public Task ScrubDictionaryByPredicate() return Verify(target, settings); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -1494,7 +1494,7 @@ VerifierSettings.ScrubMembers( _=>_.DeclaringType == typeof(TargetClass) && _.Name == "Proprty"); ``` -snippet source | anchor +snippet source | anchor Result: @@ -1548,7 +1548,7 @@ public Task MemberConverterByExpression() return Verify(input); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMemberGeneric.verified.txt b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMemberGeneric.verified.txt new file mode 100644 index 000000000..801eed9fe --- /dev/null +++ b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMemberGeneric.verified.txt @@ -0,0 +1,4 @@ +{ + Value: {Scrubbed}, + Other: keep +} \ No newline at end of file diff --git a/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersGeneric.verified.txt b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersGeneric.verified.txt new file mode 100644 index 000000000..801eed9fe --- /dev/null +++ b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersGeneric.verified.txt @@ -0,0 +1,4 @@ +{ + Value: {Scrubbed}, + Other: keep +} \ No newline at end of file diff --git a/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersType.verified.txt b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersType.verified.txt new file mode 100644 index 000000000..801eed9fe --- /dev/null +++ b/src/StaticSettingsTests/ScrubMemberGlobalTests.ScrubMembersType.verified.txt @@ -0,0 +1,4 @@ +{ + Value: {Scrubbed}, + Other: keep +} \ No newline at end of file diff --git a/src/StaticSettingsTests/ScrubMemberGlobalTests.cs b/src/StaticSettingsTests/ScrubMemberGlobalTests.cs new file mode 100644 index 000000000..3090f6cc7 --- /dev/null +++ b/src/StaticSettingsTests/ScrubMemberGlobalTests.cs @@ -0,0 +1,33 @@ +public class ScrubMemberGlobalTests : + BaseTest +{ + // The static ScrubMember(s) overloads previously delegated to Ignore*, so a + // member was silently removed instead of rendered as "Scrubbed". + + [Fact] + public Task ScrubMemberGeneric() + { + VerifierSettings.ScrubMember("Value"); + return Verify(new Target()); + } + + [Fact] + public Task ScrubMembersGeneric() + { + VerifierSettings.ScrubMembers("Value"); + return Verify(new Target()); + } + + [Fact] + public Task ScrubMembersType() + { + VerifierSettings.ScrubMembers(typeof(Target), "Value"); + return Verify(new Target()); + } + + public class Target + { + public string Value { get; set; } = "secret"; + public string Other { get; set; } = "keep"; + } +} diff --git a/src/Verify.Tests/Serialization/SerializationSettingsCopyTests.cs b/src/Verify.Tests/Serialization/SerializationSettingsCopyTests.cs new file mode 100644 index 000000000..0838633e0 --- /dev/null +++ b/src/Verify.Tests/Serialization/SerializationSettingsCopyTests.cs @@ -0,0 +1,15 @@ +public class SerializationSettingsCopyTests +{ + [Fact] + public void PreservesOrderDictionaries() + { + var settings = new SerializationSettings + { + OrderDictionaries = false + }; + + var copy = new SerializationSettings(settings); + + Assert.False(copy.OrderDictionaries); + } +} diff --git a/src/Verify.Tests/Serialization/SerializationTests.MultilineStringInArray.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.MultilineStringInArray.verified.txt new file mode 100644 index 000000000..44fc82a65 --- /dev/null +++ b/src/Verify.Tests/Serialization/SerializationTests.MultilineStringInArray.verified.txt @@ -0,0 +1,5 @@ +[ + single, + multi +line +] \ No newline at end of file diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsUlong.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsUlong.verified.txt new file mode 100644 index 000000000..f74382af3 --- /dev/null +++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsUlong.verified.txt @@ -0,0 +1,3 @@ +{ + Id: Id_1 +} \ No newline at end of file diff --git a/src/Verify.Tests/Serialization/SerializationTests.cs b/src/Verify.Tests/Serialization/SerializationTests.cs index c8c5439ad..9a118a855 100644 --- a/src/Verify.Tests/Serialization/SerializationTests.cs +++ b/src/Verify.Tests/Serialization/SerializationTests.cs @@ -2227,6 +2227,53 @@ public async Task PartialNamedTuple() public Task NamedTupleWithNull() => VerifyTuple(() => MethodWithNamedTupleWithNull()); + [Fact] + public void NineNamedTupleElements() + { + // 8+ element tuples nest a Rest ValueTuple, which used to throw the + // "all parts are named" error even when every part is named. + var dictionary = TupleConverter.ExpressionToDictionary(() => MethodWithNineNamedTuple()); + Assert.Equal(9, dictionary.Count); + Assert.Equal(1, (int) dictionary["a"]!); + Assert.Equal(8, (int) dictionary["h"]!); + Assert.Equal(9, (int) dictionary["i"]!); + } + + static (int a, int b, int c, int d, int e, int f, int g, int h, int i) MethodWithNineNamedTuple() => + (1, 2, 3, 4, 5, 6, 7, 8, 9); + + [Fact] + public void NestedNamedTupleThrows() + { + var exception = Assert.Throws( + () => TupleConverter.ExpressionToDictionary(() => MethodWithNestedTuple())); + Assert.Contains("Nested", exception.Message); + } + + static (int a, (int c, int d) b) MethodWithNestedTuple() => + (1, (2, 3)); + + [Fact] + public Task MultilineStringInArray() + { + // The array delimiter must be preserved for a multiline string element. + string[] target = ["single", "multi\nline"]; + return Verify(target); + } + + [Fact] + public Task ScrubNumericIdsUlong() + { + // ulong.MaxValue is outside Int64 range; the previous Convert.ToInt64 + // threw an OverflowException mid-serialization. + var target = new + { + Id = ulong.MaxValue + }; + return Verify(target) + .ScrubNumericIds(); + } + [Fact] public Task Claim() => Verify(new Claim("TheType", "TheValue")); diff --git a/src/Verify/Counter_NumericId.cs b/src/Verify/Counter_NumericId.cs index 9ce59109c..598764c2c 100644 --- a/src/Verify/Counter_NumericId.cs +++ b/src/Verify/Counter_NumericId.cs @@ -2,18 +2,35 @@ namespace VerifyTests; public partial class Counter { - Dictionary> numericIdCache = []; + Dictionary> numericIdCache = []; Dictionary numericIdCounters = []; public int NextNumericId(string entityName, long input) => - NextNumericIdValue(entityName, input) + NextNumericIdValue(entityName, input.ToString(CultureInfo.InvariantCulture)) .intValue; public string NextNumericIdString(string entityName, long input) => - NextNumericIdValue(entityName, input) + NextNumericIdValue(entityName, input.ToString(CultureInfo.InvariantCulture)) .stringValue; - (int intValue, string stringValue) NextNumericIdValue(string entityName, long input) + // Keys on the invariant string form of the value rather than converting to + // long, so ulong/decimal/double ids outside Int64 range (and NaN) don't throw, + // and distinct fractional values are not collapsed by rounding. + internal string NextNumericIdString(string entityName, object value) => + NextNumericIdValue(entityName, ToKey(value)) + .stringValue; + + static string ToKey(object value) + { + if (value is IFormattable formattable) + { + return formattable.ToString(null, CultureInfo.InvariantCulture); + } + + return value.ToString()!; + } + + (int intValue, string stringValue) NextNumericIdValue(string entityName, string input) { if (!numericIdCache.TryGetValue(entityName, out var cache)) { diff --git a/src/Verify/Serialization/NumericIdScrubProvider.cs b/src/Verify/Serialization/NumericIdScrubProvider.cs index acf2e59ee..21edaf067 100644 --- a/src/Verify/Serialization/NumericIdScrubProvider.cs +++ b/src/Verify/Serialization/NumericIdScrubProvider.cs @@ -12,7 +12,6 @@ public object GetValue(object target) return $"{entityName}_null"; } - var longValue = Convert.ToInt64(value); - return Counter.Current.NextNumericIdString(entityName, longValue); + return Counter.Current.NextNumericIdString(entityName, value); } } diff --git a/src/Verify/Serialization/ReflectionHelpers.cs b/src/Verify/Serialization/ReflectionHelpers.cs index c8f8a79bd..d0d17a879 100644 --- a/src/Verify/Serialization/ReflectionHelpers.cs +++ b/src/Verify/Serialization/ReflectionHelpers.cs @@ -107,7 +107,7 @@ public static bool TryGetCollectionOrDictionary(this object target, [NotNullWhen } if (type.FullName != null && - type.FullName.StartsWith("System.Linq.ILookup")) + type.FullName.StartsWith("System.Linq.ILookup", StringComparison.Ordinal)) { enumerable = enumerableTarget; isEmpty = IsEmpty(enumerable); @@ -117,7 +117,7 @@ public static bool TryGetCollectionOrDictionary(this object target, [NotNullWhen var interfaces = type.GetInterfaces(); if (interfaces.Any(_ => _.FullName != null && - _.FullName.StartsWith("System.Linq.ILookup"))) + _.FullName.StartsWith("System.Linq.ILookup", StringComparison.Ordinal))) { enumerable = enumerableTarget; isEmpty = IsEmpty(enumerable); @@ -145,7 +145,7 @@ static bool IsEmpty(IEnumerable enumerable) } static bool IsEnumerableEmpty(this Type type) => - type.FullName?.StartsWith("System.Linq.EmptyPartition") == true; + type.FullName?.StartsWith("System.Linq.EmptyPartition", StringComparison.Ordinal) == true; public static bool IsGeneric(this Type type, params Type[] generics) { @@ -175,12 +175,20 @@ static bool ImplementsGenericCollection(this Type type) => typeof(ICollection<>), typeof(IReadOnlyCollection<>)); + static ConcurrentDictionary isDefaultOrEmptyGetters = new(); + static bool IsDefaultOrEmptyImmutableArray(object target) { - var targetType = target.GetType(); + var getter = isDefaultOrEmptyGetters.GetOrAdd(target.GetType(), BuildIsDefaultOrEmptyGetter); + return getter != null && + (bool)getter.Invoke(target, null)!; + } + + static MethodInfo? BuildIsDefaultOrEmptyGetter(Type targetType) + { if (!targetType.IsGeneric(typeof(ImmutableArray<>))) { - return false; + return null; } var isDefaultOrEmptyProperty = targetType.GetProperty( @@ -188,7 +196,6 @@ static bool IsDefaultOrEmptyImmutableArray(object target) bindingAttr: BindingFlags.Public | BindingFlags.Instance) ?? throw new NotSupportedException("There is no IsDefaultOrEmpty property on ImmutableArray."); - return (bool)isDefaultOrEmptyProperty.GetMethod!.Invoke(target, null)!; + return isDefaultOrEmptyProperty.GetMethod!; } - } diff --git a/src/Verify/Serialization/Scrubbers/ApplyScrubbers.cs b/src/Verify/Serialization/Scrubbers/ApplyScrubbers.cs index 7084fdbd1..4dba9a4e0 100644 --- a/src/Verify/Serialization/Scrubbers/ApplyScrubbers.cs +++ b/src/Verify/Serialization/Scrubbers/ApplyScrubbers.cs @@ -47,6 +47,24 @@ public static void ApplyForExtension(string extension, StringBuilder target, Ver public static string ApplyForPropertyValue(CharSpan value, VerifySettings settings, Counter counter) { + // Fast path: when nothing can change the value, skip the StringBuilder + // round-trip and the directory-replacement scan (this is the hottest loop + // in the library — one call per serialized string value). + if (!value.Contains('\r')) + { + if (!settings.ScrubbersEnabled) + { + return value.ToString(); + } + + if (settings.InstanceScrubbers == null && + VerifierSettings.GlobalScrubbers.Count == 0 && + value.Length < DirectoryReplacements.ShortestFindLength) + { + return value.ToString(); + } + } + var builder = new StringBuilder(value.Length); builder.Append(value); ApplyForPropertyValue(settings, counter, builder); diff --git a/src/Verify/Serialization/Scrubbers/DirectoryReplacements.cs b/src/Verify/Serialization/Scrubbers/DirectoryReplacements.cs index 7987b631e..ca22140ae 100644 --- a/src/Verify/Serialization/Scrubbers/DirectoryReplacements.cs +++ b/src/Verify/Serialization/Scrubbers/DirectoryReplacements.cs @@ -3,6 +3,11 @@ static partial class DirectoryReplacements { static List items = []; + static int shortestFindLength = int.MaxValue; + + // Length of the shortest Find, so callers can cheaply skip values that are + // too short to contain any replacement. int.MaxValue when there are none. + public static int ShortestFindLength => shortestFindLength; public static void Replace(StringBuilder builder) => Replace(builder, items); @@ -46,6 +51,7 @@ public static void UseAssembly(string? solutionDir, string projectDir) items = values .OrderByDescending(_ => _.Find.Length) .ToList(); + shortestFindLength = items.Count == 0 ? int.MaxValue : items[^1].Find.Length; } static void AddProjectAndSolutionReplacements(string? solutionDir, string projectDir, List replacements) diff --git a/src/Verify/Serialization/Scrubbers/DirectoryReplacements_StringBuilder.cs b/src/Verify/Serialization/Scrubbers/DirectoryReplacements_StringBuilder.cs index 752e23dc9..3484f62db 100644 --- a/src/Verify/Serialization/Scrubbers/DirectoryReplacements_StringBuilder.cs +++ b/src/Verify/Serialization/Scrubbers/DirectoryReplacements_StringBuilder.cs @@ -56,6 +56,13 @@ static List FindMatches(StringBuilder builder, List pairs) return []; } + // pairs are ordered by length desc, so the last is the shortest. If the + // builder is shorter than that, no pair can match. + if (builder.Length < pairs[^1].Find.Length) + { + return []; + } + var matches = new List(); // Track matched positions var matchedRanges = new List<(int Start, int End)>(); diff --git a/src/Verify/Serialization/SerializationSettings.cs b/src/Verify/Serialization/SerializationSettings.cs index 8acd0abeb..86f9185f6 100644 --- a/src/Verify/Serialization/SerializationSettings.cs +++ b/src/Verify/Serialization/SerializationSettings.cs @@ -70,6 +70,7 @@ public SerializationSettings(SerializationSettings settings) ScrubGuids = settings.ScrubGuids; ScrubNumericIds = settings.ScrubNumericIds; + OrderDictionaries = settings.OrderDictionaries; includeObsoletes = settings.includeObsoletes; ignoredMemberPredicatesByString = settings.ignoredMemberPredicatesByString?.Clone(); ignoredMemberPredicatesByMember = settings.ignoredMemberPredicatesByMember?.Clone(); diff --git a/src/Verify/Serialization/TupleConverter.cs b/src/Verify/Serialization/TupleConverter.cs index 03d77636e..03d1422b3 100644 --- a/src/Verify/Serialization/TupleConverter.cs +++ b/src/Verify/Serialization/TupleConverter.cs @@ -7,11 +7,23 @@ static class TupleConverter var methodCall = (MethodCallExpression) unary.Operand; var attribute = ReadTupleElementNamesAttribute(methodCall.Method); var transforms = attribute.TransformNames; - var dictionary = new Dictionary(transforms.Count); var result = expression .Compile() .Invoke(); - for (var index = 0; index < transforms.Count; index++) + + // For 8+ element tuples TransformNames carries trailing nulls for the + // compiler-synthesized Rest slots; the leaf names come first, aligned + // with the flattened ITuple indexer. A nested named tuple instead + // flattens to more names than the tuple has elements, which cannot be + // mapped positionally. + var length = result.Length; + if (transforms.Count(_ => _ is not null) > length) + { + throw new("Nested named tuples cannot be used. Use a flat tuple where every part is named."); + } + + var dictionary = new Dictionary(length); + for (var index = 0; index < length; index++) { var transform = transforms[index]; if (transform is null) diff --git a/src/Verify/Serialization/VerifierSettings_MemberConverter.cs b/src/Verify/Serialization/VerifierSettings_MemberConverter.cs index a500df149..2f867b32a 100644 --- a/src/Verify/Serialization/VerifierSettings_MemberConverter.cs +++ b/src/Verify/Serialization/VerifierSettings_MemberConverter.cs @@ -14,9 +14,9 @@ public static partial class VerifierSettings { foreach (var pair in membersConverters) { - if (pair.Key.IsAssignableFrom(declaringType)) + if (pair.Key.IsAssignableFrom(declaringType) && + pair.Value.TryGetValue(name, out var membersConverter)) { - pair.Value.TryGetValue(name, out var membersConverter); return membersConverter; } } diff --git a/src/Verify/Serialization/VerifierSettings_SerializationMaps.cs b/src/Verify/Serialization/VerifierSettings_SerializationMaps.cs index 3d3f02bee..1c9c051dc 100644 --- a/src/Verify/Serialization/VerifierSettings_SerializationMaps.cs +++ b/src/Verify/Serialization/VerifierSettings_SerializationMaps.cs @@ -109,7 +109,7 @@ public static void ScrubMembers(params string[] names) where T : notnull { InnerVerifier.ThrowIfVerifyHasBeenRun(); - serialization.IgnoreMembers(names); + serialization.ScrubMembers(names); } public static void IgnoreMember(string name) @@ -123,7 +123,7 @@ public static void ScrubMember(string name) where T : notnull { InnerVerifier.ThrowIfVerifyHasBeenRun(); - serialization.IgnoreMember(name); + serialization.ScrubMember(name); } public static void IgnoreMembers(Type declaringType, params string[] names) @@ -135,7 +135,7 @@ public static void IgnoreMembers(Type declaringType, params string[] names) public static void ScrubMembers(Type declaringType, params string[] names) { InnerVerifier.ThrowIfVerifyHasBeenRun(); - serialization.IgnoreMembers(declaringType, names); + serialization.ScrubMembers(declaringType, names); } public static void IgnoreMember(Type declaringType, string name) diff --git a/src/Verify/Serialization/VerifyJsonWriter.cs b/src/Verify/Serialization/VerifyJsonWriter.cs index c6c892d27..e4bbe941e 100644 --- a/src/Verify/Serialization/VerifyJsonWriter.cs +++ b/src/Verify/Serialization/VerifyJsonWriter.cs @@ -130,6 +130,15 @@ public override void WriteValue(CharSpan value) if (value.Contains('\n')) { + if (WriteState != Argon.WriteState.Property) + { + // In array (or any non-property) position the value already sits + // on its own indented line; removing the auto-completed char here + // would eat the ',' delimiter between items. + WriteRawValue(value); + return; + } + base.Flush(); var builderLength = builder.Length; if (value[0] != '\n')