Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ dotnet_diagnostic.SA1633.severity = none
dotnet_diagnostic.SA1642.severity = suggestion

# CSharpGuidelines

# Purpose: Argument for parameter calls nested method
# Rationale: Modern debuggers allow stepping into nested calls, so no need to avoid them.
dotnet_diagnostic.AV1580.severity = none

dotnet_diagnostic.AV1561.max_parameter_count = 5
# AV1008: Class should be non-static or its name should be suffixed with Extensions
dotnet_diagnostic.AV1008.severity = none
Expand Down Expand Up @@ -262,6 +267,16 @@ dotnet_diagnostic.AV2305.severity = none
# AV2407: Region should be removed
dotnet_diagnostic.AV2407.severity = none

# Meziantou.Analyzer

# Purpose: Use string.Equals instead of Equals operator
# Rationale: Does not improve readability
dotnet_diagnostic.MA0006.severity = none

# Purpose: Regular expressions should not be vulnerable to Denial of Service attacks
# Rationale: See https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md
dotnet_diagnostic.MA0009.severity = suggestion

# ReSharper/Rider
# Convert lambda expression to method group
resharper_convert_closure_to_method_group_highlighting=none
2 changes: 1 addition & 1 deletion Build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageReference Include="Nuke.Common" Version="9.0.4" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
<PackageDownload Include="GitVersion.Tool" Version="[5.12.0]" />
<PackageDownload Include="ReportGenerator" Version="[5.1.23]" />
<PackageDownload Include="ReportGenerator" Version="[5.5.0]" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project>
<PropertyGroup>
<LangVersion>13.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' != 'net47'">
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
<RunAnalyzers>false</RunAnalyzers>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net47'">
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>9.0</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net47'">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.8.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.253">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions FluentAssertions.Json.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32210.308
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5A0B454-22D4-4694-99FF-D6A8B7DE7DA3}"
ProjectSection(SolutionItems) = preProject
Expand Down
139 changes: 75 additions & 64 deletions Src/FluentAssertions.Json/Common/JTokenExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,102 @@
using System.Linq;
using Newtonsoft.Json.Linq;

namespace FluentAssertions.Json.Common
namespace FluentAssertions.Json.Common;

internal static class JTokenExtensions
{
internal static class JTokenExtensions
{
private static readonly JTokenComparer Comparer = new();
private static readonly JTokenComparer Comparer = new();

/// <summary>
/// Recursively sorts the properties of JObject instances by name and
/// the elements of JArray instances by their string representation,
/// producing a normalized JToken for consistent comparison
/// </summary>
public static JToken Normalize(this JToken token)
/// <summary>
/// Recursively sorts the properties of JObject instances by name and
/// the elements of JArray instances by their string representation,
/// producing a normalized JToken for consistent comparison
/// </summary>
public static JToken Normalize(this JToken token)
{
return token switch
{
return token switch
{
JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name).Select(p => new JProperty(p.Name, Normalize(p.Value)))),
JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)),
_ => token
};
}
JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => new JProperty(p.Name, Normalize(p.Value)))),
JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)),
_ => token
};
}

private sealed class JTokenComparer : IComparer<JToken>
private sealed class JTokenComparer : IComparer<JToken>
{
public int Compare(JToken x, JToken y)
{
public int Compare(JToken x, JToken y)
if (ReferenceEquals(x, y))
{
if (ReferenceEquals(x, y))
return 0;

if (x is null)
return -1;

if (y is null)
return 1;

var typeComparison = x.Type.CompareTo(y.Type);
if (typeComparison != 0)
return typeComparison;

return x switch
{
JArray a => Compare(a, (JArray)y),
JObject o => Compare(o, (JObject)y),
JProperty p => Compare(p, (JProperty)y),
JValue v => Compare(v, (JValue)y),
JConstructor c => Compare(c, (JConstructor)y),
_ => string.Compare(x.ToString(), y.ToString(), StringComparison.Ordinal)
};
return 0;
}

private static int Compare(JValue x, JValue y) => Comparer<object>.Default.Compare(x.Value, y.Value);

private static int Compare(JConstructor x, JConstructor y)
if (x is null)
{
var nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal);
return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y);
return -1;
}

private static int Compare(JContainer x, JContainer y)
if (y is null)
{
var countComparison = x.Count.CompareTo(y.Count);
if (countComparison != 0)
return countComparison;
return 1;
}

