fix(advanced-search): D — correct @/| operator precedence (OR binds looser than AND)

Extracts a pure, testable AdvancedSearchUtilities.EvaluateBoolean that
splits on the outermost '|' (OR) before '@' (AND), so `a@b|c` now
evaluates as `(a&&b)||c` instead of the previous `a&&(b||c)`.
AdvancedSearchThreadWorker.EvaluateRecursive delegates to it, supplying
the entity-aware leaf evaluator.
This commit is contained in:
Kamron Batman 2026-07-19 21:42:59 -07:00
parent 8aa797a586
commit ed5e8d243f
3 changed files with 35 additions and 18 deletions

View file

@ -39,4 +39,19 @@ public class AdvancedSearchUtilitiesTests
{
Assert.True(AdvancedSearchUtilities.CompareValues(typeof(Layer), (byte)Layer.OneHanded, "onehanded", "="));
}
[Theory]
// leaf value is "T"/"F"; evalLeaf returns leaf=="T"
[InlineData("T", true)]
[InlineData("F", false)]
[InlineData("F@F|T", true)] // (F&&F)||T = T (buggy code gave F&&(F||T)=F)
[InlineData("T|F@F", true)] // T||(F&&F) = T (buggy code gave (T||F)&&F=F)
[InlineData("T@F", false)]
[InlineData("T@T", true)]
[InlineData("F|F", false)]
public void EvaluateBoolean_Precedence(string expr, bool expected)
{
var result = AdvancedSearchUtilities.EvaluateBoolean(expr, leaf => leaf == "T");
Assert.Equal(expected, result);
}
}

View file

@ -317,24 +317,8 @@ public class AdvancedSearchThreadWorker
}
}
private static bool EvaluateRecursive(IEntity entity, ReadOnlySpan<char> span)
{
var atIndex = span.IndexOf('@');
var orIndex = span.IndexOf('|');
if (atIndex == -1 && orIndex == -1)
{
return EvaluateSingleExpression(entity, span);
}
var result = atIndex != -1;
var splitIndex = result ? atIndex : orIndex;
var left = EvaluateRecursive(entity, span.Slice(0, splitIndex));
var right = EvaluateRecursive(entity, span.Slice(splitIndex + 1));
return result ? left && right : left || right;
}
private static bool EvaluateRecursive(IEntity entity, ReadOnlySpan<char> span) =>
AdvancedSearchUtilities.EvaluateBoolean(span, leaf => EvaluateSingleExpression(entity, leaf));
private static bool EvaluateSingleExpression(IEntity entity, ReadOnlySpan<char> expression)
{

View file

@ -360,6 +360,24 @@ public static class AdvancedSearchUtilities
return false;
}
// OR ('|') binds looser than AND ('@'); split on the outermost OR first, then AND.
internal static bool EvaluateBoolean(ReadOnlySpan<char> expr, Func<string, bool> evalLeaf)
{
var orIndex = expr.IndexOf('|');
if (orIndex != -1)
{
return EvaluateBoolean(expr[..orIndex], evalLeaf) || EvaluateBoolean(expr[(orIndex + 1)..], evalLeaf);
}
var andIndex = expr.IndexOf('@');
if (andIndex != -1)
{
return EvaluateBoolean(expr[..andIndex], evalLeaf) && EvaluateBoolean(expr[(andIndex + 1)..], evalLeaf);
}
return evalLeaf(expr.Trim().ToString());
}
private static int GetEnumSize(Type enumType) =>
Type.GetTypeCode(Enum.GetUnderlyingType(enumType)) switch
{