Skip to content

Commit 90c169e

Browse files
committed
style(core): decimal numeric compare in JsonLogic; explicit types
Quality gate failed on reliability (S1244: floating-point equality). Compare numbers as decimal instead of double, so ids and money compare exactly and there is no float equality check; also preserve long vs double literals. Use explicit types where the type is not apparent, per the repo .editorconfig (IDE0008). No behavior change; 48 green.
1 parent a6ccbc6 commit 90c169e

1 file changed

Lines changed: 38 additions & 34 deletions

File tree

src/NeoReports.Core/Configuration/JsonLogicFilter.cs

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace NeoReports.Core.Configuration;
1111
/// <c>{"var": "Name"}</c> reads a column by name; the supported operators are <c>==</c>, <c>===</c>,
1212
/// <c>!=</c>, <c>!==</c>, <c>&gt;</c>, <c>&gt;=</c>, <c>&lt;</c>, <c>&lt;=</c>, <c>and</c>, <c>or</c>,
1313
/// <c>!</c>, <c>!!</c> and <c>in</c>. Unknown operators raise a <see cref="ConfigurationException"/>.
14+
/// Numeric comparisons use <see cref="decimal"/> for exact equality on ids and money.
1415
/// </summary>
1516
public static class JsonLogicFilter
1617
{
@@ -26,15 +27,15 @@ public static Func<ReportRecord, bool> Compile(string expression)
2627
JsonElement root;
2728
try
2829
{
29-
using var document = JsonDocument.Parse(expression);
30+
using JsonDocument document = JsonDocument.Parse(expression);
3031
root = document.RootElement.Clone();
3132
}
3233
catch (JsonException ex)
3334
{
3435
throw new ConfigurationException($"Invalid JsonLogic filter: {ex.Message}", ex);
3536
}
3637

37-
var evaluate = Build(root);
38+
Func<ReportRecord, object?> evaluate = Build(root);
3839
return record => IsTruthy(evaluate(record));
3940
}
4041

