@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
416 lines (376 loc) • 12.9 kB
JavaScript
// @ts-nocheck
'use strict';
/**
* parseExpr() accepts any JSON object and tries to convert a token stream expression
* array into an AST like expression with CDL operator precedence.
*
* The following operators are supported:
*
* Unary: +/-
* Multiplication/Division: '*', '/'
* Addition/Subtraction: '+', '-'
* Concatenation: '||'
* Relational: '=', '<>', '>', '>=', '<', '<=', '==', '!=', 'like', 'in', 'exists', 'between and'
* Unary: 'is [not] null', 'not'
* Conditional: 'case [when then]+ [else]? end', 'and', 'or'
*
* stand-alone token: 'new'
*
* This is not an optimized LL(1) parser but a token 'sniffer'. A stream is
* cracked up in sub streams and passed down to the next higher function.
*
* Complex aggregates like case/when/else/end and between are parsed first to pass down the
* resulting sub expressions and avoiding 'and' ambiguities.
*
* Sub expressions are grouped as arrays, the final AST is an array of nested arrays.
* Alternatively, an object like AST can be produced by setting argument 'array' to false.
*
* This parser intentionally does no error handling. If a clause is malformed, it is accepted as is.
*
* @param {any} xpr A JSON object.
* @param {Object} state Object
* anno: Don't eliminate arrays with single entry in expressions (TODO?) as they are collections
* array: Bias AST representation.
* nary: return n-ary or binary tree
*/
function parseExpr(xpr, state = { array: true, nary: false }) {
state.anno = 0;
// Notes:
// - Variables `s` and `e` are used as index variables into `xpr`s for start and end.
// - xpr's are our CSN expressions, see <https://cap.cloud.sap/docs/cds/cxn>
return parseExprInt(xpr, state);
function parseExprInt(xpr, state) {
return conditionOR(...CaseWhen(Cast(xpr, state)), state);
}
function Cast(xpr, state) {
if (xpr != null && !state.array) {
if (Array.isArray(xpr))
return xpr.map(x => Cast(x, state));
if (typeof xpr === 'object') {
const castKeys = Object.keys(xpr).filter(k => k !== 'cast');
if (xpr.cast != null && castKeys.length === 1)
return { cast: [ xpr.cast, { [castKeys[0]]: xpr[castKeys[0]] } ] };
for (const n in xpr) {
// xpr could be an array with polluted prototype
if (Object.hasOwnProperty.call(xpr, n))
xpr[n] = Cast(xpr[n], state);
}
}
}
return xpr;
}
function CaseWhen(xpr) {
if (Array.isArray(xpr))
recurseIntoCases();
return [ xpr, 0, Array.isArray(xpr) ? xpr.length : 1 ];
function recurseIntoCases(casePos = -1, lvl = -1) {
for (let c = casePos + 1; c < xpr.length; c++) {
if (xpr[c] === 'case')
recurseIntoCases(c, lvl + 1);
}
if (lvl > -1) {
let endPos = casePos;
while (xpr[endPos] !== 'end' && endPos < xpr.length)
endPos++;
if (xpr[endPos] === 'end') {
const caseTree = rewriteCaseBlock(casePos, endPos);
if (casePos === 0 && endPos === xpr.length - 1)
xpr = caseTree;
else
xpr.splice(casePos, endPos - casePos + 1, caseTree);
}
}
}
/**
* @param {number} casePos
* @param {number} endPos
* @return {Array|object}
*/
function rewriteCaseBlock(casePos, endPos) {
const caseTree = state.array ? [ 'case' ] : { case: [] };
let elsePos = endPos;
let whenPos = casePos;
while (xpr[elsePos] !== 'else' && elsePos > casePos)
elsePos--;
let elseCond;
if (xpr[elsePos] === 'else') {
elseCond = xpr.slice(elsePos + 1, endPos);
endPos = elsePos;
}
while (xpr[whenPos] !== 'when' && whenPos < endPos)
whenPos++;
if (xpr[whenPos] === 'when' && whenPos - (casePos + 1) >= 1) {
const caseExpr = xpr.slice(casePos + 1, whenPos);
if (state.array)
caseTree.push(caseExpr);
else
caseTree.case.push(caseExpr.length === 1 ? caseExpr[0] : caseExpr);
}
while (xpr[whenPos] === 'when') {
const when = { when: [] };
if (state.array)
caseTree.push('when');
else
caseTree.case.push(when);
let thenPos = whenPos + 1;
while (xpr[thenPos] !== 'then' && thenPos < endPos)
thenPos++;
if (xpr[thenPos] === 'then') {
const whenExpr = xpr.slice(whenPos + 1, thenPos);
if (state.array)
caseTree.push(whenExpr);
else
when.when.push(whenExpr.length === 1 ? whenExpr[0] : whenExpr);
}
whenPos = thenPos + 1;
while (xpr[whenPos] !== 'when' && whenPos < endPos)
whenPos++;
if (xpr[whenPos] === 'when' || whenPos === endPos) {
const then = xpr.slice(thenPos + 1, whenPos);
if (state.array)
caseTree.push('then', then);
else
when.when.push(then.length === 1 ? then[0] : then);
}
}
if (elseCond) {
if (state.array)
caseTree.push('else', elseCond);
else
caseTree.case.push(elseCond.length === 1 ? elseCond[0] : elseCond);
}
if (state.array)
caseTree.push('end');
return caseTree;
}
}
function conditionOR(xpr, s, e, state) {
return binaryExpr(xpr, [ 'or' ], conditionAnd, s, e, state);
}
function conditionAnd(xpr, s, e, state) {
return binaryExpr(xpr, (xpr, s, e) => {
let a = s - 1;
let b;
do {
b = false;
for (a++; xpr[a] !== 'and' && a < e; a++) {
if (xpr[a] === 'between')
b = true;
}
} while (b && a < e);
if (!b && a < e)
return [ 1, a ];
return [ 1, -1 ];
}, conditionTerm, s, e, state);
}
function conditionTerm(xpr, s, e, state) {
if (Array.isArray(xpr)) {
if (xpr.length >= 3 && xpr[s + 1] === 'is') {
const isnull = conditionOR(xpr[s], 0, 0, state);
if (xpr[s + 2] === 'null')
return state.array ? [ isnull, 'is', 'null' ] : { isNull: isnull };
else if (xpr[s + 2] === 'not' && xpr[s + 3] === 'null')
return state.array ? [ isnull, 'is', 'not', 'null' ] : { isNotNull: isnull };
}
if (xpr[s] === 'not') {
const not = conditionTerm(xpr, s + 1, e, state);
return state.array ? [ 'not', not ] : { not };
}
if (xpr[s] === 'exists') {
const exists = conditionTerm(xpr, s + 1, e, state);
return state.array ? [ 'exists', exists ] : { exists };
}
}
return compareTerm(xpr, s, e, state);
}
function compareTerm(xpr, s, e, state) {
if (Array.isArray(xpr)) {
let i = s;
let not = false;
let between;
while (i < e && xpr[i] !== 'between')
i++;
const b = i < e ? i : -1;
while (i < e && xpr[i] !== 'and')
i++;
const a = i < e ? i : -1;
if (b >= 0) {
const token = [ 'between' ];
not = (xpr[b - 1] === 'not');
if (not)
token.splice(0, 0, 'not');
const expr = expression(xpr, s, not ? b - 1 : b, state);
between = state.array
? [ expr, ...token ]
: { between: [ expr ] };
if (a >= 0) {
const lower = expression(xpr, b + 1, a, state);
const upper = expression(xpr, a + 1, e, state);
if (state.array)
between.push(lower, 'and', upper);
else
between.between.push(lower, upper);
}
else {
const unspec = expression(xpr, b + 1, e, state);
if (state.array)
between.push(unspec);
else
between.between.push(unspec);
}
if (not && !state.array)
between = { not: between };
return between;
}
}
return binaryExpr(xpr, (xpr, s, e) => {
const token = [ '=', '<>', '>', '>=', '<', '<=', '==', '!=', 'like', 'in' ];
while (s < e && !token.includes(xpr[s]))
s++;
if (s < e) {
if (xpr[s - 1] === 'not' && (xpr[s] === 'in' || xpr[s] === 'like'))
return [ 2, s - 1 ];
return [ 1, s ];
}
return [ 1, -1 ];
}, expression, s, e, state);
}
function expression(xpr, s, e, state) {
return binaryExpr(xpr, [ '||' ], exprAddSub, s, e, state);
}
function exprAddSub(xpr, s, e, state) {
return binaryExpr(xpr, (xpr, s, e) => {
const skips = [ '+', '-', '*', '/' ];
let found = false;
let p = s;
while (!found && p < e) {
found = ((xpr[p] === '+' || xpr[p] === '-') && p > s && !skips.includes(xpr[p - 1]) && p < e);
if (!found)
p++;
}
if (found)
return [ 1, p ];
return [ 1, -1 ];
}, exprMulDiv, s, e, state);
}
function exprMulDiv(xpr, s, e, state) {
return binaryExpr(xpr, [ '*', '/' ], (state.array ? unary : dot), s, e, state);
}
function dot(xpr, s, e, state) {
return binaryExpr(xpr, [ '.' ], unary, s, e, state);
}
function unary(xpr, s, e, state) {
if (Array.isArray(xpr)) {
if (xpr[s] === '+' || xpr[s] === '-' || (!state.array && xpr[s] === 'new')) {
if (state.array)
return [ xpr[s], unary(xpr, s + 1, e, state) ];
return { [xpr[s]]: unary(xpr, s + 1, e, state) };
}
}
return terminal(xpr, s, e, state);
}
function terminal(xpr, s, e, state) {
const csnarray = [
'ref', 'args', 'columns', 'keys', 'expand', 'inline',
'requires', 'extensions', 'includes', 'excluding',
];
const xprarray = [
'xpr', 'on', 'where', 'orderBy', 'groupBy', 'having',
];
if (Array.isArray(xpr) && xpr.length > 0) {
if (e - s <= 1 && state.anno === 0 && typeof xpr[e - 1] !== 'string')
return parseExprInt(xpr[e - 1], state);
return xpr.slice(s, e).map(ix => parseExprInt(ix, state));
}
if (typeof xpr === 'object') {
// if(xpr?.func && funkyfuncs.includes(xpr?.func))
// return xpr;
for (const n in xpr) {
// xpr could be an array with polluted prototype
if (!Object.hasOwnProperty.call(xpr, n))
continue;
const x = xpr[n];
const isAnno = n[0] === '@' && isSimpleAnnoValue(x);
if (isAnno)
state.anno++;
if (Array.isArray(x)) {
if (csnarray.includes(n) || state.anno !== 0)
xpr[n] = x.map(ix => parseExprInt(ix, state));
else if (xprarray.includes(n) && x.length === 1)
xpr[n] = x.map(ix => parseExprInt(ix, state));
else
xpr[n] = parseExprInt(x, state);
}
else {
xpr[n] = parseExprInt(x, state);
}
if (isAnno)
state.anno--;
}
}
return xpr;
}
function binaryExpr(xpr, token, next, s, e, state) {
const naryExpr = [];
let not = false;
if (Array.isArray(xpr)) {
let [ tl, p ] = findToken(s, e);
if (p >= 0) {
let lhs = next(xpr, s, p, state);
naryExpr.push(lhs);
let op = xpr.slice(p, p + tl);
s = p + tl;
[ tl, p ] = findToken(s, e);
while (p >= 0) {
const rhs = next(xpr, s, p, state);
naryExpr.push(...op, rhs);
if (state.array) {
lhs = [ lhs, ...op, rhs ];
}
else {
not = op.length > 1 && op[0] === 'not';
if (not)
op = op.slice(1);
lhs = (not
? { not: { [op.join('')]: [ lhs, rhs ] } }
: { [op.join('')]: [ lhs, rhs ] });
}
op = xpr.slice(p, p + tl);
s = p + tl;
[ tl, p ] = findToken(s, e);
}
let rhs = next(xpr, s, e, state);
if (Array.isArray(rhs) && rhs.length === 0)
rhs = undefined;
naryExpr.push(...op, rhs);
if (state.array)
return (state.nary ? naryExpr : [ lhs, ...op, rhs ]);
not = op.length > 1 && op[0] === 'not';
if (not)
op = op.slice(1);
return (not
? { not: { [op.join('')]: [ lhs, rhs ] } }
: { [op.join('')]: [ lhs, rhs ] });
}
}
return next(xpr, s, e, state);
function findToken(s, e) {
if (typeof token === 'function')
return token(xpr, s, e);
while (s < e && !token.includes(xpr[s]))
s++;
if (s < e)
return [ 1, s ];
return [ 1, -1 ];
}
}
}
function isSimpleAnnoValue(val) {
// Expressions as annotation values always have a `=` and another property.
// TODO: There must be at least one known expression property, otherwise
// it could be `type: 'unchecked'`.
return !val?.['='] || Object.keys(val) < 2;
}
module.exports = {
parseExpr,
};