evaluator.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // To evaluate a result of the search syntax, we flatten the AST,
  2. // transform it to postfix notation which gets rid of parenthesis and tokens
  3. // that do not hold any value as they cannot be evaluated and then evaluate
  4. // the postfix notation.
  5. import {
  6. BooleanOperator,
  7. Token,
  8. type TokenResult,
  9. } from 'sentry/components/searchSyntax/parser';
  10. export type ProcessedTokenResult =
  11. | TokenResult<Token>
  12. | {type: 'L_PAREN'}
  13. | {type: 'R_PAREN'};
  14. export function toFlattened(tokens: TokenResult<Token>[]): ProcessedTokenResult[] {
  15. const flattened_result: ProcessedTokenResult[] = [];
  16. function flatten(token: TokenResult<Token>): void {
  17. switch (token.type) {
  18. case Token.SPACES:
  19. case Token.VALUE_BOOLEAN:
  20. case Token.VALUE_DURATION:
  21. case Token.VALUE_ISO_8601_DATE:
  22. case Token.VALUE_SIZE:
  23. case Token.VALUE_NUMBER_LIST:
  24. case Token.VALUE_NUMBER:
  25. case Token.VALUE_TEXT:
  26. case Token.VALUE_TEXT_LIST:
  27. case Token.VALUE_RELATIVE_DATE:
  28. case Token.VALUE_PERCENTAGE:
  29. case Token.KEY_SIMPLE:
  30. return;
  31. case Token.LOGIC_GROUP:
  32. flattened_result.push({type: 'L_PAREN'});
  33. for (const child of token.inner) {
  34. // Logic groups are wrapped in parenthesis,
  35. // but those parenthesis are not actual tokens returned by the parser
  36. flatten(child);
  37. }
  38. flattened_result.push({type: 'R_PAREN'});
  39. break;
  40. case Token.LOGIC_BOOLEAN:
  41. flattened_result.push(token);
  42. break;
  43. default:
  44. flattened_result.push(token);
  45. break;
  46. }
  47. }
  48. for (let i = 0; i < tokens.length; i++) {
  49. flatten(tokens[i]);
  50. }
  51. return flattened_result;
  52. }
  53. // At this point we have a flat list of groups that we can evaluate, however since the syntax allows
  54. // implicit ANDs, we should still insert those as it will make constructing a valid AST easier
  55. export function insertImplicitAND(
  56. tokens: ProcessedTokenResult[]
  57. ): ProcessedTokenResult[] {
  58. const with_implicit_and: ProcessedTokenResult[] = [];
  59. const AND = {
  60. type: Token.LOGIC_BOOLEAN,
  61. value: BooleanOperator.AND,
  62. text: 'AND',
  63. location: null as unknown as PEG.LocationRange,
  64. invalid: null,
  65. } as TokenResult<Token>;
  66. for (let i = 0; i < tokens.length; i++) {
  67. const next = tokens[i + 1];
  68. with_implicit_and.push(tokens[i]);
  69. // If current is not a logic boolean and next is not a logic boolean, insert an implicit AND.
  70. if (
  71. next &&
  72. next.type !== Token.LOGIC_BOOLEAN &&
  73. tokens[i].type !== Token.LOGIC_BOOLEAN &&
  74. tokens[i].type !== 'L_PAREN' &&
  75. next.type !== 'R_PAREN'
  76. ) {
  77. with_implicit_and.push(AND);
  78. }
  79. }
  80. return with_implicit_and;
  81. }
  82. function processTokenResults(tokens: TokenResult<Token>[]): ProcessedTokenResult[] {
  83. const flattened = toFlattened(tokens);
  84. const withImplicitAnd = insertImplicitAND(flattened);
  85. return withImplicitAnd;
  86. }
  87. function isBooleanOR(token: ProcessedTokenResult): boolean {
  88. return token?.type === Token.LOGIC_BOOLEAN && token?.value === BooleanOperator.OR;
  89. }
  90. // https://en.wikipedia.org/wiki/Shunting_yard_algorithm
  91. export function toPostFix(tokens: TokenResult<Token>[]): ProcessedTokenResult[] {
  92. const processed = processTokenResults(tokens);
  93. const result: ProcessedTokenResult[] = [];
  94. const stack: ProcessedTokenResult[] = [];
  95. for (const token of processed) {
  96. switch (token.type) {
  97. case Token.LOGIC_BOOLEAN: {
  98. while (
  99. // Establishes higher precedence for OR operators.
  100. // Whenever the current stack top operator is an OR,
  101. stack.length > 0 &&
  102. token.value === BooleanOperator.AND &&
  103. stack[stack.length - 1].type === Token.LOGIC_BOOLEAN &&
  104. stack[stack.length - 1].type !== 'L_PAREN' &&
  105. isBooleanOR(stack[stack.length - 1])
  106. ) {
  107. result.push(stack.pop()!);
  108. }
  109. stack.push(token);
  110. break;
  111. }
  112. case 'L_PAREN':
  113. stack.push(token);
  114. break;
  115. case 'R_PAREN': {
  116. while (stack.length > 0) {
  117. const top = stack[stack.length - 1];
  118. if (top.type === 'L_PAREN') {
  119. stack.pop();
  120. break;
  121. }
  122. // we dont need to check for balanced parens as the parser grammar will only succeed
  123. // in parsing the input string if the parens are balanced.
  124. result.push(stack.pop() as ProcessedTokenResult);
  125. }
  126. break;
  127. }
  128. default: {
  129. result.push(token);
  130. break;
  131. }
  132. }
  133. }
  134. // Push the remained of the operators to the output
  135. while (stack.length > 0) {
  136. result.push(stack.pop() as ProcessedTokenResult);
  137. }
  138. return result;
  139. }