Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Argon.FSharp.Tests/Argon.FSharp.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<Compile Include="FSharpListConverterTests.fs" />
<Compile Include="NullTokenTests.fs" />
<PackageReference Include="FSharp.Core" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand Down
31 changes: 31 additions & 0 deletions src/Argon.FSharp.Tests/NullTokenTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module NullTokenTests

open Argon
open Xunit

type ListHolder() =
member val List: int list = [] with get, set
member val After: int = 0 with get, set

type MapHolder() =
member val Map: Map<string, int> = Map.empty with get, set
member val After: int = 0 with get, set

// Bug: FSharpListConverter.ReadJson had no Null handling, so a JSON null crashed and
// desynced the reader instead of yielding a null list.
[<Fact>]
let ``Deserialize null into FSharp list property`` () =
let result =
JsonConvert.DeserializeObject<ListHolder>("""{"List": null, "After": 7}""", FSharpListConverter())

Assert.Null(box result.List)
Assert.Equal(7, result.After)

// Bug: FSharpMapConverter.ReadJson had the same missing Null check.
[<Fact>]
let ``Deserialize null into FSharp map property`` () =
let result =
JsonConvert.DeserializeObject<MapHolder>("""{"Map": null, "After": 7}""", FSharpMapConverter())

Assert.Null(box result.Map)
Assert.Equal(7, result.After)
5 changes: 5 additions & 0 deletions src/Argon.FSharp/FSharpListConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s