@@ -45,14 +46,16 @@ public static Func<ReportRecord, bool> Compile(string expression)
4546
case JsonValueKind.Object:
4647
return BuildOperation(node);
4748
case JsonValueKind.Array:
48-
var items = node.EnumerateArray().Select(Build).ToArray();
49+
Func<ReportRecord, object?>[] items = node.EnumerateArray().Select(Build).ToArray();
4950
return record => items.Select(f => f(record)).ToList();
5051
case JsonValueKind.String:
51-
var text = node.GetString();
52+
string? text = node.GetString();
5253
return _ => text;
5354
case JsonValueKind.Number:
54-
var number = node.TryGetInt64(out var l) ? l : node.GetDouble();
55-
return _ => number;
55+
if (node.TryGetInt64(out long longValue))
56+
return _ => longValue;
57+
double doubleValue = node.GetDouble();
58+
return _ => doubleValue;
5659
case JsonValueKind.True:
5760
return _ => true;
5861
case JsonValueKind.False:
@@ -65,20 +68,20 @@ public static Func<ReportRecord, bool> Compile(string expression)
6568
private static Func<ReportRecord, object?> BuildOperation(JsonElement node)
6669
{
6770
// A JsonLogic operation is a single-key object: { "op": args }.
68-
var properties = node.EnumerateObject().ToArray();
71+
JsonProperty[] properties = node.EnumerateObject().ToArray();
6972
if (properties.Length != 1)
7073
throw new ConfigurationException("A JsonLogic operation must be an object with exactly one operator key.");
7174

72-
var op = properties[0].Name;
73-
var value = properties[0].Value;
74-
var args = value.ValueKind == JsonValueKind.Array
75+
string op = properties[0].Name;
76+
JsonElement value = properties[0].Value;
77+
Func<ReportRecord, object?>[] args = value.ValueKind == JsonValueKind.Array
7578
? value.EnumerateArray().Select(Build).ToArray()
7679
: new[] { Build(value) };
7780

7881
return op switch
7982
{
8083
"var" => BuildVar(value),
81-
"==" => Binary(args, op, (a, b) => LooseEquals(a, b)),
84+
"==" => Binary(args, op, LooseEquals),
8285
"!=" => Binary(args, op, (a, b) => !LooseEquals(a, b)),
8386
"===" => Binary(args, op, StrictEquals),
8487
"!==" => Binary(args, op, (a, b) => !StrictEquals(a, b)),
@@ -100,11 +103,11 @@ public static Func<ReportRecord, bool> Compile(string expression)
100103
// {"var": "Name"} or {"var": ["Name", default]}.
101104
string? name;
102105
JsonElement defaultElement = default;
103-
var hasDefault = false;
106+
bool hasDefault = false;
104107

105108
if (value.ValueKind == JsonValueKind.Array)
106109
{
107-
var parts = value.EnumerateArray().ToArray();
110+
JsonElement[] parts = value.EnumerateArray().ToArray();
108111
name = parts.Length > 0 ? parts[0].GetString() : null;
109112
if (parts.Length > 1)
110113
{
@@ -120,20 +123,20 @@ public static Func<ReportRecord, bool> Compile(string expression)
120123
if (string.IsNullOrEmpty(name))
121124
throw new ConfigurationException("A JsonLogic 'var' requires a column name.");
122125

123-
var column = name;
124-
var fallback = hasDefault ? Build(defaultElement) : null;
125-
return record => record.TryGet(column, out var v) ? v : fallback?.Invoke(record);
126+
string column = name;
127+
Func<ReportRecord, object?>? fallback = hasDefault ? Build(defaultElement) : null;
128+
return record => record.TryGet(column, out object? v) ? v : fallback?.Invoke(record);
126129
}
127130

128131
private static Func<ReportRecord, object?> BuildIn(Func<ReportRecord, object?>[] args, string op)
129132
{
130133
Require(args, 2, op);
131-
var needle = args[0];
132-
var haystack = args[1];
134+
Func<ReportRecord, object?> needle = args[0];
135+
Func<ReportRecord, object?> haystack = args[1];
133136
return record =>
134137
{
135-
var n = needle(record);
136-
var h = haystack(record);
138+
object? n = needle(record);
139+
object? h = haystack(record);
137140
if (h is string s)
138141
return n is not null && s.Contains(Stringify(n), StringComparison.Ordinal);
139142
if (h is IEnumerable enumerable and not string)
@@ -146,15 +149,15 @@ public static Func<ReportRecord, bool> Compile(string expression)
146149
Func<ReportRecord, object?>[] args, string op, Func<object?, object?, bool> predicate)
147150
{
148151
Require(args, 2, op);
149-
var left = args[0];
150-
var right = args[1];
152+
Func<ReportRecord, object?> left = args[0];
153+
Func<ReportRecord, object?> right = args[1];
151154
return record => predicate(left(record), right(record));
152155
}
153156

154157
private static object EvaluateAnd(Func<ReportRecord, object?>[] args, ReportRecord record)
155158
{
156159
object? last = true;
157-
foreach (var arg in args)
160+
foreach (Func<ReportRecord, object?> arg in args)
158161
{
159162
last = arg(record);
160163
if (!IsTruthy(last))
@@ -167,7 +170,7 @@ private static object EvaluateAnd(Func<ReportRecord, object?>[] args, ReportReco
167170
private static object EvaluateOr(Func<ReportRecord, object?>[] args, ReportRecord record)
168171
{
169172
object? last = false;
170-
foreach (var arg in args)
173+
foreach (Func<ReportRecord, object?> arg in args)
171174
{
172175
last = arg(record);
173176
if (IsTruthy(last))
@@ -190,24 +193,24 @@ private static bool LooseEquals(object? a, object? b)
190193
{
191194
if (a is null || b is null)
192195
return a is null && b is null;
193-
if (TryToDouble(a, out var da) && TryToDouble(b, out var db))
194-
return da.Equals(db);
196+
if (TryToNumber(a, out decimal na) && TryToNumber(b, out decimal nb))
197+
return na == nb;
195198
return string.Equals(Stringify(a), Stringify(b), StringComparison.Ordinal);
196199
}
197200

198201
private static bool StrictEquals(object? a, object? b)
199202
{
200203
if (a is null || b is null)
201204
return a is null && b is null;
202-
if (TryToDouble(a, out var da) && TryToDouble(b, out var db))
203-
return da.Equals(db);
205+
if (TryToNumber(a, out decimal na) && TryToNumber(b, out decimal nb))
206+
return na == nb;
204207
return a.GetType() == b.GetType() && a.Equals(b);
205208
}
206209

207210
private static int Compare(object? a, object? b)
208211
{
209-
if (TryToDouble(a, out var da) && TryToDouble(b, out var db))
210-
return da.CompareTo(db);
212+
if (TryToNumber(a, out decimal na) && TryToNumber(b, out decimal nb))
213+
return na.CompareTo(nb);
211214
if (a is DateTime dta && b is DateTime dtb)
212215
return dta.CompareTo(dtb);
213216
return string.CompareOrdinal(Stringify(a), Stringify(b));
@@ -219,18 +222,19 @@ private static int Compare(object? a, object? b)
219222
bool b => b,
220223
string s => s.Length > 0,
221224
IEnumerable enumerable => enumerable.Cast<object?>().Any(),
222-
_ => !TryToDouble(value, out var d) || d != 0,
225+
_ => !TryToNumber(value, out decimal n) || n != decimal.Zero,
223226
};
224227

225-
private static bool TryToDouble(object? value, out double result)
228+
// Numbers are compared as decimal so ids and money compare exactly (no floating-point equality).
229+
private static bool TryToNumber(object? value, out decimal result)
226230
{
227231
switch (value)
228232
{
229233
case sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal:
230-
result = Convert.ToDouble(value, CultureInfo.InvariantCulture);
234+
result = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
231235
return true;
232236
default:
233-
result = 0;
237+
result = decimal.Zero;
234238
return false;
235239
}
236240
}

0 commit comments

Comments
 (0)