Browse Source

feat(search): convert search tokens to rpn (#70032)

Converts search tokens to RPN with or having higher precedence as and
Jonas 10 months ago
parent
commit
29883d3766

+ 32 - 0
static/app/components/searchSyntax/evaluator.spec.tsx

@@ -2,6 +2,7 @@ import {
   insertImplicitAND,
   insertImplicitAND,
   type ProcessedTokenResult,
   type ProcessedTokenResult,
   toFlattened,
   toFlattened,
+  toPostFix,
 } from 'sentry/components/searchSyntax/evaluator';
 } from 'sentry/components/searchSyntax/evaluator';
 import {
 import {
   parseSearch,
   parseSearch,
@@ -197,4 +198,35 @@ describe('Search Syntax Evaluator', () => {
       });
       });
     });
     });
   });
   });
+
+  describe('postfix', () => {
+    it('simple operator', () => {
+      const tokens = toPostFix(parseSearch('is:unresolved is:resolved')!);
+      expect(tokensToString(tokens)).toBe('is:unresolved is:resolved AND');
+    });
+
+    describe('logical groups', () => {
+      it('parens', () => {
+        const tokens = toPostFix(parseSearch('is:unresolved (is:resolved OR is:dead)')!);
+        expect(tokensToString(tokens)).toBe('is:unresolved is:resolved is:dead OR AND');
+      });
+
+      it('or', () => {
+        const tokens = toPostFix(
+          parseSearch('is:unresolved OR (is:resolved AND is:dead)')!
+        );
+        expect(tokensToString(tokens)).toBe('is:unresolved is:resolved is:dead AND OR');
+      });
+      it('and', () => {
+        const tokens = toPostFix(
+          parseSearch('is:unresolved AND (is:resolved AND is:dead)')!
+        );
+        expect(tokensToString(tokens)).toBe('is:unresolved is:resolved is:dead AND AND');
+      });
+      it('parentheses respect precedence', () => {
+        const tokens = toPostFix(parseSearch('is:unresolved OR (is:dead AND is:alive)')!);
+        expect(tokensToString(tokens)).toBe('is:unresolved is:dead is:alive AND OR');
+      });
+    });
+  });
 });
 });

+ 67 - 0
static/app/components/searchSyntax/evaluator.tsx

@@ -90,3 +90,70 @@ export function insertImplicitAND(
 
 
   return with_implicit_and;
   return with_implicit_and;
 }
 }
+
+function processTokenResults(tokens: TokenResult<Token>[]): ProcessedTokenResult[] {
+  const flattened = toFlattened(tokens);
+  const withImplicitAnd = insertImplicitAND(flattened);
+
+  return withImplicitAnd;
+}
+
+function isBooleanOR(token: ProcessedTokenResult): boolean {
+  return (
+    token && token.type === Token.LOGIC_BOOLEAN && token.value === BooleanOperator.OR
+  );
+}
+
+// https://en.wikipedia.org/wiki/Shunting_yard_algorithm
+export function toPostFix(tokens: TokenResult<Token>[]): ProcessedTokenResult[] {
+  const processed = processTokenResults(tokens);
+
+  const result: ProcessedTokenResult[] = [];
+  // Operator stack
+  const stack: ProcessedTokenResult[] = [];
+
+  for (const token of processed) {
+    switch (token.type) {
+      case Token.LOGIC_BOOLEAN: {
+        while (
+          stack.length > 0 &&
+          token.value === BooleanOperator.AND &&
+          stack[stack.length - 1].type === Token.LOGIC_BOOLEAN &&
+          stack[stack.length - 1].type !== 'L_PAREN' &&
+          isBooleanOR(stack[stack.length - 1])
+        ) {
+          result.push(stack.pop() as ProcessedTokenResult);
+        }
+        stack.push(token);
+        break;
+      }
+      case 'L_PAREN':
+        stack.push(token);
+        break;
+      case 'R_PAREN': {
+        while (stack.length > 0) {
+          const top = stack[stack.length - 1];
+          if (top.type === 'L_PAREN') {
+            stack.pop();
+            break;
+          }
+          // we dont need to check for balanced parens as the parser grammar will only succeed
+          // in parsing the input string if the parens are balanced.
+          result.push(stack.pop() as ProcessedTokenResult);
+        }
+        break;
+      }
+      default: {
+        result.push(token);
+        break;
+      }
+    }
+  }
+
+  // Push the remained of the operators to the output
+  while (stack.length > 0) {
+    result.push(stack.pop() as ProcessedTokenResult);
+  }
+
+  return result;
+}