mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
128 lines
3.2 KiB
TypeScript
128 lines
3.2 KiB
TypeScript
import type {LocationRange} from 'pegjs';
|
|
|
|
import {t} from 'sentry/locale';
|
|
|
|
import grammar from './grammar.pegjs';
|
|
|
|
// This constant should stay in sync with the backend parser
|
|
const MAX_OPERATORS = 10;
|
|
const MAX_OPERATOR_MESSAGE = t('Maximum operators exceeded');
|
|
|
|
type OperationOpts = {
|
|
operator: Operator;
|
|
rhs: Expression;
|
|
lhs?: Expression;
|
|
};
|
|
|
|
type Operator = 'plus' | 'minus' | 'multiply' | 'divide';
|
|
type Expression = Operation | string | number | null;
|
|
|
|
export class Operation {
|
|
operator: Operator;
|
|
lhs?: Expression;
|
|
rhs: Expression;
|
|
|
|
constructor({operator, lhs = null, rhs}: OperationOpts) {
|
|
this.operator = operator;
|
|
this.lhs = lhs;
|
|
this.rhs = rhs;
|
|
}
|
|
}
|
|
|
|
class Term {
|
|
term: Expression;
|
|
location: LocationRange;
|
|
|
|
constructor({term, location}: {location: LocationRange; term: Expression}) {
|
|
this.term = term;
|
|
this.location = location;
|
|
}
|
|
}
|
|
|
|
export class TokenConverter {
|
|
numOperations: number;
|
|
errors: Array<string>;
|
|
fields: Array<Term>;
|
|
functions: Array<Term>;
|
|
|
|
constructor() {
|
|
this.numOperations = 0;
|
|
this.errors = [];
|
|
this.fields = [];
|
|
this.functions = [];
|
|
}
|
|
|
|
tokenTerm = (maybeFactor: Expression, remainingAdds: Array<Operation>): Expression => {
|
|
if (remainingAdds.length > 0) {
|
|
remainingAdds[0].lhs = maybeFactor;
|
|
return flatten(remainingAdds);
|
|
}
|
|
return maybeFactor;
|
|
};
|
|
|
|
tokenOperation = (operator: Operator, rhs: Expression): Operation => {
|
|
this.numOperations += 1;
|
|
if (
|
|
this.numOperations > MAX_OPERATORS &&
|
|
!this.errors.includes(MAX_OPERATOR_MESSAGE)
|
|
) {
|
|
this.errors.push(MAX_OPERATOR_MESSAGE);
|
|
}
|
|
if (operator === 'divide' && rhs === '0') {
|
|
this.errors.push(t('Division by 0 is not allowed'));
|
|
}
|
|
return new Operation({operator, rhs});
|
|
};
|
|
|
|
tokenFactor = (primary: Expression, remaining: Array<Operation>): Operation => {
|
|
remaining[0].lhs = primary;
|
|
return flatten(remaining);
|
|
};
|
|
|
|
tokenField = (term: Expression, location: LocationRange): Expression => {
|
|
const field = new Term({term, location});
|
|
this.fields.push(field);
|
|
return term;
|
|
};
|
|
|
|
tokenFunction = (term: Expression, location: LocationRange): Expression => {
|
|
const func = new Term({term, location});
|
|
this.functions.push(func);
|
|
return term;
|
|
};
|
|
}
|
|
|
|
// Assumes an array with at least one element
|
|
function flatten(remaining: Array<Operation>): Operation {
|
|
let term = remaining.shift();
|
|
while (remaining.length > 0) {
|
|
const nextTerm = remaining.shift();
|
|
if (nextTerm && term && nextTerm.lhs === null) {
|
|
nextTerm.lhs = term;
|
|
}
|
|
term = nextTerm;
|
|
}
|
|
// Shouldn't happen, tokenTerm checks remaining and tokenFactor should have at least 1 item
|
|
// This is just to help ts out
|
|
if (term === undefined) {
|
|
throw new Error('Unable to parse arithmetic');
|
|
}
|
|
return term;
|
|
}
|
|
|
|
type ParseResult = {
|
|
error: string | undefined;
|
|
result: Expression;
|
|
tc: TokenConverter;
|
|
};
|
|
|
|
export function parseArithmetic(query: string): ParseResult {
|
|
const tc = new TokenConverter();
|
|
try {
|
|
const result = grammar.parse(query, {tc});
|
|
return {result, error: tc.errors[0], tc};
|
|
} catch (error) {
|
|
return {result: null, error: error.message, tc};
|
|
}
|
|
}
|