From ed5e8d243f8ca55a30331bf613e75e5cd31d8919 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:42:59 -0700 Subject: [PATCH] =?UTF-8?q?fix(advanced-search):=20D=20=E2=80=94=20correct?= =?UTF-8?q?=20@/|=20operator=20precedence=20(OR=20binds=20looser=20than=20?= =?UTF-8?q?AND)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../AdvancedSearchUtilitiesTests.cs | 15 ++++++++++++++ .../AdvancedSearchThreadWorker.cs | 20 ++----------------- .../AdvancedSearchUtilities.cs | 18 +++++++++++++++++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs index 986e0d724..412e5c1e0 100644 --- a/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/AdvancedSearch/AdvancedSearchUtilitiesTests.cs @@ -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); + } } diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs index 57ec309f1..660df1058 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchThreadWorker.cs @@ -317,24 +317,8 @@ public class AdvancedSearchThreadWorker } } - private static bool EvaluateRecursive(IEntity entity, ReadOnlySpan 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 span) => + AdvancedSearchUtilities.EvaluateBoolean(span, leaf => EvaluateSingleExpression(entity, leaf)); private static bool EvaluateSingleExpression(IEntity entity, ReadOnlySpan expression) { diff --git a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs index 9532fe20a..16355a249 100644 --- a/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs +++ b/Projects/UOContent/Engines/Advanced Search/AdvancedSearchUtilities.cs @@ -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 expr, Func 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 {