parser.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import {LocationRange} from 'pegjs';
  2. import {t} from 'sentry/locale';
  3. import grammar from './grammar.pegjs';
  4. // This constant should stay in sync with the backend parser
  5. const MAX_OPERATORS = 10;
  6. const MAX_OPERATOR_MESSAGE = t('Maximum operators exceeded');
  7. type OperationOpts = {
  8. operator: Operator;
  9. rhs: Expression;
  10. lhs?: Expression;
  11. };
  12. type Operator = 'plus' | 'minus' | 'multiply' | 'divide';
  13. type Expression = Operation | string | number | null;
  14. export class Operation {
  15. operator: Operator;
  16. lhs?: Expression;
  17. rhs: Expression;
  18. constructor({operator, lhs = null, rhs}: OperationOpts) {
  19. this.operator = operator;
  20. this.lhs = lhs;
  21. this.rhs = rhs;
  22. }
  23. }
  24. class Term {
  25. term: Expression;
  26. location: LocationRange;
  27. constructor({term, location}: {location: LocationRange; term: Expression}) {
  28. this.term = term;
  29. this.location = location;
  30. }
  31. }
  32. export class TokenConverter {
  33. numOperations: number;
  34. errors: Array<string>;
  35. fields: Array<Term>;
  36. functions: Array<Term>;
  37. constructor() {
  38. this.numOperations = 0;
  39. this.errors = [];
  40. this.fields = [];
  41. this.functions = [];
  42. }
  43. tokenTerm = (maybeFactor: Expression, remainingAdds: Array<Operation>): Expression => {
  44. if (remainingAdds.length > 0) {
  45. remainingAdds[0].lhs = maybeFactor;
  46. return flatten(remainingAdds);
  47. }
  48. return maybeFactor;
  49. };
  50. tokenOperation = (operator: Operator, rhs: Expression): Operation => {
  51. this.numOperations += 1;
  52. if (
  53. this.numOperations > MAX_OPERATORS &&
  54. !this.errors.includes(MAX_OPERATOR_MESSAGE)
  55. ) {
  56. this.errors.push(MAX_OPERATOR_MESSAGE);
  57. }
  58. if (operator === 'divide' && rhs === '0') {
  59. this.errors.push(t('Division by 0 is not allowed'));
  60. }
  61. return new Operation({operator, rhs});
  62. };
  63. tokenFactor = (primary: Expression, remaining: Array<Operation>): Operation => {
  64. remaining[0].lhs = primary;
  65. return flatten(remaining);
  66. };
  67. tokenField = (term: Expression, location: LocationRange): Expression => {
  68. const field = new Term({term, location});
  69. this.fields.push(field);
  70. return term;
  71. };
  72. tokenFunction = (term: Expression, location: LocationRange): Expression => {
  73. const func = new Term({term, location});
  74. this.functions.push(func);
  75. return term;
  76. };
  77. }
  78. // Assumes an array with at least one element
  79. function flatten(remaining: Array<Operation>): Operation {
  80. let term = remaining.shift();
  81. while (remaining.length > 0) {
  82. const nextTerm = remaining.shift();
  83. if (nextTerm && term && nextTerm.lhs === null) {
  84. nextTerm.lhs = term;
  85. }
  86. term = nextTerm;
  87. }
  88. // Shouldn't happen, tokenTerm checks remaining and tokenFactor should have at least 1 item
  89. // This is just to help ts out
  90. if (term === undefined) {
  91. throw new Error('Unable to parse arithmetic');
  92. }
  93. return term;
  94. }
  95. type parseResult = {
  96. error: string | undefined;
  97. result: Expression;
  98. tc: TokenConverter;
  99. };
  100. export function parseArithmetic(query: string): parseResult {
  101. const tc = new TokenConverter();
  102. try {
  103. const result = grammar.parse(query, {tc});
  104. return {result, error: tc.errors[0], tc};
  105. } catch (error) {
  106. return {result: null, error: error.message, tc};
  107. }
  108. }