return x
.Select((t, i) => Comparer.Compare(t, y[i]))
.FirstOrDefault(itemComparison => itemComparison != 0);
var typeComparison = x.Type.CompareTo(y.Type);
if (typeComparison != 0)
{
return typeComparison;
}

private static int Compare(JObject x, JObject y)
return x switch
{
var countComparison = x.Count.CompareTo(y.Count);
if (countComparison != 0)
return countComparison;
JArray a => Compare(a, (JArray)y),
JObject o => Compare(o, (JObject)y),
JProperty p => Compare(p, (JProperty)y),
JValue v => Compare(v, (JValue)y),
JConstructor c => Compare(c, (JConstructor)y),
_ => string.CompareOrdinal(x.ToString(), y.ToString())
};
}

private static int Compare(JValue x, JValue y) => Comparer<object>.Default.Compare(x.Value, y.Value);

private static int Compare(JConstructor x, JConstructor y)
{
var nameComparison = string.CompareOrdinal(x.Name, y.Name);
return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y);
}

return x.Properties()
.OrderBy(p => p.Name)
.Zip(y.Properties().OrderBy(p => p.Name), (px, py) => Compare(px, py))
.FirstOrDefault(itemComparison => itemComparison != 0);
private static int Compare(JContainer x, JContainer y)
{
var countComparison = x.Count.CompareTo(y.Count);
if (countComparison != 0)
{
return countComparison;
}

private static int Compare(JProperty x, JProperty y)
return x
.Select((t, i) => Comparer.Compare(t, y[i]))
.FirstOrDefault(itemComparison => itemComparison != 0);
}

private static int Compare(JObject x, JObject y)
{
var countComparison = x.Count.CompareTo(y.Count);
if (countComparison != 0)
{
var nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal);
return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value);
return countComparison;
}

return x.Properties()
.OrderBy(p => p.Name, StringComparer.Ordinal)
.Zip(y.Properties().OrderBy(p => p.Name, StringComparer.Ordinal), (px, py) => Compare(px, py))
.FirstOrDefault(itemComparison => itemComparison != 0);
}

private static int Compare(JProperty x, JProperty y)
{
var nameComparison = string.CompareOrdinal(x.Name, y.Name);
return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value);
}
}
}
23 changes: 11 additions & 12 deletions Src/FluentAssertions.Json/Common/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
namespace FluentAssertions.Json.Common
namespace FluentAssertions.Json.Common;

internal static class StringExtensions
{
internal static class StringExtensions
{
/// <summary>
/// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts.
/// </summary>
public static string EscapePlaceholders(this string value) =>
value.Replace("{", "{{").Replace("}", "}}");
/// <summary>
/// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts.
/// </summary>
public static string EscapePlaceholders(this string value) =>
value.Replace("{", "{{").Replace("}", "}}");

public static string RemoveNewLines(this string @this)
{
return @this.Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\r\\n", string.Empty);
}
public static string RemoveNewLines(this string @this)
{
return @this.Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\r\\n", string.Empty);
}
}
47 changes: 47 additions & 0 deletions Src/FluentAssertions.Json/Difference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;

namespace FluentAssertions.Json;

internal sealed class Difference
{
public Difference(DifferenceKind kind, JPath path, object actual, object expected)
: this(kind, path)
{
Actual = actual;
Expected = expected;
}

public Difference(DifferenceKind kind, JPath path)
{
Kind = kind;
Path = path;
}

private DifferenceKind Kind { get; }

private JPath Path { get; }

private object Actual { get; }

private object Expected { get; }

public override string ToString()
{
return Kind switch
{
DifferenceKind.ActualIsNull => "is null",
DifferenceKind.ExpectedIsNull => "is not null",
DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}",
DifferenceKind.OtherName => $"has a different name at {Path}",
DifferenceKind.OtherValue => $"has a different value at {Path}",
DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}",
DifferenceKind.ActualMissesProperty => $"misses property {Path}",
DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}",
DifferenceKind.ActualMissesElement => $"misses expected element {Path}",
DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order",
#pragma warning disable MA0015 // Specify the parameter name in ArgumentException
_ => throw new ArgumentOutOfRangeException(),
#pragma warning restore MA0015 // Specify the parameter name in ArgumentException
};
}
}
15 changes: 15 additions & 0 deletions Src/FluentAssertions.Json/DifferenceKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace FluentAssertions.Json;

internal enum DifferenceKind
{
ActualIsNull,
ExpectedIsNull,
OtherType,
OtherName,
OtherValue,
DifferentLength,
ActualMissesProperty,
ExpectedMissesProperty,
ActualMissesElement,
WrongOrder
}
Loading