public override object? ReadJson(JsonReader reader, Type type, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

var genericArgument = type.GetGenericArguments()[0];
return readList.MakeGenericMethod(genericArgument)
.Invoke(
Expand Down
5 changes: 5 additions & 0 deletions src/Argon.FSharp/FSharpMapConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public static void WriteMap<T, K>(JsonWriter writer, FSharpMap<T, K> value, Json

public override object? ReadJson(JsonReader reader, Type type, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

var arguments = type.GetGenericArguments();
return readMap.MakeGenericMethod(arguments[0], arguments[1])
.Invoke(
Expand Down
3 changes: 1 addition & 2 deletions src/Argon.JsonPath/FieldMultipleFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ public override IEnumerable<JToken> ExecuteFilter(JToken root, IEnumerable<JToke
{
yield return v;
}

if (settings?.ErrorWhenNoMatch ?? false)
else if (settings?.ErrorWhenNoMatch ?? false)
{
throw new JsonException($"Property '{name}' does not exist on JObject.");
}
Expand Down
2 changes: 1 addition & 1 deletion src/Argon.JsonPath/JPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ bool TryParseValue(out object? value)
while (currentIndex < expression.Length)
{
currentChar = expression[currentIndex];
if (currentChar is ' ' or ')')
if (currentChar is ' ' or ')' or '&' or '|')
{
var numberText = stringBuilder.ToString();

Expand Down
2 changes: 1 addition & 1 deletion src/Argon.JsonPath/PathFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ abstract class PathFilter
{
if (t is JArray a)
{
if (a.Count <= index)
if (index < 0 || a.Count <= index)
{
if (settings.ErrorWhenNoMatch)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Argon.Xml/XmlDocumentWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public IXmlNode CreateXmlDeclaration(string version, string? encoding, string? s
new XmlDeclarationWrapper(document.CreateXmlDeclaration(version, encoding, standalone));

public IXmlNode CreateXmlDocumentType(string name, string? publicId, string? systemId, string? internalSubset) =>
new XmlDocumentTypeWrapper(document.CreateDocumentType(name, publicId, systemId, null));
new XmlDocumentTypeWrapper(document.CreateDocumentType(name, publicId, systemId, internalSubset));

public IXmlNode CreateProcessingInstruction(string target, string data) =>
new XmlNodeWrapper(document.CreateProcessingInstruction(target, data));
Expand Down
29 changes: 24 additions & 5 deletions src/Argon.Xml/XmlNodeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,19 @@ void WriteGroupedNodes(JsonWriter writer, XmlNamespaceManager manager, bool writ

if (writeArray)
{
// Comments share the name "#comment" and cannot be array values. Write each as an
// inline JSON comment (which round-trips back to XML comments) instead of an empty
// array that silently drops the comment text.
if (groupedNodes[0].NodeType == XmlNodeType.Comment)
{
foreach (var node in groupedNodes)
{
writer.WriteComment(node.Value);
}

return;
}

if (writePropertyName)
{
writer.WritePropertyName(elementNames);
Expand Down Expand Up @@ -614,16 +627,16 @@ void DeserializeValue(JsonReader reader, IXmlDocument document, XmlNamespaceMana
switch (propertyName)
{
case textName:
currentNode.AppendChild(document.CreateTextNode(ConvertTokenToXmlValue(reader)!));
currentNode.AppendChild(document.CreateTextNode(ConvertTokenToXmlValue(reader) ?? ""));
return;
case cDataName:
currentNode.AppendChild(document.CreateCDataSection(ConvertTokenToXmlValue(reader)!));
currentNode.AppendChild(document.CreateCDataSection(ConvertTokenToXmlValue(reader) ?? ""));
return;
case whitespaceName:
currentNode.AppendChild(document.CreateWhitespace(ConvertTokenToXmlValue(reader)!));
currentNode.AppendChild(document.CreateWhitespace(ConvertTokenToXmlValue(reader) ?? ""));
return;
case significantWhitespaceName:
currentNode.AppendChild(document.CreateSignificantWhitespace(ConvertTokenToXmlValue(reader)!));
currentNode.AppendChild(document.CreateSignificantWhitespace(ConvertTokenToXmlValue(reader) ?? ""));
return;
default:
// processing instructions and the xml declaration start with ?
Expand Down Expand Up @@ -874,9 +887,15 @@ void ReadArrayElements(JsonReader reader, IXmlDocument document, string property

if (count == 1 && WriteArrayAttribute)
{
// Match on the decoded local name and namespace (not the raw JSON property name),
// otherwise prefixed or XML-encoded names never match and a single-element nested
// array loses its json:Array marker on round-trip.
XmlUtils.GetQualifiedNameParts(propertyName, out var qualifiedPrefix, out var localName);
var ns = qualifiedPrefix.IsNullOrEmpty() ? manager.DefaultNamespace : manager.LookupNamespace(qualifiedPrefix);

foreach (var childNode in nestedArrayElement.ChildNodes)
{
if (childNode is IXmlElement element && element.LocalName == propertyName)
if (childNode is IXmlElement element && element.LocalName == localName && element.NamespaceUri == ns)
{
AddJsonArrayAttribute(element, document);
break;
Expand Down
4 changes: 2 additions & 2 deletions src/Argon/JsonConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ public static string ToString(Guid value)
public static string ToString(TimeSpan value)
{
Span<char> destination = stackalloc char[26];
value.TryFormat(destination, out _, ['c']);
return ToString(destination, '"');
value.TryFormat(destination, out var charsWritten, ['c']);
return ToString(destination[..charsWritten], '"');
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Argon/JsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public sealed override void WriteJson(JsonWriter writer, object value, JsonSeria
throw new JsonSerializationException($"Converter cannot read JSON with the specified existing value. {typeof(T)} is required.");
}

if (reader.Value is null)
if (reader.TokenType is JsonToken.Null or JsonToken.Undefined)
{
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Argon/JsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ protected void SetNoneToken()
/// </summary>
protected void SetUndefinedToken()
{
tokenType = JsonToken.Null;
tokenType = JsonToken.Undefined;
value = null;
SetPostValueState(false);
}
Expand Down
7 changes: 5 additions & 2 deletions src/Argon/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,15 @@ public virtual IContractResolver? ContractResolver

public JsonContract ResolveContract(Type type)
{
if (contractResolver == null)
// Read through the virtual ContractResolver property so that subclasses
// (e.g. JsonSerializerProxy handed to converters) can redirect resolution.
var resolver = ContractResolver;
if (resolver == null)
{
return DefaultContractResolver.Instance.ResolveContract(type);
}

return contractResolver.ResolveContract(type);
return resolver.ResolveContract(type);
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Argon/JsonTextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,9 +2171,15 @@ object ParseNumberNaN(ReadType readType, bool matched)
/// </summary>
public override void Close()
{
if (CurrentState == State.Closed)
{
return;
}

base.Close();

BufferUtils.ReturnBuffer(charBuffer);
charBuffer = [];

if (CloseInput)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Argon/JsonTextWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ public override void WriteValue(ushort value)
public override void WriteValue(char value)
{
InternalWriteValue(JsonToken.String);
WriteValueInternal(JsonConvert.ToString(value));
WriteEscapedString([value], QuoteValue);
}

/// <summary>
Expand Down
5 changes: 1 addition & 4 deletions src/Argon/JsonWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -796,10 +796,7 @@ public virtual void WriteValue(StringBuilder? value)
return;
}

foreach (var chunk in value.GetChunks())
{
WriteValue(chunk.Span);
}
WriteValue(value.ToString());
}

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Argon/Linq/JContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,10 @@ internal bool TryAddInternal(int index, object? content, bool skipParentCheck)
var multiIndex = index;
foreach (var c in enumerable)
{
TryAddInternal(multiIndex, c, skipParentCheck);
multiIndex++;
if (TryAddInternal(multiIndex, c, skipParentCheck))
{
multiIndex++;
}
}

return true;
Expand Down
5 changes: 4 additions & 1 deletion src/Argon/Linq/JObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,10 @@ public bool TryGetValue(string propertyName, [NotNullWhen(true)] out JToken? val
return true;
}

ICollection<JToken?> IDictionary<string, JToken?>.Values => throw new NotImplementedException();
ICollection<JToken?> IDictionary<string, JToken?>.Values =>
Properties()
.Select(_ => (JToken?) _.Value)
.ToList();

#endregion

Expand Down
42 changes: 27 additions & 15 deletions src/Argon/Linq/JValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,13 @@ static int CompareBigInteger(BigInteger i1, object i2)
// check for fraction if result is two numbers are equal
if (i2 is decimal d1)
{
return 0m.CompareTo(Math.Abs(d1 - Math.Truncate(d1)));
return 0m.CompareTo(d1 - Math.Truncate(d1));
}

if (i2 is double or float)
{
var d = Convert.ToDouble(i2, InvariantCulture);
return 0d.CompareTo(Math.Abs(d - Math.Truncate(d)));
return 0d.CompareTo(d - Math.Truncate(d));
}

return result;
Expand Down Expand Up @@ -759,14 +759,33 @@ public override void WriteTo(JsonWriter writer, params IList<JsonConverter> conv
throw MiscellaneousUtils.CreateArgumentOutOfRangeException(nameof(Type), valueType, "Unexpected token type.");
}

internal override int GetDeepHashCode()
// Numeric JValues of the same JTokenType are equal by numeric value regardless of the
// backing CLR type (int vs long, decimal vs double, BigInteger). Hash a canonical double
// so GetHashCode stays consistent with Equals for exact numeric values. Values that are
// only approximately equal cannot share a hash - that is inherent to the approximate float
// comparison used by Equals.
int GetValueHashCode()
{
var valueHashCode = value?.GetHashCode() ?? 0;
if (value == null)
{
return 0;
}

// GetHashCode on an enum boxes so cast to int
return ((int) valueType).GetHashCode() ^ valueHashCode;
if (valueType is JTokenType.Integer or JTokenType.Float)
{
var d = value is BigInteger bigInteger
? (double) bigInteger
: Convert.ToDouble(value, InvariantCulture);
return d.GetHashCode();
}

return value.GetHashCode();
}

internal override int GetDeepHashCode() =>
// GetHashCode on an enum boxes so cast to int
((int) valueType).GetHashCode() ^ GetValueHashCode();

static bool ValuesEquals(JValue v1, JValue v2) =>
v1 == v2 || (v1.valueType == v2.valueType && Compare(v1.valueType, v1.value, v2.value) == 0);

Expand Down Expand Up @@ -804,15 +823,8 @@ public override bool Equals(object? obj)
/// <returns>
/// A hash code for the current <see cref="Object" />.
/// </returns>
public override int GetHashCode()
{
if (value == null)
{
return 0;
}

return value.GetHashCode();
}
public override int GetHashCode() =>
GetValueHashCode();

/// <summary>
/// Returns a <see cref="String" /> that represents this instance.
Expand Down
5 changes: 3 additions & 2 deletions src/Argon/Serialization/DefaultContractResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public class DefaultContractResolver : IContractResolver
new EncodingConverter(),
new PathInfoConverter(),
new RegexConverter(),
new EncodingConverter(),
new TimeZoneInfoConverter(),
new VersionConverter(),
new StringWriterConverter()
Expand Down Expand Up @@ -690,7 +689,9 @@ protected virtual IList<JsonProperty> CreateProperties(Type type, MemberSerializ

if (needsSort)
{
list.Sort(static (a, b) => (a.Order ?? -1).CompareTo(b.Order ?? -1));
// OrderBy is a stable sort, so properties without an explicit Order keep
// their declaration order. List.Sort (introsort) is unstable above 16 items.
return list.OrderBy(_ => _.Order ?? -1).ToList();
}

return list;
Expand Down
2 changes: 1 addition & 1 deletion src/Argon/Serialization/JsonSerializerInternalWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ static IEnumerable<DictionaryEntry> Items(IDictionary values)
{
if (contract.DictionaryKeyType == typeof(string))
{
foreach (var entry in Items(values).OrderBy(_ => ((string) _.Key, StringComparer.OrdinalIgnoreCase)))
foreach (var entry in Items(values).OrderBy(_ => (string) _.Key, StringComparer.OrdinalIgnoreCase))
{
SerializeDictionaryItem(writer, contract, member, entry.Key, entry.Value, keyContract, underlying);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Argon/Serialization/JsonSerializerProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public override IContractResolver? ContractResolver
set => serializer.ContractResolver = value;
}

public override List<JsonConverter> Converters => serializer.Converters;

public override MissingMemberHandling? MissingMemberHandling
{
get => serializer.MissingMemberHandling;
Expand Down
Loading
Loading